vue模塊化開發,webpack那些事:淺入深出-源碼解析構建優化

 2023-10-15 阅读 29 评论 0

摘要:基礎知識回顧 入口(entry) module.exports = {entry: './path/to/my/entry/file.js' }; //或者 module.exports = {entry: {main: './path/to/my/entry/file.js'} }; 輸出(output) module.exports = {output: {filename:'[name][chunkhash:8

image-20210127111317254

基礎知識回顧

  • 入口(entry)

    module.exports = {entry: './path/to/my/entry/file.js'
    };
    //或者
    module.exports = {entry: {main: './path/to/my/entry/file.js'}
    };
    
  • 輸出(output)

    module.exports = {output: {filename:'[name][chunkhash:8].js',path:path.resolve(__dirname,'dist')}
    };
    
  • loader
    預處理loader

    • css-loader 處理css中路徑引用等問題
    • style-loader 動態把樣式寫入css
    • sass-loader scss編譯器
    • less-loader less編譯器
    • postcss-loader scss再處理

    vue模塊化開發。處理js loader

    • babel-loader
    • jsx-loader
    • ts-loader

    圖片處理loader

    • url-loader
  • 插件(plugin)
    plugins里面放的是插件,插件的作用在于提高開發效率,能夠解放雙手,讓我們去做更多有意義的事情。一些很low的事就統統交給插件去完成。

    const webpackConfig = {plugins: [//清除文件new CleanWebpackPlugin(),//css單獨打包new MiniCssExtractPlugin({filename: "[name].css",chunkFilename: "[name].css"}),// 引入熱更新插件new webpack.HotModuleReplacementPlugin() ]
    }
    
  • 模式(mode)

    • production 生產環境
  • development 開發環境

    • 提升了構建速度
    • 默認為開發環境,不需要專門配置
    • 提供壓縮功能,不需要借助插件
    • 提供SouceMap,不需要專門配置
  • web前端,瀏覽器兼容性(browser compatibility)

  • 環境(environment)

項目中詳細配置

構建過程

Webpack 處理應用程序時,它會遞歸地構建一個依賴關系圖(dependency graph),其中包含應用程序需要的每個模塊,然后將所有模塊打包成一個或多個 bundle

其實就是:Webpack 是一個 JS 代碼打包器。

前端webpack。至于圖片、CSS、Less、TS等其他文件,就需要 Webpack 配合 loader 或者 plugin 功能來實現。

構建流程

  1. 根據配置,識別入口文件;
  2. 逐層識別模塊依賴(包括 Commonjs、AMD、或 ES6 的 import 等,都會被識別和分析);
  3. Webpack 主要工作內容就是分析代碼,轉換代碼,編譯代碼,最后輸出代碼;
  4. 輸出最后打包后的代碼。

webpack構建的三個階段:

  1. 初始化階段
  2. 編譯階段
  3. 輸出階段

初始化

  • 初始化參數: 從配置文件和 Shell 語句中讀取與合并參數,得出最終的參數。這個過程中還會執行配置文件中的插件實例化語句 new Plugin()。

  • 初始化默認參數配置: new WebpackOptionsDefaulter().process(options)

  • 實例化Compiler對象:用上一步得到的參數初始化Compiler實例,Compiler負責文件監聽和啟動編譯。Compiler實例中包含了完整的Webpack配置,全局只有一個Compiler實例。

  • webpack4樣式加載。加載插件: 依次調用插件的apply方法,讓插件可以監聽后續的所有事件節點。同時給插件傳入compiler實例的引用,以方便插件通過compiler調用Webpack提供的API。

  • 處理入口: 讀取配置的Entrys,為每個Entry實例化一個對應的EntryPlugin,為后面該Entry的遞歸解析工作做準備

編譯

1、生成chunk

chunk是webpack內部運行時的概念;一個chunk是對依賴圖的部分進行封裝的結果(``Chunkthe class is the encapsulation for parts of your dependency graph);可以通過多個entry-point來生成一個chunk
chunk可以分為三類;

  • entry chunk
    • 包含webpack runtime code并且是最先執行的chunk
  • initial chunk
    • 包含同步加載進來的module且不包含runtime code的chunk
    • 在entry chunk執行后再執行的
  • normal chunk
    • 使用require.ensureSystem.importimport()異步加載進來的module,會被放到normal chunk中
image-20210127170328144

每個chunk都至少有一個屬性:

  • name: 默認為main
  • id: 唯一的編號,開發環境和name相同,生產環境是一個數字,從0開始

2、構建依賴模塊

var compiler = webpack(options);

webpack5。屏幕快照 2020-07-15 15.52.36.png

從入口文件index.js開始分析,檢查右側表格中的記錄,如果有記錄就結束。沒有記錄就繼續讀取文件內容,讀取完文件內容后,開始進行抽象樹語法分析,將代碼字符串轉換成一個對象的描述文件。并將其中的依賴保存在dependencies數組中

dependencies:["./src/a.js"]

保存完以后,替換依賴函數

console.log("index.js");
_webpack_reuqire("./src/a.js");

將轉換后的代碼字符串保存在右側的表格中

模塊id轉換后的代碼
./src/index.jsconsole.log(“index.js”);_webpack_reuqire("./src/a.js");

因為dependencies中有數據,開始遞歸解析dependencies中的數據。取出.src/a.js

// .src/a.js
console.log("a.js");
require("b")

webpack、查看右側表格,發現沒有a.js,開始讀取文件內容,生成ast抽象語法樹,將依賴記錄在數組中

dependencies: ["./src/b.js"]

然后替換函數依賴

console.log("a.js");
_webpack_require("./src/b.js");
module.exports = "a"

將轉換后的代碼記錄在右側的表格中

模塊id轉換后的代碼
./src/index.jsconsole.log(“index.js”);_webpack_reuqire("./src/a.js");
./src/a.jsconsole.log(“a.js”);_webpack_require("./src/b.js");module.exports = “a”

然后繼續取出來dependencies的內容./src/b.js

console.log("b.js");
module.exports = "b";

發現右側表格中沒有b.js這個文件,就繼續讀取文件內容,進行ast抽象語法樹分析,發現沒有依賴項,就不需要往數組中放東西,也不需要替換依賴項,將代碼字符串存在表格中

模塊id轉換后的代碼
./src/index.jsconsole.log(“index.js”);_webpack_reuqire("./src/a.js");
./src/a.jsconsole.log(“a.js”);_webpack_require("./src/b.js");module.exports = “a”
./src/b.jsconsole.log(“b.js”);module.exports = “b”;

vue.js項目實戰。然后遞歸回去,發現index下產生的數組是空,整個文件依賴就解析完畢

3、產生chunk assets

在第二步完成以后,chunk中會產生一個模塊列表,列表中包含了模塊id模塊轉換后的代碼

接下來,webpack會根據配置為chunk生成一個資源列表,即chunk assets,資源列表可以理解為是生成到最終文件的文件名和文件內容

  • 為什么叫資源列表呢?
  • 因為有可能配置devtool生成的除了./dist/main.js還有./dist/main.js.map

即:文件名:./dist/main.js

文件內容:

(function(){})({"./src/index.js": function(){//是否是eval可以根據devtool來設置,有很多種方式eval("console.log(\"index module\");\nvar a = __webpack_require__(/*! ./a */ \"./src/a.js\"); \na.abc();\nconsole.log(a);\n\n\n//# sourceURL=webpack:///./src/index.js?")}
})

webpack源碼解析。屏幕快照 2020-07-15 17.04.09.png

chunk hash: 是根據所有的chunk assets的內容生成的一個hash字符串
hash: 一種算法,具有很多分類。特點是將一個任意長度的字符串轉換成一個固定長度的字符串,而且可以保證原始內容不變

就是將我們上面生成的文件內容,全部聯合起來,然后生成一個固定長度的哈希值鏈接

簡圖: 屏幕快照 2020-07-15 17.13.36.png

多個chunk assets就是一個bundle(一捆)

4、合并chunk assets

深入淺出webpack。將多個chunk的assets合并到一起,并產生一個總的hash 屏幕快照 2020-07-15 17.16.11.png

輸出

webpack將利用node中的fs模塊(文件處理模塊),根據編譯產生的總的assets,生成相應的文件

屏幕快照 2020-07-15 17.22.03.png

涉及術語

  1. module: 模塊,分割的代碼單元,webpack中的模塊可以是任何內容的文件,不僅限于JS
  2. chunk: webpack內部構建模塊的塊,一個chunk中包含多個模塊,這些模塊是從入口模塊通過依賴分析得來的
  3. bundle:chunk構建好模塊后會生成chunk的資源清單,清單中的每一項就是一個bundle,可以認為bundle就是最終生成的文件
  4. hash:最終的資源清單所有內容聯合生成的hash值
  5. chunkhash: chunk生成的資源清單內容聯合生成的hash值
  6. chunkname:chunk的名稱,如果沒有配置則使用main
  7. id: 通常指chunk的唯一編號,如果在開發環境下構建,和chunkname相同;如果是生產環境下構建,則使用一個從0開始的數字進行編號

HMR熱更新原理

簡介

Hot Module Replacement(以下簡稱:HMR 模塊熱替換)是 Webpack 提供的一個非常有用的功能,它允許在 JavaScript 運行時更新各種模塊,而無需完全刷新

當我們修改代碼并保存后,Webpack 將對代碼重新打包,HMR 會在應用程序運行過程中替換、添加或刪除模塊,而無需重新加載整個頁面。
HMR 主要通過以下幾種方式,來顯著加快開發速度:

  • 保留在完全重新加載頁面時丟失的應用程序狀態;
  • 只更新變更內容,以節省寶貴的開發時間;
  • 調整樣式更加快速 - 幾乎相當于在瀏覽器調試器中更改樣式。

服務啟動

webpackmiddle源碼、webpack-dev-server:不是一個插件,而是一個web服務器

服務啟動流程

image-20210128141442540

webpack-dev-server源碼解析

//啟動服務的具體方法
function startDevServer(config, options) {const log = createLogger(options);//聲明全局webpack實例let compiler;try {compiler = webpack(config);} catch (err) {if (err instanceof webpack.WebpackOptionsValidationError) {log.error(colors.error(options.stats.colors, err.message));// eslint-disable-next-line no-process-exitprocess.exit(1);}throw err;}try {//創建server服務server = new Server(compiler, options, log);serverData.server = server;} catch (err) {if (err.name === 'ValidationError') {log.error(colors.error(options.stats.colors, err.message));// eslint-disable-next-line no-process-exitprocess.exit(1);}throw err;}if (options.socket) {//設置服務監聽server.listeningApp.on('error', (e) => {if (e.code === 'EADDRINUSE') {//使用socket建立長連接//初始化socketconst clientSocket = new net.Socket();clientSocket.on('error', (err) => {if (err.code === 'ECONNREFUSED') {// No other server listening on this socket so it can be safely removedfs.unlinkSync(options.socket);server.listen(options.socket, options.host, (error) => {if (error) {throw error;}});}});clientSocket.connect({ path: options.socket }, () => {throw new Error('This socket is already used');});}});server.listen(options.socket, options.host, (err) => {if (err) {throw err;}// chmod 666 (rw rw rw)const READ_WRITE = 438;fs.chmod(options.socket, READ_WRITE, (err) => {if (err) {throw err;}});});} else {server.listen(options.port, options.host, (err) => {if (err) {throw err;}});}
}
//啟動webpack-dev-server服務器
processOptions(config, argv, (config, options) => {startDevServer(config, options);
});

server.js源碼解析

class Server {constructor(compiler, options = {}, _log) {......//構造函數初始化服務}//創建初始化express應用setupApp() {this.app = new express();}// 綁定監聽事件setupHooks() {//當監聽到一次webpack編譯結束,就會調用_sendStats方法通過websocket給瀏覽器發送通知,//ok和hash事件,這樣瀏覽器就可以拿到最新的hash值了,做檢查更新邏輯const addHooks = (compiler) => {const { compile, invalid, done } = compiler.hooks;compile.tap('webpack-dev-server', invalidPlugin);invalid.tap('webpack-dev-server', invalidPlugin);// 監聽webpack的done鉤子,tapable提供的監聽方法done.tap('webpack-dev-server', (stats) => {this._sendStats(this.sockets, this.getStats(stats));this._stats = stats;});};......}//使用webpack-dev-middleware中間件,返回生成的bundle文件setupDevMiddleware() {// middleware for serving webpack bundlethis.middleware = webpackDevMiddleware(this.compiler,Object.assign({}, this.options, { logLevel: this.log.options.level }));}......//創建http服務,并啟動服務createServer() { ... }//創建socket服務器建立長連接createSocketServer() {......}//使用socket在服務器和瀏覽器直接建立一個websocket長連接listen(port, hostname, fn){ ... }// 通過websoket給客戶端發消息_sendStats(sockets, stats, force) {......this.sockWrite(sockets, 'hash', stats.hash);if (stats.errors.length > 0) {this.sockWrite(sockets, 'errors', stats.errors);} else if (stats.warnings.length > 0) {this.sockWrite(sockets, 'warnings', stats.warnings);} else {this.sockWrite(sockets, 'ok');}}

webpack打包查看源碼、client/index.js源碼解析

var onSocketMessage = { hash: function hash(_hash) {// 更新currentHash值status.currentHash = _hash;},ok: function ok() {sendMessage('Ok');// 進行更新檢查等操作reloadApp(options, status);},
}
// 連接服務地址socketUrl,?http://localhost:8080,本地服務地址
socket(socketUrl, onSocketMessage);

熱更新

熱更新流程

webpackHMR流程

  1. 文件系統發生變化
  2. 當監聽到文件發生變化時,webpack 使用HotModuleReplacementPlugin編譯文件,并將代碼保存在內存中(webpack-dev-middleware)。
  3. 同時,webpack-dev-server通過編譯器compiler獲得文件的編譯情況。
  4. 在compiler的 done 鉤子函數(生命周期)里調用_sendStats發送向client發送hash值,在client保存下來。
  5. client接收到ok或warning消息后調用reloadApp發布客戶端檢查更新事件webpackHotUpdate。
  6. webpack/hot監聽到webpackHotUpdate事件,調用check方法進行hash值對比以及檢查各modules是否需要更新。
  7. 調用JsonpMainTemplate.runtime的hotDownloadManifest方法向server端發送ajax請求
  8. 服務端返回hot-update.json(manifest)文件,該文件包含所有要更新模塊的hash值和chunk名。
  9. JsonpRuntime根據返回的json值使用jsonp請求具體的代碼塊
  10. jsonp返回最新的chunk代碼,并直接執行。
  11. HotModulePlugin 將會對新舊模塊進行對比,決定是否更新模塊,檢查模塊之間的依賴關系,更新模塊的同時更新模塊間的依賴引用。
  12. HMR runtime本身并不會處理代碼修改,它會將不同文件交給對應的loader runtime處理
  13. 更新代碼
  14. 如果更新失敗,則直接刷新

webpaserver端源碼

在項目初始化時,服務端與客戶端已經開啟了長連接服務,當webpack對文件編譯產生變化時,服務端會及時通知客戶端。

class Server {...setupHooks() {//添加webpack的done事件回調const addHooks = (compiler) => {const { compile, invalid, done } = compiler.hooks;//通知正在客戶端編譯  compile.tap(\'webpack-dev-server\', invalidPlugin);done.tap(\'webpack-dev-server\', (stats) => {//編譯完成向客戶端發送消息this._sendStats(this.sockets, this.getStats(stats)); this._stats = stats;});};addHooks(this.compiler);} _sendStats(sockets, stats, force) {if (...) { //無變化則returnreturn this.sockWrite(sockets, \'still-ok\');}//如果有變化,則發送hash值this.sockWrite(sockets, \'hash\', stats.hash);if (stats.errors.length > 0) {this.sockWrite(sockets, \'errors\', stats.errors);} else {//沒有報錯發送okthis.sockWrite(sockets, \'ok\');}}...//使用sockjs在瀏覽器端和服務端之間建立一個 websocket 長連接listen(port, hostname, fn) {...}
}

這里仍然是Server.js中的代碼,我詳細的寫展示了setupHooks中的代碼,setupHooks 調用 webpack api 監聽 compile的 done 事件,當編譯完成,執行done鉤子,調用_sendStats,在_sendStats方法中如果文件變化則發送hash。最后發送ok,客戶端在接受到OK后會執行reload。

client端源碼

客戶端socket接受到hash后保存起來,隨后接受到ok執行reload命令。

//Client/index.js
var onSocketMessage = {...hash: function hash(_hash) {//將hash保存到全局currentHash中status.currentHash = _hash; },ok: function ok() {...//執行更新的reloadApp函數reloadApp(options, status); },...
};
socket(socketUrl, onSocketMessage);
//Client/utils/reloadApp.js
function reloadApp(_ref, _ref2) {if (hot) {//hotEmitter是events類,webpack-dev-server發布webpackHotUpdate給webapckvar hotEmitter = require(\'webpack/hot/emitter\');hotEmitter.emit(\'webpackHotUpdate\', currentHash);if (typeof self !== \'undefined\' && self.window) {// broadcast update to windowself.postMessage("webpackHotUpdate".concat(currentHash), \'*\');}} 
}

Webpack多入口css?客戶端接收到ok指令后,執行reloadApp函數。reloadApp函數中,hotEmitter其實是events模塊的實例,即在全局實現發布訂閱模式,hotEmitter發布了webpackHotUpdate事件,同時webpack訂閱這個指令。

在這里以后,瀏覽器端進入webpack的代碼,webpack-dev-server在客戶端的部分完成。

訂閱webpackHotUpdate事件的代碼在webpack/hot/dev-server.js中:

if (module.hot) {var lastHash;var check = function check() {module.hot.check(true).then(function (updatedModules) {//檢查所有要更新的模塊,如果沒有模塊要更新那么回調函數就是nullif (!updatedModules) {window.location.reload();return;}if (!upToDate()) {//如果還有更新check();}})};var hotEmitter = require("./emitter");hotEmitter.on("webpackHotUpdate", function (currentHash) {lastHash = currentHash;check(); //調用check方法});
}

module為全局對象,module.hot的代碼在HMR runtime中,module.hot.check對應hotCheck方法:

hotCheck = () => { //module.hot.check方法return hotDownloadManifest.then((update) => { //保存全局的熱更新信息hotAvailableFilesMap = update.c;hotUpdateNewHash = update.h;/*globals chunkId */hotEnsureUpdateChunk(chunkId)})
}
hotDownloadManifest(){ //ajax請求模塊manifestreturn new Promise(...);
}
hotEnsureUpdateChunk(){ //檢測模塊if (!hotAvailableFilesMap[chunkId]) {...} else {hotRequestedFilesMap[chunkId] = true;hotDownloadUpdateChunk();}
}
hotDownloadUpdateChunk(){} //jsonp格式請求代碼模塊chunk//chunk是js代碼塊,格式是webpackHotUpdate("main", {...}),收到后直接執行,window全局中有對應方法
window["webpackHotUpdate"]=function webpackHotUpdateCallback(){hotAddUpdateChunk()
}
hotAddUpdateChunk(){//動態的更新代碼模塊for (var moduleId in moreModules) {//記錄全局的熱更新模塊hotUpdate[moduleId] = moreModules[moduleId];}hotUpdateDownloaded()
}
hotUpdateDownloaded(){ //執行hotApply模塊hotApply()
}
hotApply(){//將代碼更新到modules中
}

主要包含了兩個請求,在hotDownloadManifest中客戶端請求了ajax的manifest,他的格式為 {"h":"bbff25e45ca71af784d0","c":{"main":true}} 包含了要更新模塊的hash值和chunk名;另一個hotDownloadUpdateChunk通過jsonp方法請求更新的代碼塊,
hotDownloadUpdateChunk獲取更新的代碼
獲取到的代碼塊可以直接執行,webpack已經在window中注冊了webpackHotUpdate方法,執行后調用hotApply熱模塊替換方法。

function hotApply(options) {function getAffectedStuff(updateModuleId) {...return { //返回過期的模塊和依賴type: "accepted",moduleId: updateModuleId,outdatedModules: outdatedModules,outdatedDependencies: outdatedDependencies};}...result = getAffectedStuff(moduleId);...{switch (result.type) {case "self-declined":...break;case "accepted"://對結果進行標記及處理if (options.onAccepted) options.onAccepted(result);doApply = true; break;case "disposed":...break;default:...}...while (queue.length > 0) {moduleId = queue.pop();...delete installedModules[moduleId];//刪除過期的模塊和依賴delete outdatedDependencies[moduleId];}...for (moduleId in appliedUpdate) { if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {//新的模塊添加到modules中modules[moduleId] = appliedUpdate[moduleId];}}...
}

webpack按需加載原理。模塊熱替換主要分三個部分,首先是找出 outdatedModules 和 outdatedDependencies;然后從緩存中刪除這些;最后,將新的模塊添加到 modules 中,當下次調用 webpack_require (webpack 重寫的 require 方法)方法的時候,就是獲取到了新的模塊代碼了。

如果在熱更新過程中出現錯誤,熱更新將回退到刷新瀏覽器。

當用新的模塊代碼替換老的模塊后,但是我們的業務代碼并不能知道代碼已經發生變化,也就是說,當入口文件修改后,我們需要在入口文件中調用 HMR 的 accept 方法

// index.js
if(module.hot) {module.hot.accept(\'./main.js\', function() {render()})
}

更新的代碼每次在下面這個循環中執行, cb(moduleOutdatedDependencies)
就是module.hot.accept的內容,從而實現對代碼的渲染

function hotApply(options) {...for (moduleId in outdatedDependencies) {...moduleOutdatedDependencies = outdatedDependencies[moduleId];var callbacks = [];for (i = 0; i < moduleOutdatedDependencies.length; i++) {dependency = moduleOutdatedDependencies[i];cb = module.hot._acceptedDependencies[dependency];callbacks.push(cb); //獲取所有的模塊}for (i = 0; i < callbacks.length; i++) {cb = callbacks[i];cb(moduleOutdatedDependencies);//執行代碼模塊}...}...
}

手寫webpack構建工具

手寫webpack流程

image-20210128153623321

AST

AST(Abstract Syntax Tree)

抽象語法樹,源代碼語法結構的一種抽象表示

  • 以樹狀的形式表現編程語言的語法結構
  • 樹上的每個節點都表示源代碼中的一種結構

image-20210128190259694

AST生成過程

抽象語法樹的生成主要依靠的是Javascript Parser(js解析器)

  • 詞法分析(Lexical Analysis)
  • 語法分析(Parse Analysis)

image-20210128190704960

在手寫webpack中使用

通過Visitor完成依賴的收集

訪問者(visitor)是一個用于 AST 遍歷的跨語言模式,定義 了用于在一個樹狀結構中獲取具體節點的方法

image-20210128190855377

樹的寬度優先搜索(BFS)算法思想

應用于循環分析依賴

image-20210128190936336

樹的寬度優先搜索(BFS)算法思想

循環分析結果

image-20210128191032396

打包結果為一個IIFE

image-20210128191102224

打包結果分析

image-20210128191133160

結果運行分析

image-20210128191208190

webpack構建優化

背景

如今前端工程化的概念早已經深入人心,選擇一款合適的編譯和資源管理工具已經成為了所有前端工程中的標配,而在諸多的構建工具中,webpack以其豐富的功能和靈活的配置而深受業內吹捧,逐步取代了grunt和gulp成為大多數前端工程實踐中的首選,React,Vue,Angular等諸多知名項目也都相繼選用其作為官方構建工具,極受業內追捧。但是,隨者工程開發的復雜程度和代碼規模不斷地增加,webpack暴露出來的各種性能問題也愈發明顯,極大的影響著開發過程中的體驗。

問題歸納

歷經了多個web項目的實戰檢驗,我們對webapck在構建中逐步暴露出來的性能問題歸納主要有如下幾個方面:

代碼全量構建速度過慢,即使是很小的改動,也要等待長時間才能查看到更新與編譯后的結果(引入HMR熱更新后有明顯改進);
隨著項目業務的復雜度增加,工程模塊的體積也會急劇增大,構建后的模塊通常要以M為單位計算;
多個項目之間共用基礎資源存在重復打包,基礎庫代碼復用率不高;
node的單進程實現在耗cpu計算型loader中表現不佳;
針對以上的問題,我們來看看怎樣利用webpack現有的一些機制和第三方擴展插件來逐個擊破。

慢在何處

作為工程師,我們一直鼓勵要理性思考,用數據和事實說話,“我覺得很慢”,“太卡了”,“太大了”之類的表述難免顯得太籠統和太抽象,那么我們不妨從如下幾個方面來著手進行分析:

從項目結構著手,代碼組織是否合理,依賴使用是否合理;
從webpack自身提供的優化手段著手,看看哪些api未做優化配置;
從webpack自身的不足著手,做有針對性的擴展優化,進一步提升效率;
在這里我們推薦使用一個wepback的可視化資源分析工具:webpack-bundle-analyzer,在webpack構建的時候會自動幫你計算出各個模塊在你的項目工程中的依賴與分布情況,方便做更精確的資源依賴和引用的分析。

從上圖中我們不難發現大多數的工程項目中,依賴庫的體積永遠是大頭,通常體積可以占據整個工程項目的7-9成,而且在每次開發過程中也會重新讀取和編譯對應的依賴資源,這其實是很大的的資源開銷浪費,而且對編譯結果影響微乎其微,畢竟在實際業務開發中,我們很少會去主動修改第三方庫中的源碼,改進方案如下:

方案一、合理配置 CommonsChunkPlugin

webpack的資源入口通常是以entry為單元進行編譯提取,那么當多entry共存的時候,CommonsChunkPlugin的作用就會發揮出來,對所有依賴的chunk進行公共部分的提取,但是在這里可能很多人會誤認為抽取公共部分指的是能抽取某個代碼片段,其實并非如此,它是以module為單位進行提取。

假設我們的頁面中存在entry1,entry2,entry3三個入口,這些入口中可能都會引用如utils,loadash,fetch等這些通用模塊,那么就可以考慮對這部分的共用部分機提取。通常提取方式有如下四種實現:

1、傳入字符串參數,由chunkplugin自動計算提取

new webpack.optimize.CommonsChunkPlugin('common.js')

這種做法默認會把所有入口節點的公共代碼提取出來, 生成一個common.js

2、有選擇的提取公共代碼

new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);

只提取entry1節點和entry2中的共用部分模塊, 生成一個common.js

3、將entry下所有的模塊的公共部分(可指定引用次數)提取到一個通用的chunk中

new webpack.optimize.CommonsChunkPlugin({name: 'vendors',minChunks: function (module, count) {return (module.resource &&/\.js$/.test(module.resource) &&module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0)}
});

提取所有node_modules中的模塊至vendors中,也可以指定minChunks中的最小引用數;

4、抽取enry中的一些lib抽取到vendors中

entry = {vendors: ['fetch', 'loadash']
};
new webpack.optimize.CommonsChunkPlugin({name: "vendors",minChunks: Infinity
});

添加一個entry名叫為vendors,并把vendors設置為所需要的資源庫,CommonsChunk會自動提取指定庫至vendors中。

方案二、通過 externals 配置來提取常用庫

在實際項目開發過程中,我們并不需要實時調試各種庫的源碼,這時候就可以考慮使用external選項了。

簡單來說external就是把我們的依賴資源聲明為一個外部依賴,然后通過script外鏈腳本引入。這也是我們早期頁面開發中資源引入的一種翻版,只是通過配置后可以告知webapck遇到此類變量名時就可以不用解析和編譯至模塊的內部文件中,而改用從外部變量中讀取,這樣能極大的提升編譯速度,同時也能更好的利用CDN來實現緩存。

external的配置相對比較簡單,只需要完成如下三步:

1、在頁面中加入需要引入的lib地址,如下:

<head>
<script src="//cdn.bootcss.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/underscore.min.js"></script>
<script src="/static/common/react.min.js"></script>
<script src="/static/common/react-dom.js"></script>
<script src="/static/common/react-router.js"></script>
<script src="/static/common/immutable.js"></script>
</head>

2、在webapck.config.js中加入external配置項:

module.export = {externals: {'react-router': {amd: 'react-router',root: 'ReactRouter',commonjs: 'react-router',commonjs2: 'react-router'},react: {amd: 'react',root: 'React',commonjs: 'react',commonjs2: 'react'},'react-dom': {amd: 'react-dom',root: 'ReactDOM',commonjs: 'react-dom',commonjs2: 'react-dom'}}
}

這里要提到的一個細節是:此類文件在配置前,構建這些資源包時需要采用amd/commonjs/cmd相關的模塊化進行兼容封裝,即打包好的庫已經是umd模式包裝過的,如在node_modules/react-router中我們可以看到umd/ReactRouter.js之類的文件,只有這樣webpack中的requireimport * from 'xxxx'才能正確讀到該類包的引用,在這類js的頭部一般也能看到如下字樣:

if (typeof exports === ‘object’ && typeof module === ‘object’) {
module.exports = factory(require(“react”));
} else if (typeof define === ‘function’ && define.amd) {
define([“react”], factory);
} else if (typeof exports === ‘object’) {
exports[“ReactRouter”] = factory(require(“react”));
} else {
root[“ReactRouter”] = factory(root[“React”]);
}

3、非常重要的是一定要在output選項中加入如下一句話:

output: {libraryTarget: 'umd'
}

由于通過external提取過的js模塊是不會被記錄到webapckchunk信息中,通過libraryTarget可告知我們構建出來的業務模塊,當讀到了externals中的key時,需要以umd的方式去獲取資源名,否則會有出現找不到module的情況。

通過配置后,我們可以看到對應的資源信息已經可以在瀏覽器的source map中讀到了。

對應的資源也可以直接由頁面外鏈載入,有效地減小了資源包的體積。

方案三、利用 DllPlugin 和 DllReferencePlugin 預編譯資源模塊

我們的項目依賴中通常會引用大量的npm包,而這些包在正常的開發過程中并不會進行修改,但是在每一次構建過程中卻需要反復的將其解析,如何來規避此類損耗呢?這兩個插件就是干這個用的。

簡單來說DllPlugin的作用是預先編譯一些模塊,而DllReferencePlugin則是把這些預先編譯好的模塊引用起來。這邊需要注意的是DllPlugin必須要在DllReferencePlugin執行前先執行一次,dll這個概念應該也是借鑒了windows程序開發中的dll文件的設計理念。

相對于externals,dllPlugin有如下幾點優勢:

  • dll預編譯出來的模塊可以作為靜態資源鏈接庫可被重復使用,尤其適合多個項目之間的資源共享,如同一個站點pc和手機版等;
  • dll資源能有效地解決資源循環依賴的問題,部分依賴庫如:react-addons-css-transition-group這種原先從react核心庫中抽取的資源包,整個代碼只有一句話:
module.exports = require('react/lib/ReactCSSTransitionGroup');

卻因為重新指向了react/lib中,這也會導致在通過externals引入的資源只能識別react,尋址解析react/lib則會出現無法被正確索引的情況。

  • 由于externals的配置項需要對每個依賴庫進行逐個定制,所以每次增加一個組件都需要手動修改,略微繁瑣,而通過dllPlugin則能完全通過配置讀取,減少維護的成本;

1、配置dllPlugin對應資源表并編譯文件

那么externals該如何使用呢,其實只需要增加一個配置文件:webpack.dll.config.js

const webpack = require('webpack');
const path = require('path');
const isDebug = process.env.NODE_ENV === 'development';
const outputPath = isDebug ? path.join(__dirname, '../common/debug') : path.join(__dirname, '../common/dist');
const fileName = '[name].js';// 資源依賴包,提前編譯
const lib = ['react','react-dom','react-router','history','react-addons-pure-render-mixin','react-addons-css-transition-group','redux','react-redux','react-router-redux','redux-actions','redux-thunk','immutable','whatwg-fetch','byted-people-react-select','byted-people-reqwest'
];const plugin = [new webpack.DllPlugin({/*** path* 定義 manifest 文件生成的位置* [name]的部分由entry的名字替換*/path: path.join(outputPath, 'manifest.json'),/*** name* dll bundle 輸出到那個全局變量上* 和 output.library 一樣即可。*/name: '[name]',context: __dirname}),new webpack.optimize.OccurenceOrderPlugin()
];if (!isDebug) {plugin.push(new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}),new webpack.optimize.UglifyJsPlugin({mangle: {except: ['$', 'exports', 'require']},compress: { warnings: false },output: { comments: false }}))
}module.exports = {devtool: '#source-map',entry: {lib: lib},output: {path: outputPath,filename: fileName,/*** output.library* 將會定義為 window.${output.library}* 在這次的例子中,將會定義為`window.vendor_library`*/library: '[name]',libraryTarget: 'umd',umdNamedDefine: true},plugins: plugin
};

然后執行命令:

$ NODE_ENV=development webpack --config  webpack.dll.lib.js --progress
$ NODE_ENV=production webpack --config  webpack.dll.lib.js --progress 

即可分別編譯出支持調試版和生產環境中lib靜態資源庫,在構建出來的文件中我們也可以看到會自動生成如下資源:

common
├── debug│   ├── lib.js│   ├── lib.js.map│   └── manifest.json
└── dist├── lib.js├── lib.js.map└── manifest.json

文件說明:

lib.js可以作為編譯好的靜態資源文件直接在頁面中通過src鏈接引入,與externals的資源引入方式一樣,生產與開發環境可以通過類似charles之類的代理轉發工具來做路由替換;
manifest.json中保存了webpack中的預編譯信息,這樣等于提前拿到了依賴庫中的chunk信息,在實際開發過程中就無需要進行重復編譯;

2、dllPlugin的靜態資源引入

lib.js和manifest.json存在一一對應的關系,所以我們在調用的過程也許遵循這個原則,如當前處于開發階段,對應我們可以引入common/debug文件夾下的lib.js和manifest.json,切換到生產環境的時候則需要引入common/dist下的資源進行對應操作,這里考慮到手動切換和維護的成本,我們推薦使用add-asset-html-webpack-plugin進行依賴資源的注入,可得到如下結果:

<head>
<script src="/static/common/lib.js"></script>
</head>
在webpack.config.js文件中增加如下代碼:const isDebug = (process.env.NODE_ENV === 'development');
const libPath = isDebug ? '../dll/lib/manifest.json' : 
'../dll/dist/lib/manifest.json';// 將mainfest.json添加到webpack的構建中module.export = {plugins: [new webpack.DllReferencePlugin({context: __dirname,manifest: require(libPath),})]
}

配置完成后我們能發現對應的資源包已經完成了純業務模塊的提取

多個工程之間如果需要使用共同的lib資源,也只需要引入對應的lib.js和manifest.js即可,plugin配置中也支持多個webpack.DllReferencePlugin同時引入使用,如下:

module.export = {plugins: [new webpack.DllReferencePlugin({context: __dirname,manifest: require(libPath),}),new webpack.DllReferencePlugin({context: __dirname,manifest: require(ChartsPath),})]

方案四、使用 Happypack 加速你的代碼構建

以上介紹均為針對webpack中的chunk計算和編譯內容的優化與改進,對資源的實際體積改進上也較為明顯,那么除此之外,我們能否針對資源的編譯過程和速度優化上做些嘗試呢?

眾所周知,webpack中為了方便各種資源和類型的加載,設計了以loader加載器的形式讀取資源,但是受限于node的編程模型影響,所有的loader雖然以async的形式來并發調用,但是還是運行在單個 node的進程以及在同一個事件循環中,這就直接導致了當我們需要同時讀取多個loader文件資源時,比如babel-loader需要transform各種jsx,es6的資源文件。在這種同步計算同時需要大量耗費cpu運算的過程中,node的單進程模型就無優勢了,那么happypack就針對解決此類問題而生。

開啟happypack的線程池

happypack的處理思路是將原有的webpack對loader的執行過程從單一進程的形式擴展多進程模式,原本的流程保持不變,這樣可以在不修改原有配置的基礎上來完成對編譯過程的優化,具體配置如下:

 const HappyPack = require('happypack');const os = require('os')const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length}); // 啟動線程池});module:{rules: [{test: /\.(js|jsx)$/,// use: ['babel-loader?cacheDirectory'],use: 'happypack/loader?id=jsx',exclude: /^node_modules$/}]},plugins:[new HappyPack({id: 'jsx',cache: true,threadPool: HappyThreadPool,loaders: ['babel-loader']})]

我們可以看到通過在loader中配置直接指向happypack提供的loader,對于文件實際匹配的處理 loader,則是通過配置在plugin屬性來傳遞說明,這里happypack提供的loader與plugin的銜接匹配,則是通過id=happybabel來完成。配置完成后,laoder的工作模式就轉變成了如下所示:

happypack在編譯過程中除了利用多進程的模式加速編譯,還同時開啟了cache計算,能充分利用緩存讀取構建文件,對構建的速度提升也是非常明顯的,經過測試,最終的構建速度提升如下:

優化前:

優化后:

關于happyoack的更多介紹可以查看:

happypack

happypack 原理解析

方案五、增強 uglifyPlugin

uglifyJS憑借基于node開發,壓縮比例高,使用方便等諸多優點已經成為了js壓縮工具中的首選,但是我們在webpack的構建中觀察發現,當webpack build進度走到80%前后時,會發生很長一段時間的停滯,經測試對比發現這一過程正是uglfiyJS在對我們的output中的bunlde部分進行壓縮耗時過長導致,針對這塊我們可以使用webpack-uglify-parallel來提升壓縮速度。

從插件源碼中可以看到,webpack-uglify-parallel的是實現原理是采用了多核并行壓縮的方式來提升我們的壓縮速度。

plugin.nextWorker().send({input: input,inputSourceMap: inputSourceMap,file: file,options: options
});plugin._queue_len++;if (!plugin._queue_len) {callback();
}               if (this.workers.length < this.maxWorkers) {var worker = fork(__dirname + '/lib/worker');worker.on('message', this.onWorkerMessage.bind(this));worker.on('error', this.onWorkerError.bind(this));this.workers.push(worker);
}this._next_worker++;
return this.workers[this._next_worker % this.maxWorkers];

使用配置也非常簡單,只需要將我們原來webpack中自帶的uglifyPlugin配置:

new webpack.optimize.UglifyJsPlugin({exclude:/\.min\.js$/mangle:true,compress: { warnings: false },output: { comments: false }
})
修改成如下代碼即可:const os = require('os');const UglifyJsParallelPlugin = require('webpack-uglify-parallel');new UglifyJsParallelPlugin({workers: os.cpus().length,mangle: true,compressor: {warnings: false,drop_console: true,drop_debugger: true}})

目前webpack官方也維護了一個支持多核壓縮的UglifyJs插件:uglifyjs-webpack-plugin,使用方式類似,優勢在于完全兼容webpack.optimize.UglifyJsPlugin中的配置,可以通過uglifyOptions寫入,因此也做為推薦使用,參考配置如下:

 const UglifyJsPlugin = require('uglifyjs-webpack-plugin');new UglifyJsPlugin({uglifyOptions: {ie8: false,ecma: 8,mangle: true,output: { comments: false },compress: { warnings: false }},sourceMap: false,cache: true,parallel: os.cpus().length * 2})

方案六、Tree-shaking & Scope Hoisting

wepback在2.X和3.X中從rolluo中借鑒了tree-shaking和Scope Hoisting,利用es6的module特性,利用AST對所有引用的模塊和方法做了靜態分析,從而能有效地剔除項目中的沒有引用到的方法,并將相關方法調用歸納到了獨立的webpack_module中,對打包構建的體積優化也較為明顯,但是前提是所有的模塊寫法必須使用ES6 Module進行實現,具體配置參考如下:

 // .babelrc: 通過配置減少沒有引用到的方法{"presets": [["env", {"targets": {"browsers": ["last 2 versions", "safari >= 7"]}}],// https://www.zhihu.com/question/41922432["es2015", {"modules": false}]  // tree-shaking]}// webpack.config: Scope Hoisting{plugins:[// https://zhuanlan.zhihu.com/p/27980441new webpack.optimize.ModuleConcatenationPlugin()]}

適用場景

在實際的開發過程中,可靈活地選擇適合自身業務場景的優化手段。

優化手段開發環境生產環境
CommonsChunk
externals
DllPlugin
Happypack
uglify-parallel

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/139990.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息