本文將繼續引入更多的 webpack
配置,建議先閱讀【webpack 系列】基礎篇的內容。如果發現文中有任何錯誤,請在評論區指正。本文所有代碼都可在 github 找到。
打包多頁應用
之前我們配置的是一個單頁的應用,但是我們的應用可能需要是個多頁應用。下面我們來進行多頁應用的 webpack
配置。
先看一下我們的目錄結構
├── public
│ ├── detail.html
│ └── index.html
├── src
│ ├── detail-entry.js
│ ├── index-entry.js
public
下面有 index.html
和 detail.html
兩個頁面,對應 src
下面有 index-entry.js
和 detail-entry.js
兩個入口文件。
在webpack.config.js
配置
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
// ...
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index-entry.js'),
detail: path.resolve(__dirname, 'src/detail-entry.js')
},
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[hash:6].js', // 輸出文件名
},
plugins: [
// index.html
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'), // 指定模板文件,不指定會生成默認的 index.html 文件
filename: 'index.html', // 打包后的文件名
chunks: ['index'] // 指定引入的 js 文件,對應在 entry 配置的 chunkName
}),
// detail.html
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/detail.html'), // 指定模板文件,不指定會生成默認的 index.html 文件
filename: 'detail.html', // 打包后的文件名
chunks: ['detail'] // 指定引入的 js 文件,對應在 entry 配置的 chunkName
}),
// 打包前自動清除dist目錄
new CleanWebpackPlugin()
]
}
npm run build
之后可以看到生成的 dist
目錄如下
dist
├── assets
│ └── author_ee489e.jpg
├── detail.dbcb15.js
├── detail.dbcb15.js.map
├── detail.html
├── index.dbcb15.js
├── index.dbcb15.js.map
└── index.html
index.html
頁面中已經引入了打包好的 index.dbcb15.js
文件,detail.html
文件也已經引入了 detail.dbcb15.js
文件。更多配置請查看 html-webpack-plugin。
將 CSS 樣式單獨抽離生成文件
webpack4
對 css
模塊支持的完善以及在處理 css
文件提取的方式上也做了些調整,由 mini-css-extract-plugin
來代替之前使用的 extract-text-webpack-plugin
,使用方式很簡單。
該插件將 css
提取到單獨的文件中,為每個包含 css
的 js
文件創建一個 css
文件,支持 css
和 sourcemap
的按需加載。
與 extract-text-webpack-plugin
相比有如下優點
- 異步加載
- 沒有重復的編譯(性能)
- 更容易使用
- 特定於
css
安裝 extract-text-webpack-plugin
npm i -D mini-css-extract-plugin
配置 webpack.config.js
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ...
module.exports = {
// ...
module: {
rules: [
{
test: /\.(c|le)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
exclude: /node_modules/
},
{
test: /\.sass$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
exclude: /node_modules/
},
// ...
]
},
plugins: [
// ...
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css'
})
]
}
npm run build
之后會發現在 dist/css
目錄有了抽離出來的 css
文件了。
這時我們發現兩個問題:
- 打包生成的
css
文件沒有進行壓縮。 - 所有文件命名的
hash
部分都是一樣的,存在緩存問題。
對 css 文件進行壓縮
通過 optimize-css-assets-webpack-plugin
插件壓縮 css
代碼
npm i -D optimize-css-assets-webpack-plugin
配置 webpack.config.js
// webpack.config.js
//...
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
//...
plugins: [
//...
new OptimizeCssPlugin()
]
}
這樣就可以對 css
文件進行壓縮了。
對於第二個問題,我們首先需要了解下 hash
、chunkHash
、contentHash
的區別。
hash、chunkhash、contenthash 的區別和使用
hash
hash
是基於整個 module identifier
序列計算得到的,webpack 默認為給各個模塊分配一個 id
以作標識,用來處理模塊之間的依賴關系,默認的 id
命名規則是根據模塊引入的順序賦予一個整數(1
、2
、3
...)。任意修改、增加、刪除一個模塊的依賴,都會對整個 id
序列造成影響,從而改變 hash
值。也就是每次修改或者增刪任何一個文件,所有文件名的 hash
值都將改變,整個項目的文件緩存都將失效。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[hash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css'
})
可以看到打包后的 js
和 css
文件的 hash
值是一樣的,所以對於沒有發生改變的模塊而言,這樣做是不合理的。
當然可以看到,對於圖片等資源該 hash
還是可以生成一個唯一值的。
chunkhash
chunkhash
根據不同的入口文件進行依賴文件解析、構建對應的 chunk
,生成對應的哈希值。我們將 filename
配置成 chunkhash
來看一下打包的結果。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[chunkhash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash:6].css'
})
可以看到此時打包之后的 index.js
和 detail.js
的 chunkhash
是不一樣的。但是會發現 index.js
和 index.css
以及 detail.js
和 detail.css
的 chunkhash
是一致的,並且任意改動 js
或者 css
都會引起對應的 css
和 js
文件的 chunkhash
的改變,這是不合理的。所以這里抽離出來的 css
文件將使用 contenthash
,來區分 css
文件和 js
文件的更新。
contenthash
contenthash
是針對文件內容級別的,只有你自己模塊的內容變了,那么 hash
值才改變。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[chunkhash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css'
})
OK
,可以看到分離出來的 css
文件已經和入口文件的 hash
值區分開了。
如何使用
為了實現理想的緩存,我們一般這樣使用他們:
JS
文件使用chunkhash
- 抽離的
CSS
樣式文件使用contenthash
gif|png|jpe?g|eot|woff|ttf|svg|pdf
等使用hash
按需加載
很多時候我們並不需要在一個頁面中一次性加載所有的 js
或者 css
文件,而是應該是需要用到時才去加載相應的 js
或者 css
文件。
import()
比如,現在我們需要點擊一個按鈕才會使用對應的 js
、css
文件,需要 import()
語法:
// index-entry.js
import './index.sass';
//...
const handle = () => import('./handle');
const handle2 = () => import('./handle2');
document.querySelector('#btn').onclick = () => {
handle().then(module => {
module.handleClick();
});
handle2().then(module => {
module.default();
});
}
// handle.js
import './handle.css';
export function handleClick () {
console.log('handleClick');
}
// handle2.js
export default function handleClick () {
console.log('handleClick2');
}
npm run build
可以看到,多了這 3
個文件,並且只有在我們點擊該按鈕是才會去加載這 3
個文件。
webpackChunkName
這些文件可能不太好區分,我們可以通過設置 webpackChunkName
來定義生成的文件名
// index-entry.js
const handle = () => import(/* webpackChunkName: "handle" */ './handle');
const handle2 = () => import(/* webpackChunkName: "handle2" */ './handle2');
我們再將這些文件的 hash
長度設置為 8
加以區分
// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[chunkhash:6].js', // 輸出文件名
chunkFilename: '[name].[chunkhash:8].js'
}
// ...
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css',
chunkFilename: 'css/[name].[contenthash:8].css'
})
}
npm run build
之后查看
當然我們也可以將 handle
和 handle2
文件的 webpackChunkName
設置成一樣的,這樣這兩個文件將會打包在一起生成一個文件,可以減少請求數量。
熱更新( HMR, Hot Module Replacement )
開發過程中,我們希望在瀏覽器不刷新頁面的情況下能夠去加載我們修改的代碼,來提高我們的開發效率。我們來看下如何配置:
- 打開
webpack-dev-server
的熱更新開關 - 使用
HotModuleReplacementPlugin
插件
HotModuleReplacementPlugin
插件是 Webpack
自帶的,在 webpack.config.js
直接配置
// webpack.config.js
module.exports = {
devServer: {
//...
hot: true
},
plugins: [
//...
new webpack.HotModuleReplacementPlugin() // 熱更新插件
]
}
在入口文件添加
if (module && module.hot) {
module.hot.accept()
}
這樣就完成了熱更新的配置,但是此時 webpack
打包卻報錯了。
搜了一下相關的問題,在開發環境中我們使用了 HotModuleReplacementPlugin
此時需要使用 hash
來輸出文件,使用 chunkhash
會導致 webpack
報錯,而生產環境則沒有問題。但是現在我們只是通過 process.env.NODE_ENV
這個變量來區分環境,這顯然不是一個很好的方式。
我們最好能夠需要區分一下開發環境和生產環境的配置文件。
定義不同環境的配置
我們可以給不同的環境定義不同的配置文件,但是這些文件將會有大量相似的配置,這時我們可以這樣來定義文件:
webpack.base.js
:定義公共的配置webpack.dev.js
:定義開發環境的配置webpack.prod.js
:定義生產環境的配置
我們可以將一些公共的配置抽離到 webpack.base.js
,然后在 webpack.dev.js
和 webpack.prod.js
進行對應環境的配置。我們還需要通過 webpack-merge
來合並兩個配置文件。
安裝 webpack-merge
npm i -D webpack-merge
現在 webpack.dev.js
就是這樣的
// webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.base');
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: '9000', // 默認是8080
compress: true, // 是否啟用 gzip 壓縮
hot: true
},
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[hash:6].js', // 輸出文件名
chunkFilename: '[name].[hash:8].js'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css',
chunkFilename: 'css/[name].[hash:8].css'
}),
new webpack.HotModuleReplacementPlugin() // 熱更新插件
]
});
同時需要在 package.json
中指定我們的配置文件
// package.json
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.pro.js"
},
這時我們就很優雅的區分開不同環境的配置了。
拷貝靜態資源
有時候我們需要在 html
中直接引用一個打包好的第三方插件庫,這個庫不需要通過 webpack
編譯。比如我們 lib
目錄下有個 lib-a.js
,需要在 public/index.html
中直接引用它。
<!-- public/index.html -->
<script src="/lib/lib-a.js"></script>
這時 build
之后會發現 dist
下是沒有 lib
目錄的,這時會找不到這個文件。這時我們需要借助 CopyWebpackPlugin
這個插件來幫助我們把根目錄下的 lib
目錄拷貝到 dist
目錄下面。
首先安裝 CopyWebpackPlugin
npm i -D CopyWebpackPlugin
配置 webpack.config.js
// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
//...
plugins: [
//...
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, 'lib'),
to: path.resolve(__dirname, 'dist/lib')
}
])
]
}
這時后運行 npm run build
就會發現,dist
目錄下已經有了 lib
目錄及文件了。
更多的配置請查看copy-webpack-plugin。
Resolve 配置
Webpack
在啟動后會從配置的入口模塊出發找出所有依賴的模塊,Resolve
配置 Webpack
如何尋找模塊所對應的文件。 Webpack
內置 JavaScript
模塊化語法解析功能,默認會采用模塊化標准里約定好的規則去尋找,但你也可以根據自己的需要修改默認的規則。
alias
resolve.alias
配置項通過別名來把原導入路徑映射成一個新的導入路徑。
比如我們在 index-entry.js
中引入 lib/lib-b.js
,你可能需要這樣引入
import '../lib/lib-b.js';
而當目錄層級比較深時,這個相對路徑就會變得不好辨認了。這時我們可以配置 lib
的一個別名。
// webpack.config.js
module.exports = {
//...
resolve: {
alias: {
'@lib': path.resolve(__dirname, 'lib') // 為lib目錄添加別名
}
}
}
這時無論你處於目錄的哪個層級,你只需要這樣引入
import '@lib/lib-b.js';
extensions
如果在導入文件時沒有帶后綴名,webpack
會自動帶上后綴后去嘗試訪問文件是否存在。 resolve.extensions
用於配置在嘗試過程中用到的后綴列表,默認是
extensions: ['.js', '.json']
就是說當遇到 import '@lib/lib-b';
時,webpack
會先去尋找 @lib/lib-b.js
文件,如果該文件不存在就去尋找 @lib/lib-b.json
文件, 如果還是找不到就報錯。
如果你想優先使用其他后綴文件,比如 .ts
文件,可以這樣配置
// webpack.config.js
module.exports = {
//...
resolve: {
alias: {
'@lib': path.resolve(__dirname, 'lib'), // 為lib目錄添加別名
extensions: ['.ts', '.js', '.json'] // 從左往右
}
}
}
這樣就會先去找 .ts
了。不過一般我們會將高頻的后綴放在前面,並且數組不要太長,減少嘗試次數,不然會影響打包速度。
現在我們引入 js
文件時可以省略后綴名了。
modules
resolve.modules
配置 webpack
去哪些目錄下尋找第三方模塊,默認是只會去 node_modules
目錄下尋找。如果項目中某個文件夾下的模塊經常被導入,不希望寫很長的路徑,比如 import '../../../components/link'
,那么就可以通過配置 resolve.modules
來簡化。
// webpack.config.js
module.exports = {
//...
resolve: {
modules: ['./src/components', 'node_modules'] // 從左到右查找
}
}
這時,你就可以通過 import 'link'
引入了。
mainFields
有一些第三方模塊會針對不同環境提供幾份代碼。例如分別提供采用 es5
和 es6
的 2
份代碼,這 2
份代碼的位置寫在 package.json
文件里。
{
"jsnext:main": "es/index.js",// 采用 ES6 語法的代碼入口文件
"main": "lib/index.js" // 采用 ES5 語法的代碼入口文件
}
webpack
會根據 mainFields
的配置去決定優先采用那份代碼, mainFields
默認配置如下:
mainFields: ['browser', 'main']
假如你想優先采用 ES6
的那份代碼,可以這樣配置:
mainFields: ['jsnext:main', 'browser', 'main']
enforceExtension
resolve.enforceExtension
如果配置為 true
,那么所有導入語句都必須要帶文件后綴。
enforceModuleExtension
enforceModuleExtension
和 enforceExtension
作用類似,但 enforceModuleExtension
只對 node_modules
下的模塊生效。 因為安裝的第三方模塊中大多數導入語句沒帶文件后綴,如果這時你配置了 enforceExtension
為 true
,那么就需要配置 enforceModuleExtension: false
來兼容第三方模塊。
利用 webpack 解決跨域問題
本地開發時,前端項目的端口號是 9000
,但是服務端可能是 9001
,根據瀏覽器的同源策略,是不能直接請求到后端服務的。當然你可以在后端配置 CORS
相關的頭部來實現跨域,其實也可以通過 webpack
的配置來解決跨域問題。
首先,我們起一個后端服務,安裝 koa
、koa-router
npm i -D koa koa-router
新建 server/index.js
// server/index.js
const Koa = require('koa');
const KoaRouter = require('koa-router');
const app = new Koa();
// 創建 router 實例對象
const router = new KoaRouter();
// 注冊路由
router.get('/user', async (ctx, next) => {
ctx.body = {
code: 0,
data: {
name: '阿林十一'
},
msg: 'success'
};
});
app.use(router.routes()); // 添加路由中間件
app.use(router.allowedMethods()); // 對請求進行一些限制處理
app.listen(9001);
使用 node server/index.js
啟動服務后,在 http://localhost:9001/user
可以訪問結果。
之后再修改 handle.js
,在點擊按鈕之后會請求接口
import './handle.css';
export function handleClick () {
console.log('handleClick');
fetch('/api/user')
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.log(err));
}
這是會發現接口報 404
,下面我們配置一下 webpack.config.dev.js
// webpack.config.dev.js
module.exports = {
//...
proxy: {
'/api': {
target: 'http://127.0.0.1:9001/',
pathRewrite: {
'^/api': ''
}
}
}
}
請求到 http://localhost:9000/api/user
現在會被代理到請求 http://localhost:9001/user
。點擊按鈕發起請求:
最后
現在,我們對 webpack
的配置有了更進一步的了解了,快動手試試吧。本文所有代碼可以查看 github。
后續將會繼續推出 webpack
系列的其他內容哦~
喜歡本文的話點個贊吧~
更多精彩內容,歡迎關注微信公眾號~