本文將繼續引入更多的 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
系列的其他內容哦~
喜歡本文的話點個贊吧~
更多精彩內容,歡迎關注微信公眾號~