webpack是一個js打包工具,不是一個完整的前端構建工具。它的流行得益於模組化和單頁應用的流行。webpack提供擴充套件機制,在龐大的社群支援下各種場景基本它都可找到解決方案。本文的目的是教會你用webpack解決實戰中常見的問題。
webpack原理
在深入實戰前先要知道webpack的執行原理
webpack核心概念
entry
一個可執行模組或庫的入口檔案。chunk
多個檔案組成的一個程式碼塊,例如把一個可執行模組和它所有依賴的模組組合和一個chunk
這體現了webpack的打包機制。loader
檔案轉換器,例如把es6轉換為es5,scss轉換為css。plugin
外掛,用於擴充套件webpack的功能,在webpack構建生命週期的節點上加入擴充套件hook為webpack加入功能。
webpack構建流程
從啟動webpack構建到輸出結果經歷了一系列過程,它們是:
- 解析webpack配置引數,合併從shell傳入和
webpack.config.js
檔案裡配置的引數,生產最後的配置結果。 - 註冊所有配置的外掛,好讓外掛監聽webpack構建生命週期的事件節點,以做出對應的反應。
- 從配置的
entry
入口檔案開始解析檔案構建AST語法樹,找出每個檔案所依賴的檔案,遞迴下去。 - 在解析檔案遞迴的過程中根據檔案型別和loader配置找出合適的loader用來對檔案進行轉換。
- 遞迴完後得到每個檔案的最終結果,根據
entry
配置生成程式碼塊chunk
。 - 輸出所有
chunk
到檔案系統。
需要注意的是,在構建生命週期中有一系列外掛在合適的時機做了合適的事情,比如UglifyJsPlugin
會在loader轉換遞迴完後對結果再使用UglifyJs
壓縮覆蓋之前的結果。
場景和方案
通過各種場景和對應的解決方案讓你深入掌握webpack
單頁應用
demo redemo
一個單頁應用需要配置一個entry
指明執行入口,webpack會為entry
生成一個包含這個入口所有依賴檔案的chunk
,但要讓它在瀏覽器裡跑起來還需要一個HTML檔案來載入chunk
生成的js檔案,如果提取出了css還需要讓HTML檔案引入提取出的css。web-webpack-plugin裡的WebPlugin
可以自動的完成這些工作。
webpack配置檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const { WebPlugin } = require('web-webpack-plugin'); module.exports = { entry: { app: './src/doc/index.js', }, plugins: [ // 一個WebPlugin對應生成一個html檔案 new WebPlugin({ //輸出的html檔名稱 filename: 'index.html', //這個html依賴的`entry` requires: ['app'], }), ], }; |
requires: ['doc']
指明這個HTML依賴哪些entry
,entry
生成的js和css會自動注入到HTML裡。
你還可以配置這些資源的注入方式,支援如下屬性:
_dist
只有在生產環境下才引入該資源_dev
只有在開發環境下才引入該資源_inline
把該資源的內容潛入到html裡_ie
只有IE瀏覽器才需要引入的資源
要設定這些屬性可以通過在js裡配置
1 2 3 4 5 6 7 8 9 |
new WebPlugin({ filename: 'index.html', requires: { app:{ _dist:true, _inline:false, } }, }), |
或者在模版裡設定,使用模版的好處是靈活的控制資源注入點。
1 2 3 4 |
new WebPlugin({ filename: 'index.html', template: './template.html', }), |
1 2 3 4 5 6 7 8 9 10 11 |
<!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
的更多功能,見文件。
一個專案裡管理多個單頁應用
一般專案裡會包含多個單頁應用,雖然多個單頁應用也可以合併成一個但是這樣做會導致使用者沒訪問的部分也載入了。如果專案裡有很多個單頁應用,為每個單頁應用配置一個entry
和WebPlugin
?如果專案又新增了一個單頁應用,又去新增webpack配置?這樣做太麻煩了,web-webpack-plugin裡的AutoWebPlugin
可以方便的解決這些問題。
1 2 3 4 5 6 |
module.exports = { plugins: [ // 所有頁面的入口目錄 new AutoWebPlugin('./src/'), ] }; |
AutoWebPlugin
會把./src/
目錄下所有每個資料夾作為一個單頁頁面的入口,自動為所有的頁面入口配置一個WebPlugin輸出對應的html。要新增一個頁面就在./src/
下新建一個資料夾包含這個單頁應用所依賴的程式碼,AutoWebPlugin
自動生成一個名叫資料夾名稱的html檔案。AutoWebPlugin
的更多功能見文件。
程式碼分割優化
一個好的程式碼分割對瀏覽器首屏效果提升很大。比如對於最常見的react體系你可以
- 先抽出基礎庫
react
react-dom
redux
react-redux
到一個單獨的檔案而不是和其它檔案放在一起打包為一個檔案,這樣做的好處是隻要你不升級他們的版本這個檔案永遠不會被重新整理。如果你把這些基礎庫和業務程式碼打包在一個檔案裡每次改動業務程式碼都會導致檔案hash值變化從而導致快取失效瀏覽器重複下載這些包含基礎庫的程式碼。以上的配置為:
1 2 3 4 5 6 7 8 9 |
// vender.js 檔案抽離基礎庫到單獨的一個檔案裡防止跟隨業務程式碼被重新整理 // 所有頁面都依賴的第三方庫 // react基礎 import 'react'; import 'react-dom'; import 'react-redux'; // redux基礎 import 'redux'; import 'redux-thunk'; |
1 2 3 4 5 6 |
// webpack配置 { entry: { vendor: './path/to/vendor.js', }, } |
- 再通過CommonsChunkPlugin可以提取出多個程式碼塊都依賴的程式碼形成一個單獨的
chunk
。在應用有多個頁面的場景下提取出所有頁面公共的程式碼減少單個頁面的程式碼,在不同頁面之間切換時所有頁面公共的程式碼之前被載入過而不必重新載入。
構建npm包
demo remd
除了構建可執行的web應用,webpack也可用來構建釋出到npm上去的給別人呼叫的js庫。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const nodeExternals = require('webpack-node-externals'); module.exports = { entry: { index: './src/index.js', }, externals: [nodeExternals()], target: 'node', output: { path: path.resolve(__dirname, '.npm'), filename: '[name].js', libraryTarget: 'commonjs2', }, }; |
這裡有幾個區別於web應用不同的地方:
externals: [nodeExternals()]
用於排除node_modules
目錄下的程式碼被打包進去,因為放在node_modules
目錄下的程式碼應該通過npm安裝。libraryTarget: 'commonjs2'
指出entry
是一個可供別人呼叫的庫而不是可執行的,輸出的js檔案按照commonjs規範。
構建服務端渲染
服務端渲染的程式碼要執行在nodejs環境,和瀏覽器不同的是,服務端渲染程式碼需要採用commonjs規範同時不應該包含除js之外的檔案比如css。webpack配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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裡執行服務端渲染也用不上的檔案被打包進去。
從fis3遷移到webpack
fis3和webpack有相似的地方也有不同的地方。相似在於他們都採用commonjs規範,不同在於匯入css這些非js資源的方式。fis3通過// @require './index.scss'
而webpack通過require('./index.scss')
。如果想從fis3平滑遷移到webpack可以使用comment-require-loader。比如你想在webpack構建是使用採用了fis3方式的imui
模組,配置如下:
1 2 3 4 5 |
loaders:[{ test: /\.js$/, loaders: ['comment-require-loader'], include: [path.resolve(__dirname, 'node_modules/imui'),] }] |
自定義webpack擴充套件
如果你在社群找不到你的應用場景的解決方案,那就需要自己動手了寫loader或者plugin了。
在你編寫自定義webpack擴充套件前你需要想明白到底是要做一個loader
還是plugin
呢?可以這樣判斷:
如果你的擴充套件是想對一個個單獨的檔案進行轉換那麼就編寫
loader
剩下的都是plugin
。
其中對檔案進行轉換可以是像:
babel-loader
把es6轉換成es5
file-loader
把檔案替換成對應的URLraw-loader
注入文字檔案內容到程式碼裡去
編寫 webpack loader
demo comment-require-loader
編寫loader
非常簡單,以comment-require-loader為例:
1 2 3 |
module.exports = function (content) { return replace(content); }; |
loader
的入口需要匯出一個函式,這個函式要乾的事情就是轉換一個檔案的內容。
函式接收的引數content
是一個檔案在轉換前的字串形式內容,需要返回一個新的字串形式內容作為轉換後的結果,所有通過模組化倒入的檔案都會經過loader
。從這裡可以看出loader
只能處理一個個單獨的檔案而不能處理程式碼塊。想編寫更復雜的loader可參考官方文件
編寫 webpack plugin
demo end-webpack-plugin
plugin
應用場景廣泛,所以稍微複雜點。以end-webpack-plugin為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 裡有2個核心概念:
Compiler
: 從webpack啟動到推出只存在一個Compiler
,Compiler
存放著webpack配置Compilation
: 由於webpack的監聽檔案變化自動編譯機制,Compilation
代表一次編譯。
Compiler
和 Compilation
都會廣播一系列事件。
webpack生命週期裡有非常多的事件可以在event-hooks和Compilation裡查到。以上只是一個最簡單的demo,更復雜的可以檢視 how to write a plugin或參考web-webpack-plugin。
總結
webpack其實很簡單,可以用一句話涵蓋它的本質:
webpack是一個打包模組化js的工具,可以通過loader轉換檔案,通過plugin擴充套件功能。
如果webpack讓你感到複雜,一定是各種loader和plugin的原因。
希望本文能讓你明白webpack的原理與本質讓你可以在實戰中靈活應用webpack。