webpack是一個js打包工具,不一個完整的前端構建工具。它的流行得益於模塊化和單頁應用的流行。webpack提供擴展機制,在龐大的社區支持下各類場景基本它均可找到解決方案。本文的目的是教會你用webpack解決實戰中常見的問題。css
webpack原理
在深刻實戰前先要知道webpack的運行原理html
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
壓縮覆蓋以前的結果。node
場景和方案
經過各類場景和對應的解決方案讓你深刻掌握webpackreact
單頁應用
demo redemo
一個單頁應用須要配置一個entry
指明執行入口,webpack會爲entry
生成一個包含這個入口全部依賴文件的chunk
,但要讓它在瀏覽器裏跑起來還須要一個HTML文件來加載chunk
生成的js文件,若是提取出了css還須要讓HTML文件引入提取出的css。web-webpack-plugin裏的WebPlugin
能夠自動的完成這些工做。webpack
webpack配置文件git
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裏。
你還能夠配置這些資源的注入方式,支持以下屬性:es6
_dist
只有在生產環境下才引入該資源_dev
只有在開發環境下才引入該資源_inline
把該資源的內容潛入到html裏_ie
只有IE瀏覽器才須要引入的資源
要設置這些屬性能夠經過在js裏配置github
new WebPlugin({ filename: 'index.html', requires: { app:{ _dist:true, _inline:false, } }, }),
或者在模版裏設置,使用模版的好處是靈活的控制資源注入點。web
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
的更多功能,見文檔。
一個項目裏管理多個單頁應用
通常項目裏會包含多個單頁應用,雖然多個單頁應用也能夠合併成一個可是這樣作會致使用戶沒訪問的部分也加載了。若是項目裏有不少個單頁應用,爲每一個單頁應用配置一個entry
和WebPlugin
?若是項目又新增了一個單頁應用,又去新增webpack配置?這樣作太麻煩了,web-webpack-plugin裏的AutoWebPlugin
能夠方便的解決這些問題。
module.exports = { plugins: [ // 全部頁面的入口目錄 new AutoWebPlugin('./src/'), ] };
AutoWebPlugin
會把./src/
目錄下全部每一個文件夾做爲一個單頁頁面的入口,自動爲全部的頁面入口配置一個WebPlugin輸出對應的html。要新增一個頁面就在./src/
下新建一個文件夾包含這個單頁應用所依賴的代碼,AutoWebPlugin
自動生成一個名叫文件夾名稱的html文件。AutoWebPlugin
的更多功能見文檔。
代碼分割優化
一個好的代碼分割對瀏覽器首屏效果提高很大。好比對於最多見的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
。在應用有多個頁面的場景下提取出全部頁面公共的代碼減小單個頁面的代碼,在不一樣頁面之間切換時全部頁面公共的代碼以前被加載過而沒必要從新加載。
構建npm包
demo remd
除了構建可運行的web應用,webpack也可用來構建發佈到npm上去的給別人調用的js庫。
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配置以下:
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
模塊,配置以下:
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爲例:
module.exports = function (content) { return replace(content); };
loader
的入口須要導出一個函數,這個函數要乾的事情就是轉換一個文件的內容。
函數接收的參數content
是一個文件在轉換前的字符串形式內容,須要返回一個新的字符串形式內容做爲轉換後的結果,全部經過模塊化倒入的文件都會通過loader
。從這裏能夠看出loader
只能處理一個個單獨的文件而不能處理代碼塊。想編寫更復雜的loader可參考官方文檔
編寫 webpack plugin
demo end-webpack-pluginplugin
應用場景普遍,因此稍微複雜點。以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 裏有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讓你感到複雜,必定是你本身沒有下定決心去學好它。但願本文能讓你明白webpack的原理與本質讓你能夠在實戰中靈活應用webpack。