1、前言javascript
如今隨着前端開發的複雜度和規模愈來愈大,鷹不能拋開工程化來獨立開發,好比:react的jsx代碼必須編譯後才能在瀏覽器中使用,好比sass和less代碼瀏覽器是不支持的。若是摒棄這些開發框架,開發效率會大幅降低。css
在衆多前端工程化工具中,webpack脫穎而出成爲了當今最流行的前端構建工具。html
2、webpack的原理前端
知其然知其因此然。vue
一、核心概念java
(1)entry:一個可執行模塊或者庫的入口。node
(2)chunk:多個文件組成一個代碼塊。能夠將可執行的模塊和他所依賴的模塊組合成一個chunk,這是打包。react
(3)loader:文件轉換器。例如把es6轉爲es5,scss轉爲css等webpack
(4)plugin:擴展webpack功能的插件。在webpack構建的生命週期節點上加入擴展hook,添加功能。git
二、webpack構建流程(原理)
從啓動構建到輸出結果一系列過程:
(1)初始化參數:解析webpack配置參數,合併shell傳入和webpack.config.js文件配置的參數,造成最後的配置結果。
(2)開始編譯:上一步獲得的參數初始化compiler對象,註冊全部配置的插件,插件監聽webpack構建生命週期的事件節點,作出相應的反應,執行對象的 run 方法開始執行編譯。
(3)肯定入口:從配置的entry入口,開始解析文件構建AST語法樹,找出依賴,遞歸下去。
(4)編譯模塊:遞歸中根據文件類型和loader配置,調用全部配置的loader對文件進行轉換,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理。
(5)完成模塊編譯並輸出:遞歸完過後,獲得每一個文件結果,包含每一個模塊以及他們之間的依賴關係,根據entry配置生成代碼塊chunk。
(6)輸出完成:輸出全部的chunk到文件系統。
注意:在構建生命週期中有一系列插件在作合適的時機作合適事情,好比UglifyPlugin會在loader轉換遞歸完對結果使用UglifyJs壓縮覆蓋以前的結果。
3、業務場景和對應解決方案
一、單頁應用
一個單頁應用須要配置一個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中。
還支持配置這些資源注入方式,支持以下屬性:
(1)_dist只有在生產環境中才引入的資源;
(2)_dev只有在開發環境中才引入的資源;
(3)_inline把資源的內容潛入到html中;
(4)_ie只有IE瀏覽器才須要引入的資源。
這些屬性能夠經過在js裏配置,看個簡單例子:
new WebPlugin({ filename: 'index.html', requires: { app:{ _dist:true, _inline:false, } }, }),
這些屬性還能夠在模板中設置,使用模板好處就是能夠靈活的控制資源的注入點。
new WebPlugin({ filename: 'index.html', template: './template.html', }), //template模板 <!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文件。
三、代碼分隔優化
一個好的代碼分割對瀏覽器首屏效果提高很大。
最多見的react體系:
(1)先抽出基礎庫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', }, }
(2)經過CommonsChunkPlugin能夠提取出多個代碼塊都依賴的代碼造成一個單獨的chunk
。在應用有多個頁面的場景下提取出全部頁面公共的代碼減小單個頁面的代碼,在不一樣頁面之間切換時全部頁面公共的代碼以前被加載過而沒必要從新加載。因此經過CommonsChunkPlugin能夠提取出多個代碼塊都依賴的代碼造成一個單獨的chunk。
四、構建服務端渲染
服務端渲染的代碼要運行在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', }, ] }, };
分析一下:
(1)target: 'node'指明構建出代碼要運行在node環境中。
(2)libraryTarget: 'commonjs2' 指明輸出的代碼要是commonjs規範。
(3){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'),] }]
4、自定義webpack擴展
若是你在社區找不到你的應用場景的解決方案,那就須要本身動手了寫loader或者plugin了。
在你編寫自定義webpack擴展前你須要想明白究竟是要作一個loader
仍是plugin
呢?能夠這樣判斷:
若是你的擴展是想對一個個單獨的文件進行轉換那麼就編寫loader剩下的都是plugin。
其中對文件進行轉換能夠是像:
一、babel-loader把es6轉爲es5;
二、file-loader把文件替換成對應的url;
三、raw-loader注入文本文件內容到代碼中。
一、編寫webpack loader
編寫loader
很是簡單,以comment-require-loader爲例:
module.exports = function (content) { return replace(content); };
loader
的入口須要導出一個函數,這個函數要乾的事情就是轉換一個文件的內容。
函數接收的參數content
是一個文件在轉換前的字符串形式內容,須要返回一個新的字符串形式內容做爲轉換後的結果,全部經過模塊化倒入的文件都會通過loader
。從這裏能夠看出loader
只能處理一個個單獨的文件而不能處理代碼塊。能夠參考官方文檔
二、編寫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的兩個核心概念:
(1)compiler:從webpack啓動到退出只存在一個Compiler,compiler存放着webpack的配置。
(2)compilation:因爲webpack的監聽文件變化自動編譯機制,compilation表明一次編譯。
Compiler
和 Compilation
都會廣播一系列事件。webpack生命週期裏有很是多的事件
以上只是一個最簡單的demo,更復雜的能夠查看 how to write a plugin或參考web-webpack-plugin。
5、總結
webpack其實比較簡單,用一句話歸納本質:
webpack是一個打包模塊化js的工具,能夠經過loader轉換文件,經過plugin擴展功能。
若是webpack讓你感到複雜,必定是各類loader和plugin的緣由。
6、一些問題
一、webpack與grunt、gulp的不一樣?
三者都是前端構建工具,grunt和gulp在早期比較流行,如今webpack相對來講比較主流,不過一些輕量化的任務仍是會用gulp來處理,好比單獨打包CSS文件等。
grunt和gulp是基於任務和流(Task、Stream)的。相似jQuery,找到一個(或一類)文件,對其作一系列鏈式操做,更新流上的數據, 整條鏈式操做構成了一個任務,多個任務就構成了整個web的構建流程。
webpack是基於入口的。webpack會自動地遞歸解析入口所須要加載的全部資源文件,而後用不一樣的Loader來處理不一樣的文件,用Plugin來擴展webpack功能。
總結:(1)從構建思路來講:gulp和grunt須要開發者將整個前端構建過程拆分紅多個`Task`,併合理控制全部`Task`的調用關係 webpack須要開發者找到入口,並須要清楚對於不一樣的資源應該使用什麼Loader作何種解析和加工;
(2)對於知識背景:gulp更像後端開發者的思路,須要對於整個流程瞭如指掌 webpack更傾向於前端開發者的思路。
二、 與webpack相似的工具還有哪些?談談你爲何最終選擇(或放棄)使用webpack?
一樣是基於入口的打包工具還有如下幾個主流的:webpack,rollup,parcel。
從應用場景上來看:(1)webpack適合大型複雜的前端站點構建;(2)rollup適合基礎庫的打包,好比vue,react;(3)parcel適用於簡單的實驗室項目,可是打包出錯很難調試。
三、有哪些常見的Loader?他們是解決什麼問題的?
(1)babel-loader:把es6轉成es5;
(2)css-loader:加載css,支持模塊化,壓縮,文件導入等特性;
(3)style-loader:把css代碼注入到js中,經過dom操做去加載css;
(4)eslint-loader:經過Eslint檢查js代碼;
(5)image-loader:加載而且壓縮圖片晚間;
(6)file-loader:文件輸出到一個文件夾中,在代碼中經過相對url去引用輸出的文件;
(7)url-loader:和file-loader相似,文件很小的時候能夠base64方式吧文件內容注入到代碼中。
(8)source-map-loader:加載額外的source map文件,方便調試。
四、有哪些常見的Plugin?他們是解決什麼問題的?
(1)uglifyjs-webpack-plugin:經過UglifyJS去壓縮js代碼;
(2)commons-chunk-plugin:提取公共代碼;
(3)define-plugin:定義環境變量。
五、loader和plugin的不一樣
做用不一樣:(1)loader讓webpack有加載和解析非js的能力;(2)plugin能夠擴展webpack功能,在webpack運行週期中會廣播不少事件,Plugin能夠監聽一些事件,經過webpack的api改變結果。
用法不一樣:(1)loader在module.rule中配置。類型爲數組,每一項都是Object;(2)plugin是單獨配置的,類型爲數組,每一項都是plugin實例,參數經過構造函數傳入。
六、webpack的構建流程是什麼?從讀取配置到輸出文件這個過程儘可能說全
Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:
(1)初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
(2)開始編譯:用上一步獲得的參數初始化 Compiler 對象,加載全部配置的插件,執行對象的 run 方法開始執行編譯;
(3)肯定入口:根據配置中的 entry 找出全部的入口文件;
(4)編譯模塊:從入口文件出發,調用全部配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理;
(5)完成模塊編譯:在通過第4步使用 Loader 翻譯完全部模塊後,獲得了每一個模塊被翻譯後的最終內容以及它們之間的依賴關係;
(6)輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會;
(7)輸出完成:在肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫入到文件系統。
在以上過程當中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。
七、是否寫過Loader和Plugin?描述一下編寫loader或plugin的思路?
編寫Loader時要遵循單一原則,每一個Loader只作一種"轉義"工做。 每一個Loader的拿到的是源文件內容(source),能夠經過返回值的方式將處理後的內容輸出,也能夠調用this.callback()方法,將內容返回給webpack。 還能夠經過 this.async()生成一個callback函數,再用這個callback將處理後的內容輸出出去。
Plugin的編寫就靈活了許多。 webpack在運行的生命週期中會廣播出許多事件,Plugin 能夠監聽這些事件,在合適的時機經過 Webpack 提供的 API 改變輸出結果。
八、webpack的熱更新是如何作到的?說明其原理?
webpack的熱更新又稱熱替換(Hot Module Replacement),縮寫爲HMR。 這個機制能夠作到不用刷新瀏覽器而將新變動的模塊替換掉舊的模塊。
原理:
分析:
(1)第一步,在 webpack 的 watch 模式下,文件系統中某一個文件發生修改,webpack 監聽到文件變化,根據配置文件對模塊從新編譯打包,並將打包後的代碼經過簡單的 JavaScript 對象保存在內存中。
(2)第二步是 webpack-dev-server 和 webpack 之間的接口交互,而在這一步,主要是 dev-server 的中間件 webpack-dev-middleware 和 webpack 之間的交互,webpack-dev-middleware 調用 webpack 暴露的 API對代碼變化進行監控,而且告訴 webpack,將代碼打包到內存中。
(3)第三步是 webpack-dev-server 對文件變化的一個監控,這一步不一樣於第一步,並非監控代碼變化從新打包。當咱們在配置文件中配置了devServer.watchContentBase 爲 true 的時候,Server 會監聽這些配置文件夾中靜態文件的變化,變化後會通知瀏覽器端對應用進行 live reload。注意,這兒是瀏覽器刷新,和 HMR 是兩個概念。
(4)第四步也是 webpack-dev-server 代碼的工做,該步驟主要是經過 sockjs(webpack-dev-server 的依賴)在瀏覽器端和服務端之間創建一個 websocket 長鏈接,將 webpack 編譯打包的各個階段的狀態信息告知瀏覽器端,同時也包括第三步中 Server 監聽靜態文件變化的信息。瀏覽器端根據這些 socket 消息進行不一樣的操做。固然服務端傳遞的最主要信息仍是新模塊的 hash 值,後面的步驟根據這一 hash 值來進行模塊熱替換。
(5)webpack-dev-server/client 端並不可以請求更新的代碼,也不會執行熱更模塊操做,而把這些工做又交回給了 webpack,webpack/hot/dev-server 的工做就是根據 webpack-dev-server/client 傳給它的信息以及 dev-server 的配置決定是刷新瀏覽器呢仍是進行模塊熱更新。固然若是僅僅是刷新瀏覽器,也就沒有後面那些步驟了。
(6)HotModuleReplacement.runtime 是客戶端 HMR 的中樞,它接收到上一步傳遞給他的新模塊的 hash 值,它經過 JsonpMainTemplate.runtime 向 server 端發送 Ajax 請求,服務端返回一個 json,該 json 包含了全部要更新的模塊的 hash 值,獲取到更新列表後,該模塊再次經過 jsonp 請求,獲取到最新的模塊代碼。這就是上圖中 七、八、9 步驟。
(7)而第 10 步是決定 HMR 成功與否的關鍵步驟,在該步驟中,HotModulePlugin 將會對新舊模塊進行對比,決定是否更新模塊,在決定更新模塊後,檢查模塊之間的依賴關係,更新模塊的同時更新模塊間的依賴引用。
(8)最後一步,當 HMR 失敗後,回退到 live reload 操做,也就是進行瀏覽器刷新來獲取最新打包代碼。
九、如何利用webpack來優化前端性能?(提升性能和體驗)
用webpack優化前端性能是指優化webpack的輸出結果,讓打包的最終結果在瀏覽器運行快速高效。
(1)壓縮代碼。刪除多餘的代碼、註釋、簡化代碼的寫法等等方式。能夠利用webpack的UglifyJsPlugin和ParallelUglifyPlugin來壓縮JS文件, 利用cssnano(css-loader?minimize)來壓縮css。使用webpack4,打包項目使用production模式,會自動開啓代碼壓縮。
(2)利用CDN加速。在構建過程當中,將引用的靜態資源路徑修改成CDN上對應的路徑。能夠利用webpack對於output參數和各loader的publicPath參數來修改資源路徑
(3)刪除死代碼(Tree Shaking)。將代碼中永遠不會走到的片斷刪除掉。能夠經過在啓動webpack時追加參數--optimize-minimize來實現或者使用es6模塊開啓刪除死代碼。
(4)優化圖片,對於小圖可使用 base64 的方式寫入文件中
(5)按照路由拆分代碼,實現按需加載,提取公共代碼。
(6)給打包出來的文件名添加哈希,實現瀏覽器緩存文件
十、如何提升webpack的構建速度?
(1)多入口的狀況下,使用commonsChunkPlugin來提取公共代碼;
(2)經過externals配置來提取經常使用庫;
(3)使用happypack實現多線程加速編譯;
(4)使用webpack-uglify-parallel來提高uglifyPlugin的壓縮速度。原理上webpack-uglify-parallel採用多核並行壓縮來提高壓縮速度;
(5)使用tree-shaking和scope hoisting來剔除多餘代碼。
十一、怎麼配置單頁應用?怎麼配置多頁應用?
單頁應用能夠理解爲webpack的標準模式,直接在entry中指定單頁應用的入口便可。
多頁應用的話,可使用webpack的 AutoWebPlugin來完成簡單自動化的構建,可是前提是項目的目錄結構必須遵照他預設的規範。
十二、npm打包時須要注意哪些?如何利用webpack來更好的構建?
NPM模塊須要注意如下問題:
(1)要支持CommonJS模塊化規範,因此要求打包後的最後結果也遵照該規則
(2)Npm模塊使用者的環境是不肯定的,頗有可能並不支持ES6,因此打包的最後結果應該是採用ES5編寫的。而且若是ES5是通過轉換的,請最好連同SourceMap一同上傳。
(3)Npm包大小應該是儘可能小(有些倉庫會限制包大小)
(4)發佈的模塊不能將依賴的模塊也一同打包,應該讓用戶選擇性的去自行安裝。這樣能夠避免模塊應用者再次打包時出現底層模塊被重複打包的狀況。
(5)UI組件類的模塊應該將依賴的其它資源文件,例如.css文件也須要包含在發佈的模塊裏。
基於以上須要注意的問題,咱們能夠對於webpack配置作如下擴展和優化:
(1)CommonJS模塊化規範的解決方案: 設置output.libraryTarget='commonjs2'使輸出的代碼符合CommonJS2 模塊化規範,以供給其它模塊導入使用;
(2)輸出ES5代碼的解決方案:使用babel-loader把 ES6 代碼轉換成 ES5 的代碼。再經過開啓devtool: 'source-map'輸出SourceMap以發佈調試。
(3)Npm包大小盡可能小的解決方案:Babel 在把 ES6 代碼轉換成 ES5 代碼時會注入一些輔助函數,最終致使每一個輸出的文件中都包含這段輔助函數的代碼,形成了代碼的冗餘。解決方法是修改.babelrc文件,爲其加入transform-runtime插件
(4)不能將依賴模塊打包到NPM模塊中的解決方案:使用externals配置項來告訴webpack哪些模塊不須要打包。
(5)對於依賴的資源文件打包的解決方案:經過css-loader和extract-text-webpack-plugin來實現,配置以下:
1三、如何在vue項目中實現按需加載?
常常會引入現成的UI組件庫如ElementUI、iView等,可是他們的體積和他們所提供的功能同樣,是很龐大的。
不過不少組件庫已經提供了現成的解決方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安裝以上插件後,在.babelrc配置中或babel-loader的參數中進行設置,便可實現組件按需加載了。
單頁應用的按需加載 如今不少前端項目都是經過單頁應用的方式開發的,可是隨着業務的不斷擴展,會面臨一個嚴峻的問題——首次加載的代碼量會愈來愈多,影響用戶的體驗。
7、參考
一、https://github.com/webpack/docs/wiki/how-to-write-a-plugin
二、https://webpack.js.org/api/compiler-hooks/
三、https://webpack.js.org/concepts/loaders
四、https://webpack.js.org/concepts/plugins
【謝謝關注和閱讀,後續新的文章首發:sau交流學習社區:https://www.mwcxs.top/】