1.webpack 的核心概念#
- entry(入口):一個可執行模塊或者庫的入口。定義了打包後的入口文件。
- output(出口):指示 webpack 如何去輸出,以及在哪裡輸出。
path: 打包文件存放的絕對路徑
publicPath: 網站運行時的訪問路徑
filename: 打包後的文件名 - module(模塊):在 webpack 裡,一切皆模塊,一個模塊對應一個文件。webpack 會從配置的 entry 中開始遞歸找出所有依賴的模塊。
- chunk(代碼塊):一個
chunk
由多個模塊
組合而成。可以將可執行的模塊和它所依賴的模塊組合成一個 chunk ,這就是打包。 - loader(模塊轉換器):用於把一個模塊原內容按照需求轉換成新的內容。例如:es6 轉換為 es5,scss 轉換為 css 等。
- plugin(擴展):擴展 webpack 功能的插件。在 webpack 構建的生命週期節點上加入擴展 hook,添加功能。
2.webpack 構建流程#
- 初始化參數:解析 webpack 的配置參數,合併 shell 傳入和 webpack.config.js 文件配置的參數,形成最後的配置結果。
- 開始編譯:上一步得到的參數初始化 compiler 對象,註冊所有配置的插件,插件監聽 webpack 構建生命週期的事件節點,做出相應的反應,執行對象的 run 方法開始執行編譯。
- 確定入口:其配置的 entry 入口,開始解析文件構建的 AST 語法樹,找出依賴,遞歸下去。
- 編譯模塊:根據文件類型和 loader 配置,調用所有配置的 loader 對文件進行轉換,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理。
- 完成模塊編譯並輸出:遞歸完後,得到每個文件結果,包含了每個模塊及它們之間的依賴關係,根據 entry 配置生成代碼塊 chunk 。
- 輸出完成:輸出所有的 chunk 到文件系統。
3. 有哪些常見的 loader ?#
- babel-loader:把 es6 轉成 es5;
- css-loader:加載 css,支持模塊化,壓縮,文件導入等特性;
- style-loader:把 css 代碼注入到 js 中,通過 dom 操作去加載 css;
- eslint-loader:通過 Eslint 檢查 js 代碼;
- image-loader:加載並且壓縮圖片;
- file-loader:文件輸出到一個文件夾中,在代碼中通過相對 url 去引用輸出的文件;
- url-loader:和 file-loader 類似,文件很小的時候可以 base64 方式把文件內容注入到代碼中。
- source-map-loader:加載額外的 source map 文件,方便調試。
4. 業務場景和對應解決方案#
1. 單頁應用#
一個單頁應用需要配置一個 entry 指明執行入口,web-webpack-plugin 裡的 WebPlugin 可以自動的完成這些工作:webpack 會為 entry 生成一個包含這個入口的所有依賴文件的 chunk,但是還需要一個 html 來加載 chunk 生成的 js,如果還提取出 css 需要 HTML 文件中引入提取的 css。
一個簡單的 webpack 配置文件例子:
const { WebPlugin } = require('web-webpack-plugin');
module.exports = {
entry: {
app: './src/doc/index.js',
home: './src/doc/home.js'
},
plugins: [
// 一個WebPlugin對應生成一個html文件
new WebPlugin({
//輸出的html文件名稱
filename: 'index.html',
//這個html依賴的`entry`
requires: ['app','home'],
}),
],
};
說明:require: ['app', 'home'] 指明這個 html 依賴哪些 entry,entry 生成的 js 和 css 會自動注入到 html 中。
還支持配置這些資源注入方式,支持如下屬性:
- _dist 只有在生產環境中才引入的資源;
- _dev 只有在開發環境中才引入的資源;
- _inline 把資源的內容潛入到 html 中;
- _ie 只有 IE 瀏覽器才需要引入的資源。
這些屬性可以通過在 js 裡配置,看個簡單例子:
new WebPlugin({
filename: 'index.html',
requires: {
app:{
_dist:true,
_inline:false,
}
},
}),
這些屬性還可以在模板中設置,使用模板好處就是可以靈活的控制資源的注入點
new WebPlugin({
filename: 'index.html',
template: './template.html',
}),
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<link rel="stylesheet" href="app?_inline">
<script src="ie-polyfill?_ie"></script>
</head>
<body>
<div id="react-body"></div>
<script src="app"></script>
</body>
</html>
WebPlugin 插件借鑒了 fis3 的思想,補足了 webpack 缺失的以 HTML 為入口的功能。想了解 WebPlugin 的更多功能,見文檔。
2. 一個項目管理多個單頁面#
一個項目中會包含多個單頁應用,雖然多個單頁應用可以合成一個,但是這樣做會導致用戶沒有訪問的部分也加載了,如果項目中有很多的單頁應用。為每一個單頁應用配置一個 entry 和 WebPlugin?如果又新增,又要新增 webpack 配置,這樣做麻煩,這時候有一個插件 web-webpack-plugin 裡的 AutoWebPlugin 方法可以解決這些問題。
module.exports = {
plugins: [
// 所有頁面的入口目錄
new AutoWebPlugin('./src/'),
]
};
分析:
AutoWebPlugin
會把./src/ 目錄下所有每個文件夾作為一個單頁頁面的入口,自動為所有的頁面入口配置一個WebPlugin
輸出對應的 html。- 要新增一個頁面就在
./src/
下新建一個文件夾包含這個單頁應用所依賴的代碼,AutoWebPlugin
自動生成一個名叫文件夾名稱的 html 文件。
3. 代碼分隔優化#
一個好的代碼分割對瀏覽器首屏效果提升很大。
最常見的 react 體系:
- 先抽出基礎庫 react react-dom redux react-redux 到一個單獨的文件而不是和其它文件放在一起打包為一個文件,這樣做的好處是只要你不升級他們的版本這個文件永遠都會被刷新。如果你把這些基礎庫和業務代碼打包在一個文件裡每次改動業務代碼都會導致文件 hash 值變化從而導致緩存失效瀏覽器重複下載這些包含基礎庫的代碼。所以把基礎庫打包成一個文件。
// vender.js 文件抽離基礎庫到單獨的文件裡防止跟隨業務代碼被刷新
// 所有頁面都依賴的第三方庫
// react基礎
import 'react';
import 'react-dom';
import 'react-redux';
// redux基礎
import 'redux';
import 'redux-thunk';
// webpack配置
{
entry: {
vendor: './path/to/vendor.js',
},
}
- 通過 CommonsChunkPlugin 可以提取出多個代碼塊都依賴的代碼形成一個單獨的 chunk。在應用有多個頁面的場景下提取出所有頁面公共的代碼減少單個頁面的代碼,在不同頁面之間切換時所有頁面公共的代碼之前被加載過而不必重新加載。所以通過 CommonsChunkPlugin 可以提取出多個代碼塊都依賴的代碼形成一個單獨的 chunk。
4. 構建服務端渲染#
服務端渲染的代碼要運行在 nodejs 環境,和瀏覽器不同的是,服務端渲染代碼需要採用 commonjs 規範同時不應該包含除 js 之外的文件比如 css。
webpack 配置如下:
module.exports = {
target: 'node',
entry: {
'server_render': './src/server_render',
},
output: {
filename: './dist/server/[name].js',
libraryTarget: 'commonjs2',
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.(scss|css|pdf)$/,
loader: 'ignore-loader',
},
]
},
};
分析一下:
-
target: 'node'
指明構建出代碼要運行在 node 環境中。 -
libraryTarget: 'commonjs2'
指明輸出的代碼要是 commonjs 規範。 -
{test: /\.(scss|css|pdf)$/,loader: 'ignore-loader'}
是為了防止不能在 node 裡執行服務端渲染也用不上的文件被打包進去。
5.fis3 遷移到 webpack#
fis3 和 webpack 有很多相似地方也有不同的地方,相似地方:都採用 commonjs 規範,不同地方:導入 css 這些非 js 資源的方式。
fis3 通過 @require './index.scss',而 webpack 是通過 require ('./index.scss')。
如果想把 fis3 平滑遷移到 webpack,可以使用 comment-require-loader。
比如:你想在 webpack 構建是使用採用了 fis3 方式的 imui 模塊
loaders:[{
test: /\.js$/,
loaders: ['comment-require-loader'],
include: [path.resolve(__dirname, 'node_modules/imui'),]
}]
5. 自定義 webpack 擴展#
如果你在社區找不到你的應用場景的解決方案,那就需要自己動手了寫 loader 或者 plugin 了。
在你編寫自定義 webpack 擴展前你需要想明白到底是要做一個 loader 還是 plugin 呢?可以這樣判斷:
如果你的擴展是想對一個個單獨的文件進行轉換那麼就編寫 loader 剩下的都是 plugin。
其中對文件進行轉換可以是像:
- babel-loader 把 es6 轉為 es5;
- file-loader 把文件替換成對應的 url;
- raw-loader 注入文本文件內容到代碼中。
1. 編寫 webpack loader#
編寫 loader 非常簡單,以 comment-require-loader 為例:
module.exports = function (content) {
return replace(content);
};
loader 的入口需要導出一個函數,這個函數要幹的事情就是轉換一個文件的內容。
函數接收的參數 content 是一個文件在轉換前的字符串形式內容,需要返回一個新的字符串形式內容作為轉換後的結果,所有通過模塊化導入的文件都會經過 loader。從這裡可以看出 loader 只能處理一個個單獨的文件而不能處理代碼塊。可以參考官方文檔。
2. 編寫 webpack plugin#
plugin 應用場景廣泛,所以稍微複雜點。以 end-webpack-plugin 為例:
class EndWebpackPlugin {
constructor(doneCallback, failCallback) {
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
// 監聽webpack生命週期裡的事件,做相應的處理
compiler.plugin('done', (stats) => {
this.doneCallback(stats);
});
compiler.plugin('failed', (err) => {
this.failCallback(err);
});
}
}
module.exports = EndWebpackPlugin;
loader 的入口需要導出一個 class,在 new EndWebpackPlugin () 的時候通過構造函數傳入這個插件需要的參數,在 webpack 啟動的時候會先實例化 plugin,再調用 plugin 的 apply 方法,插件在 apply 函數裡監聽 webpack 生命週期裡的事件,做相應的處理。
webpack plugin 的兩個核心概念:
- compiler:從 webpack 啟動到退出只存在一個 Compiler,compiler 存放著 webpack 的配置。
- compilation:由於 webpack 的監聽文件變化自動編譯機制,compilation 代表一次編譯。
Compiler 和 Compilation 都會廣播一系列事件。webpack 生命週期裡有非常多的事件。
以上只是最簡單的 demo,更複雜的可以查看 how to write a plugin 或參考 web-webpack-plugin。