本文將繼續引入更多的 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 系列的其他內容哦~
喜歡本文的話點個贊吧~

更多精彩內容,歡迎關注微信公眾號~
