wepack——提高工程化(原理篇)
webpack 是我們前端工程師必須掌握的一項技能,我們的日常開發已經離不開這個配置。關於這方面的文章已經很多,但還是想把自己的學習過程總結記錄下來。
一共兩篇文章,分為原理篇和實踐篇,從 webpack 構建原理開始,然後基於這個原理之上,明確我們實際工程配置中可以去優化的方向。
- 構建原理篇,先幫助大家知道整體打包構建的流程,已經瞭解的可以略過這篇,直接看下一篇實踐篇。
- 構建優化實踐篇,主要從 2 個方向去優化
- 提升構建速度,也就是減少整個打包構建的時間,
- 優化構建輸出,也就是減小我們最終構建輸出的檔案體積。
1. webpack 究竟解決了什麼問題
- 模組化解決方案
在早前web前端只需要一個簡單的 html 頁面,插入幾條script標籤 去引用 js 檔案就可以滿足需求,隨著專案越來越複雜,要實現的功能越來越多,檔案也越來越多,全部都這麼引入已經不再現實,這時候前端模組化就出現了,從AMD、CMD 到現在的 ES6 模組化寫法,我們可以把程式碼拆成一個個 JS 檔案,通過 import 去關聯依賴檔案,最後再通過某個打包工具把這麼多 js 檔案按照依賴關係最終打包成一個或多個 js 檔案在html 頁面去引入。
所以 webpack首要要解決的問題是將多個模組化的 js檔案 按照依賴關係打包為一個或多個檔案,所以我們通常都會說他是一個模組化解決方案
-
處理資源轉換
隨著 ES6,ES7,ES8 的出現,還有 vue、react 等前端框架的出現,我們發現這些檔案瀏覽器是不能直接執行的,需要我們中間編譯轉換一下為瀏覽器可執行的檔案,所以這時候 webpack 要做的事情又多了一項,按照依賴打包的同時,還要對原始檔進行編譯轉換處理,也就是我們日常配置的 loader 處理。
-
tree-shaking以及程式碼壓縮
現在webpack已經支援了對檔案編譯轉換後再進行打包,滿足了我們的基本需求。這時候我們又開始對效能提出了要求,希望打包出的體積越小越好。比如有些檔案雖然整個引用了,但其實真正只用了其中部分程式碼,沒用到的部分希望可以被剔除掉。這種是通過剔除無效程式碼來減小總的打包體積,另外一種方式是通過程式碼壓縮,比如空格、較長的函式名都可以被壓縮。因此webpack支援了 tree-shaking和程式碼壓縮。
-
程式碼拆分(非同步載入 + 抽出第三方公用庫)
現在 webpack 打包結果是不是做到了極致了呢?不行,我們還是嫌棄最終打包出的檔案體積太大了。這時候懶載入(非同步載入)出現了,你只需要把進入首頁時所需要的所有資源打包為一個檔案輸出就行,這樣進入首頁我只需要載入該檔案就行,其他資原始檔等我真正執行的時候再去載入就可以。就這樣,webpack又支援了非同步載入檔案的拆包功能,這時候我們最終打包出的主檔案只是當前首頁需要的資源。
- 開發輔助工具的提供
我們對於打包的基本需求以及效能需求終於得到了滿足,又開始追求開發時的體驗了,開發越便捷越好,webpack 就提供了一系列的開發輔助功能,比如 devserver,HMR 等等什麼的幫助我們高效的開發。
現在我們回過頭總結下看,webpack幫我們做了好多事啊。
- 作為一個模組化解決方案,幫助我們將繁多的 JS 模組化檔案按照依賴關係打包 為一個或多個檔案輸出
- 支援針對檔案指定檔案進行編譯轉換後再打包
- 支援針對打包後的內容優化、壓縮處理
來減小總的檔案體積 - 支援非同步載入以及其他拆包方式
- 提供一系列開發輔助工具
現在大家有沒有好奇webpack究竟是怎麼去實現這麼多功能的?
2. webpack 構建原理
- 原理概述
webpack的構建從處理入口檔案開始著手,首先解析入口檔案,需要 經過 loader轉換編譯這時候就轉換編譯,轉換完了開始分析是否有依賴檔案,有依賴檔案就接著處理依賴檔案,流程和剛剛一致,需要編譯轉換就轉換,然後接著解析依賴檔案是否還有依賴檔案,有再接著處理。就這樣通過入口檔案以及依賴關係,webpack 可以獲取並處理所有的依賴檔案。然後再基於這些檔案做進一步的優化處理,比如 treeshaking 或者 程式碼壓縮,最後生成為我們需要的一個或多個js 檔案。先獻上總的構建原理圖,接下來按照每個模組去闡述。
2.1. 準備工作
首先是開始編譯前的準備工作,我們在專案工程裡會配置 webpack.config.js檔案,裡面是我們的一些自定義配置,
webpack 首先會將我們的配置檔案和它自己的預設配置做一個 merge,生成最終的一個配置檔案,其次會將這個最終配置檔案裡的所有外掛plugin在這個時候都註冊好,在這裡要提一下 webpack 的事件機制,他是基於一個 tapable庫做的事件流控制,在整個的編譯過程中暴露出各種hook,而我們寫的 plugin 也就是去註冊監聽了某個 hook,在這個 hook 觸發時,去執行我們的 plugin。(大家要看 webpack 的原始碼,一定要先去看下tapable這個庫的用法,否則看起來會很累,一頭霧水。)。 現在我們配置完成了,plugin也註冊了,終於可以開始工作了。
2.2. 處理入口檔案
前面說了它會從入口檔案開始處理,在我們日常的入口檔案配置中,我們有多個配置方式,可能會有單入口、多入口、甚至動態入口等多種形式。
// 入口檔案
module.exports = {
// 單入口
entry: {
main: './path/to/my/entry/file.js'
},
// 多入口
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
},
// 動態入口
entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))
};
在 webpack 的處理中多種入口最後都會轉化為同一方法去處理,單入口不用說,多入口我可以先遍歷,再去執行該方法,動態入口,我先執行函式再去處理,最終都會進入到 生成入口檔案 module 例項階段。
大家都說 webpack 中一切檔案都是 module,那 module 是什麼呢,其實他就是一個存了當前檔案所有資訊的一個物件而已,這個檔案包含了以下資訊。
module = {
type,
request,
userRequest,
rawRequest,
loaders,
resource,
matchResource,
parser,
generator,
resolveOptions
}
2.3 遞迴生成檔案module 例項
- resolve 階段
我們已經做好了各種入口檔案形式的相容處理了,現在開始真正處理檔案生成 module 例項。首先進入 resolve 階段,它使用了一個 enhanced-resolve 庫。它主要做了什麼呢?想一想我們要處理檔案,首先是不是要先知道檔案在哪裡?在入口檔案的配置中,我們只配了相對路徑,所以我們要先拿到該檔案的絕對路徑位置,拿到位置還不夠,如果這個檔案是 es6 編寫的,我是需要對其轉換的。那我怎麼知道這個檔案是否需要轉換呢,需要的話,又是需要通過哪些 loader 進行轉換呢?這是我們 resolve 階段要處理的問題,通過我們的 resolve 配置和 rules 配置去獲取到當前檔案的絕對路徑和需要經過哪些loader 進行處理,然後將這些資訊存到我們當前這個檔案對應的 module 例項裡面。
resolve: {
// 位於 src 資料夾下常用模組,建立別名,準確匹配
alias: {
xyz$: path.resolve(__dirname, 'path/to/file.js')
},
modules: ['node_modules'], // 查詢範圍的減小
extensions: ['.js', '.json'], // import 檔案未加檔案字尾是,webpack 根據 extensions 定義的檔案字尾進行依次查詢
mainFields: ['loader', 'main'], // 減少入口檔案的搜尋步驟,設定第三方模組的入口檔案位置
},
module: {
rules: [
{
test: /\.js$/, // 匹配的檔案
use: 'babel-loader',
// 在此檔案範圍內去查詢
include: [],
// 此檔案範圍內不去查詢
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
}
]
}
在解析對應要執行的 loaders 過程中,需要注意 loaders 組裝的順序,webpack 會優先處理 inline-loader。
import Styles from 'style-loader!css-loader?modules!./styles.css';
webpack 採用正則匹配的方式解析出要執行的 內聯loader。在解析完內聯 loader 後,根據配置的 rules,解析剩餘的 loaders,組裝得到最後的 loaders是一個陣列,內容按照[postLoader, inlineLoader, normalLoader, preLoader]先後順序組合。
注意:這裡提到了幾種不用型別的 loader,除了 inline-loader 前面介紹了,還有postLoader,preLoader是有 enforce 欄位指定的:
module: {
rules: [
{
enforce: 'pre',
test: /\.(js|vue)$/,
loader: require.resolve('eslint-loader'),
exclude: /node_modules/
},
]
}
enforce 可以取值’pre’和’post’,分別對應preLoader和postLoader,沒有設定此欄位的就是normalLoader。
- 執行 loader 階段
現在檔案基本資訊拿到了,發現需要經過 loader 進行處理,好,那我們下一階段就是去執行他的 loaders,在這裡提一點需要注意的地方,loader 的執行是倒序的,為什麼他是倒序的呢,是因為loader的執行分為 2 個階段,
- pitching 階段:執行 loader 上的 pitch 方法
- normal 階段:執行 loader 常規方法
下面給了一個demo,我們看看執行順序:
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};
我們定義了 3 個 loader,a-loader, b-loader, c-loader,它的執行順序是 a.pitch-> b.pitch-> c.pitch-> c-loader-> b-loader-> a-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 的執行過程中,一但出現了返回結果,後面的 loader 和 pitch 都不會執行。
比如說 a-loader的 pitch 執行返回的結果,那 b 和 c 的 pitch 和 loader 都不會執行,直接跳到 a-loader 的執行上。
在前面我們提到 loaders 的組裝順序是[postLoader, inlineLoader, normalLoader, preLoader],最終對應的執行順序如下:
- pitching 階段: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
- normal 階段: preLoader -> normalLoader -> inlineLoader -> postLoader
執行完 loader 後,也就是對檔案做了編譯轉換,使其變成了最終可以被瀏覽器執行的程式碼。
- parse 階段
這時候我們要開始處理依賴了,那我怎麼知道當前檔案有依賴呢?webpack 是採用將loader 執行過後的原始檔source轉換為AST 去分析依賴。這裡使用了acorn 庫,將source生成對應的 AST。生成的 AST 劃分為 3部分,ImportDeclaration、FunctionDeclaration和VariablesDeclaration,接下來遍歷 AST 去收集依賴。
找到 import 等關鍵字去達到依賴收集的目的。
- 遞迴處理依賴階段
基於我們解析到的依賴檔案,我們要開始遞迴處理依賴了,又回到了我們處理入口檔案的整個流程,去生成依賴檔案的 module 例項,再執行 對應loader。就這樣 webpack 遞迴處理了所有的依賴檔案並完成了所有檔案的轉換。
前面說了,我們最終是要把這些檔案打包為一個或者多個檔案輸出的,那接下來是不是要對這些檔案做一個整合和優化處理?
2.4 生成 chunk
- 生成 Module-graph
由於這個時候我們是已經拿到了所有檔案的 module 例項以及依賴關係,可以先建立基本的一個 module-graph 了,下面我給了一個 demo,a.js作為入口檔案,紅色的是非同步依賴,綠色的是同步依賴。
- 生成 Basic-chunk-graph
在講具體的拆包之前,先描述下 module 、chunk和 chunkgroup 之間的關係:
如圖所示,chunkGroup 包含多個 chunk,chunk 包含多個 module。webpack 會先劃分出 chunkGroup,然後再根據使用者自定義的拆包配置,從 chunkGroup 中拆出多個 chunk 最為最終的檔案輸出。
我們可以分析下這4 個 chunk-group, 裡面有的模組存在多次引用,比如 chunk-group2 只有個 d.js,chunk-group1裡面已經包含了 d.js,這時候 chunkgroup2 會被剔除,就這樣最後只剩 2 個 chunk-group。這時候集合我們的optimization 配置,來劃分最終的輸出 chunk 檔案。 我們已經對所有的 module 例項進行了劃分為一個個的 chunk,這時要遍歷 chunk做一些優化操作 基於前面已經優化的chunk,現在終於到了最後的生成打包檔案環節了。 webpack 把這些檔案按照內建的 template 渲染生成最終的打包檔案。 總結一下 webpack 的整個構建打包過程,首先通過依賴關係和 loader 配置獲取經過編譯轉換後的所有module 例項,然後再根據配置進行拆分為一個或多個chunk,最後按照內建的template 渲染出最終的檔案輸出。 作者:吳海元
現在要基於Module-graph進行一個分包操作,分包的依據是非同步依賴。首先入口檔案會作為一個chunk-group,在分析依賴的過程中解析到非同步依賴就回去劃分 chunk-group,可以看到最後劃分了 4 個chunk-group 。
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
2.6 優化
module.exports = {
//...
optimization: {
moduleIds: 'hashed'
}
};
2.5 生成檔案
3. 總結
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2667270/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- webpack ——提高工程化(實踐篇)Web
- wepack學習筆記筆記
- webpack 快速入門 系列 - 自定義 wepack 上Web
- WePack —— 助力企業漸進式 DevOps 轉型dev
- 前端工程化前端
- # python工程化Python
- go 工程化Go
- webpack系列之-原理篇Web
- 跳錶(SkipList)原理篇
- 機器學習——原理篇機器學習
- vue模板編譯(原理篇)Vue編譯
- GBDT 演算法:原理篇演算法
- 前端工程化概述前端
- 理解前端工程化前端
- DevOps的工程化dev
- Angularjs的工程化AngularJS
- API 工程化分享API
- 揭祕Flutter Hot Reload(原理篇)Flutter
- Hybrid App技術解析 -- 原理篇APP
- Hybrid App技術解析 — 原理篇APP
- 瀏覽器架構-原理篇瀏覽器架構
- vue工程化開發Vue
- 前端工程化(理解篇)前端
- JS工程化集錦JS
- Redux其實很簡單(原理篇)Redux
- Android 效能監控系列一(原理篇)Android
- GoldenGate for Java Adapter介紹一(原理篇)GoJavaAPT
- 前端工程化--構建工具前端
- Vue 工程化最佳實踐Vue
- Vue工程化最佳實踐Vue
- GraphQL java工程化實踐Java
- (三)Java工程化–Git起步JavaGit
- 前端工程化,你做了多少?前端
- 工程化篇-JS相容方案JS
- 前端工程化 Webpack基礎前端Web
- Flutter 工程化搭建(Android端)FlutterAndroid
- 前端工程化最佳實踐前端
- 什麼是前端工程化?前端