webpack 自出現時,一直備受青睞。做爲強大的打包工具,它只是出如今項目初始或優化的階段。若是沒有參與項目的構建,接觸的機會幾乎爲零。即便是參與了,但也會由於項目的週期短,從網上東拼西湊草草了事。javascript
縱觀網上的 webpack 教程,要麼是走馬觀花,科普了一些常規配置項;要麼是過於深刻原理,於實際操做無益。最近一段時間,我把 webpack 的官方文檔來來回回地看了幾遍,結果發現,真香。中文版的官方文檔,通俗易懂,很感謝翻譯組的辛勤奉獻。看完以後,雖然達不到爐火純青的地步,但也不會捉襟見肘,疲於應付。前端
對於這種工具類的博文,依然沿襲 用Type馴化JavaScript 的風格,串聯各個概念。至於細節,就是官方文檔的事了。vue
本文基於 webpack v4.31.0 版本。java
Tapable 是一個小型的庫,容許你對一個 javascript 模塊添加和應用插件。它能夠被繼承或混入到其餘模塊中。相似於 NodeJS 的 EventEmitter 類,專一於自定義事件的觸發和處理。除此以外,Tapable 還容許你經過回調函數的參數,訪問事件的「觸發者(emittee)」或「提供者(producer)」。node
tapable 是 webpack 的核心,webpack 中的不少對象(compile, compilation等)都擴展自tapable,包括 webpack 也是 tapable 的實例。擴展自 tapable 的對象內部會有不少鉤子,它們貫穿了 webpack 構建的整個過程。咱們能夠利用這些鉤子,在其被觸發時,作一些咱們想作的事情。react
拋開 webpack 不談,先看看 tapable 的簡單使用。jquery
// Main.js
const {
SyncHook
} = require("tapable");
class Main {
constructor(options) {
this.hooks = {
init: new SyncHook(['init'])
};
this.plugins = options.plugins;
this.init();
}
init() {
this.beforeInit();
if (Array.isArray(this.plugins)) {
this.plugins.forEach(plugin => {
plugin.apply(this);
})
}
this.hooks.init.call('初始化中。。。');
this.afterInit();
}
beforeInit() {
console.log('初始化前。。。');
}
afterInit() {
console.log('初始化後。。。');
}
}
module.exports = Main;
// MyPlugin.js
class MyPlugin {
apply(main) {
main.hooks.init.tap('MyPlugin', param => {
console.log('init 鉤子,作些啥;', param);
});
}
};
module.exports = MyPlugin;
// index.js
const Main = require('./Main');
const MyPlugin = require('./MyPlugin');
let myPlugin = new MyPlugin();
new Main({ plugins: [myPlugin] });
// 初始化前。。。
// init 鉤子,作些啥; 初始化中。。。
// 初始化後。。。
複製代碼
理解起來很簡單,就是在 init
處觸發鉤子,this.hooks.init.call(params)
相似於咱們熟悉的 EventEmitter.emit('init', params)
。main.hooks.init.tap
相似於 EventEmitter.on('init', callback)
,在 init
鉤子上綁定一些咱們想作的事情。在後面將要說的 webpack 自定義插件,就是在 webpack 中的某個鉤子處,插入自定義的事。webpack
依賴圖 在單頁面應用中,只要有一個入口文件,就能夠把散落在項目下的各個文件整合到一塊兒。何謂依賴,當前文件須要什麼,什麼就是當前文件的依賴。依賴引入的形式有以下:web
import
語句require()
語句define
和 require
語句url(...)
)或 HTML 文件(<img src=...>
)中的圖片連接入口(entry) 入口起點(entry point)指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖(dependency graph)的開始。json
輸出(output) output 屬性告訴 webpack 在哪裏輸出它所建立的 bundle,以及如何命名這些文件。
模塊(module) 決定了如何處理項目中的不一樣類型的模塊。好比設置 loader,處理各類模塊。設置 noParse,忽略無需 webpack 解析的模塊。
解析(resolve) 設置模塊如何被解析。引用依賴時,須要知道依賴間的路徑關係,應遵循何種解析規則。好比給路徑設置別名(alias),解析模塊的搜索目錄(modules),解析 loader 包路徑(resolveLoader)等。
外部擴展(externals) 防止將某些 import 的包(package)打包到 bundle 中,而是在運行時(runtime)再去從外部獲取這些擴展依賴。好比說,項目中引用了 jQuery 的CDN資源,在使用 import $ from 'jquery';
時,webpack 會把 jQuery 打包進 bundle,其實這是沒有必要的,此時須要配置 externals: {jquery: 'jQuery'}
,將其剔除 bundle。
插件(plugins) 用於以各類方式自定義 webpack 構建過程。能夠利用 webpack 中的鉤子,作些優化或者搞些小動做。
開發設置(devServer) 顧名思義,就是開發時用到的選項。好比,開發服務根路徑(contentBase),模塊熱替換(hot,需配合 HotModuleReplacementPlugin
使用),代理(proxy)等。
模式(mode) 提供 mode 配置選項,告知 webpack 使用相應環境的內置優化。具體可見 模式(mode)
優化(optimization) 從 webpack 4 開始,會根據你選擇的 mode 來執行不一樣的優化,不過全部的優化仍是能夠手動配置和重寫。好比,CommonsChunkPlugin
被 optimization.splitChunks
取代。
webpack 差很少就是這幾個配置項,搞清楚這幾個概念,上手仍是比較容易的。
如今的前端項目愈來愈複雜,若是最終導出爲一個 bundle,會極大地影響加載速度。切割 bundle,控制資源加載優先級,按需加載或並行加載,合理應用就會大大縮短加載時間。官方文檔提供了三種常見的代碼分離方法:
入口起點 配置多個入口文件,而後將最終生成的過個 bundle 出入到 HTML 中。
// webpack.config.js
entry: {
index: './src/index.js',
vendor: './src/vendor.js'
}
output: {
filename: '[name].bundle.js',
},
plugins: [
new HtmlWebpackPlugin({
chunks: ['vendor', 'index']
})
]
複製代碼
不過若是這兩個文件中存在相同的模塊,這就意味着相同的模塊被加載了兩次。此時,咱們就須要提取出重複的模塊。
防止重複 在 webpack 老的版本中,CommonsChunkPlugin
經常使用來提取公共的模塊。新版本中 SplitChunksPlugin
取而代之,能夠經過 optimization.splitChunks
設置,多見於多頁面應用。
動態導入 就是在須要時再去加載模塊,而不是一股腦的所有加載。webpack 還提供了預取和預加載的方式。非入口 chunk,咱們能夠經過 chunkFilename 爲其命名。常見的如,vue 路由動態導入。
// webpack.config.js
output: {
chunkFilename: '[name].bundle.js',
}
// index.js
import(/* webpackChunkName: "someJs" */ 'someJs');
import(/* webpackPrefetch: true */ 'someJs');
import(/* webpackPreload: true */ 'someJs');
複製代碼
基於瀏覽器的緩存策略,咱們知道若是本地緩存命中,則無需再次請求資源。對於改動不頻繁或基本不會再作改動的模塊,能夠剝離出來。
// webpack.config.js
output: {
filename: '[name].[contenthash].js',
}
複製代碼
按照咱們的想法,只要模塊的內容沒有變化,對應的名字也就不會發生變化,這樣緩存就會起做用了。事實上並不是如此,webpack 打包後的文件,並不是只有用戶本身的代碼,還包括管理用戶代碼的代碼,如 runtime 和 manifest。
模塊依賴間的整合並非簡單的代碼拼接,其中包括模塊的加載和解析邏輯。注入的 runtime 和 manifest 在每次構建後都會發生變化。這就致使了即便用戶代碼沒有變化,某些 hash 仍是發生了改變。經過 optimization.runtimeChunk
提取 runtime 代碼。經過 optimization.splitChunks
剝離第三方庫。好比, react,react-dom。
module.exports = {
//...
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'vendor',
chunks: 'all',
}
}
}
}
};
複製代碼
最後使用 HashedModuleIdsPlugin
來消除因模塊 ID 變更帶來的影響。
loader 用於對模塊的源代碼進行轉換。loader 是導出爲一個函數的 node 模塊。該函數在 loader 轉換資源的時候調用。給定的函數將調用 loader API,並經過 this 上下文訪問。
// loader API;
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
// sync loader
module.exports = function(content, map, meta){
this.callback(null, syncOperation(content, map, meta));
return;
}
// async loader
module.exports = function(content, map, meta){
let callback = this.async();
asyncOperation(content, (error, result) => {
if(error) callback(error);
callback(null, result, map, meta);
return;
})
}
複製代碼
多個 loader 串行時,在從右向左執行 loader 以前,會向從左到右調用 loader 上的 pitch 方法。若是在 pitch 中返回告終果,則會跳事後續 loader。
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
<!-- pitch 中返回結果 -->
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
複製代碼
webpack 的自定義插件和本文開頭 Tapable 中的差很少。webpack 插件是一個具備 apply 方法的 JavaScript 對象。apply 方法會被 webpack compiler 調用,而且 compiler 對象可在整個編譯生命週期訪問。鉤子有同步的,也有異步的,這須要根據 webpack 提供的 API 文檔。
// 官方例子
class FileListPlugin {
apply(compiler) {
// emit 是異步 hook,使用 tapAsync 觸及它,還可使用 tapPromise/tap(同步)
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// 在生成文件中,建立一個頭部字符串:
var filelist = 'In this build:\n\n';
// 遍歷全部編譯過的資源文件,
// 對於每一個文件名稱,都添加一行內容。
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// 將這個列表做爲一個新的文件資源,插入到 webpack 構建中:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
}
}
module.exports = FileListPlugin;
複製代碼
ProvidePlugin 自動加載模塊,無需到處引用。有點相似 expose-loader
。
// webpack.config.js
new webpack.ProvidePlugin({
$: 'jquery',
})
// some.js
$('#item');
複製代碼
DllPlugin 將基礎模塊打包進動態連接庫,當依賴的模塊存在於動態連接庫中時,無需再次打包,而是直接從動態連接庫中獲取。DLLPlugin 負責打包出動態連接庫,DllReferencePlugin 負責從主要配置文件中引入 DllPlugin 插件打包好的動態連接庫文件。
// webpack-dll-config.js
// 先執行該配置文件
output: {
path: path.join(__dirname, "dist"),
filename: "MyDll.[name].js",
library: "[name]_[hash]"
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, "dist", "[name]-manifest.json"),
name: "[name]_[hash]"
})
]
// webpack-config.js
// 後執行該配置文件
plugins: [
new webpack.DllReferencePlugin({
manifest: require("../dll/dist/alpha-manifest.json")
}),
]
複製代碼
HappyPack 啓動子進程處理任務,充分利用資源。不過進程間的通信比較耗資源,要酌情處理。
const HappyPack = require('happypack');
// loader
{
test: /\.js$/,
use: ['happypack/loader?id=babel'],
exclude: path.resolve(__dirname, 'node_modules'),
},
// plugins
new HappyPack({
id: 'babel',
loaders: ['babel-loader?cacheDirectory'],
}),
複製代碼
webpack-bundle-analyzer
webpack 打包後的分析工具。
webpack 告一段落,淺嘗輒止。