標籤:

webpack增量打包

一,webpack打包存在的問題

webpack的打包順序:

var path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].js"n }n};n

1,找到入口文件

2,根據入口文件,找出具有依賴關係的文件js/css

3,最後,把css/js全部打包成一個js包

好的,打包完成,打包了整個世界,那麼問題來了:

產品說:按鈕顏色不對,給我改成#ccc

技術:好的,這就改。

然後就有了如下流程:

1,找到了entry -> js -> componet -> button.less修改了一個色值

2,執行webpack打包

這時就暴露了問題:

1,明明只是修改了一個色值,卻要從入口開始重新打包

2,業務代碼明明沒有變化,卻也被牽連了

3,最後生成的js要全部推到線上,覆蓋掉線上原本沒問題的業務js,純粹是增加風險

二,思考解決方案

首先想到的是,既然只修改一個文件,那能不能重新打包一個文件呢?

1,單獨打包更新的文件?

這種方案,很快就被自我否定了。

因為:

1,從入口打包的文件,已經通過依賴關係,把老版本button.less打入了最終輸出的js文件

2,單獨打包button.less輸出了一個獨立的button.js,這個文件需要手動引入到html中,一旦這類文件製造的多了,根本無法維護

經過反覆思考,單獨打包每一個文件的想法,不符合webpack的設計初衷,從入口打包的流程是不能夠產生變化的

在這個問題上卡了真的很久.....很久

2,能否通過依賴關係,打包某一個入口?

由於我面臨的場景是多頁應用,所以存在多個入口,那麼既然如此,那麼能否通過依賴關係,找到需要更新的入口呢?

這種方案,也思索了很久,後來也被否定了。

因為:

1,webpack沒有適合輸出模塊依賴關係的插件,遍尋無果啊

2,通過webpack的stats分析指標,能夠輸出依賴關係,但數據量太大,如果不加過濾,目前項目輸出12W行json信息,還需要花力氣處理一遍這個信息才能拿到關係

3,如果一個組件被多個入口引用,那麼需要找到每一個引用的入口點,再重新打包每個被波及的入口

上面尤其是第三點,完全不符合我們想增量發布的目的,如果改了一個button組件,要重新打包二三十個入口,這完全沒有增量發布的意義

在這個問題上又糾結了很久......很久

三,合理的解決方案

經過前面兩個問題後,我發現思考的方向完全是錯誤的,總是妄想改變webpack的打包方式,簡直就是跟它的理念對著干。

既然不能改變webpack的打包方式,那麼我能否改變webpack的輸出結果呢?

其實webpack關於緩存方面的功能,提供了很多功能強大的插件,例如:

  • CommonsChunkPlugin可以用來在打包的時候提取公共js代碼
  • ExtractTextPlugin可以用來從js中提出css,將其輸出到一個獨立的文件

利用這兩個插件,我們能夠將我們打包的精度加以劃分,將公共引用的部分打包為一個單獨的文件

如果公共引用的部分變為了一個單獨的文件,再添加上hash進行緩存,當再次修改的時候只要更新hash,這樣我們不就能夠確定,究竟改動了哪個文件了嗎

既然如此,我們一步一步進行探索:

1,首先使用CommonsChunkPlugin,提取公共js

現在我們創建測試入口文件:

src/one.js:

import jquery from jquery;n console.log(one);n

src/two.js:

import jquery from jquery;n console.log(two);n

webpack.config.js

var path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].js"n }n};n

執行webpack

輸出了2個文件,大小都是271kb,這是因為one.js和two.js都引用了jquery,jquery打包了2次,分別打包到了兩個文件中

這樣顯然不是很友好,像jquery這種文件,顯然平時不會改動,還是緩存起來比較好,修改webpack.config.js

var webpack = require("webpack");nvar path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n ]n};n

現在我們添加了CommonsChunkPlugin插件,它的作用是提取公共js,再次執行webpack

可以看到one.js和two.js的大小已經不到1k了,而common則274k,可以看到jquery已經被打包到了common.js當中

2,為文件添加hash

var webpack = require("webpack");nvar path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[hash:6].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n ]n};nn上面修改了output的輸出內容[name].[hash].jsn

現在執行webpack:

可以看到打包的三個文件都有了hash,但需要主意,此時每個文件的hash都是一樣的

再次執行一遍webpack:

可以看到,兩次構建輸出的結果一致,這很好,因為沒有修改文件,自然不希望hash發生改變

那麼接下來,修改一下文件:one.js

import jquery from jquery;nconsole.log(修改one);n

悲劇了,所有文件全部修改了hash,查看輸出的結果:

可以發現只修改一個文件,卻修改了全部文件的hash,這個問題很嚴重,顯然不是我們想要的

3,使用chunkhash替代hash

webpack中關於緩存,提供了好幾種添加hash的方法,其中就有chunkhash

chunkhash簡單來說,就是根據模塊內容來添加hash,既然這樣的話,只要文件沒有改變,就不會生成新的hash

var webpack = require("webpack");nvar path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n ]n};n

如上圖,修改filename:[name].[chunkhash:8]/js

執行webpack

可以看到這一次生成的hash是4897....

但是輸出的每個文件的hash卻不是4897....

很好,接下來再執行一次webpack:

可以看到兩次輸出之間hash並沒有發生變化

現在,修改one.js,再執行webapck

import jquery from jquery;nconsole.log(使用chunkhash後修改one);n

可以看到two.js的hash沒有改變one.js的hash改變了,但common.js的hash竟然也改了...

4,提取manifest

前面用CommonsChunkPlugin提取代碼後,公共的代碼已經被抽離,但是他們之間肯定存在一個映射關係,例如

之所以commonjs的hash會變,是因為修改one.js生成了新的hash,而jquery又與one.js存在映射關係,映射關係會更新,也就是說common.js它要從新的one.js中提取了jquery

manifest就可以簡單理解為模塊映射關係的集合,而這個manifest將隨著這些被分離出來的代碼共同打包!!!

所以現在分離manifest

var webpack = require("webpack");nvar path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n new webpack.optimize.CommonsChunkPlugin({n name: manifest // 用於提取manifestn })n ]n};n

這裡主要是利用CommonsChunkPlugin的一個功能,通過默認的名字,來提取公共代碼,因為webpack打包的是有有一個默認模塊就是manifest,所以我們可以通過這個來實現

現在我們執行webpack:

可以看到,多輸出了一個manifest.js

接下來,再修改one.js

import jquery from jquery;nconsole.log(分離manifest後修改one);n

可以看到,現在只有one.js和manifest.js的hash發生了改變,common.js被成功緩存了

使用代碼對比工具,比較兩次manifest之間的區別,可以看到確實是映射的chunkid發生了改變

5,使用webpack-md5-hash插件

前面我們輸出了一個manifest.js,但這樣還需要單獨處理這個manifest.js,所以可以使用webpack的另一個插件webpack-md5-hash

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nmodule.exports = {n entry: {n one: "./src/one.js",n two: "./src/two.js"n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new WebpackMd5Hash(),n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n ]n};n

執行一次打包:

沒有manifest輸出,修改one.js

import jquery from jquery;nconsole.log(使用WebpackMd5Hash修改one);n

再次打包:

這一次僅有one.js的hash發生了改變

雖然webpack-md5-hash解決了我們的問題,但這也讓打包的模塊關係變成了黑盒,存在一定的未知風險,還需要仔細實踐評估是否有問題

6,打包修改頻率超級低的庫

前面已經抽離出來了公共代碼,但是還存在問題,假如這時候又需要引入lodash,那common的hash是否會改變?

修改one.js

import jquery from jquery;nimport lodash from lodash;nconsole.log(引入lodash修改one);n

修改two.js

import jquery from jquery;nimport lodash from lodash;nconsole.log(引入lodash修改two);n

這一次,所有文件的hash都發生了改變,不僅如此,而且更顯著的是common的體積增大了

這就意味者lodash也被打進了common當中,但這本身是一個錯誤的行為,lodash和jquery,平時根本不會對其進行修改,既然如此,那還需要優化,把他們單獨打包出去

現在修改webapack.config.js

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nmodule.exports = {n entry: {n two: "./src/two.js",n one: "./src/one.js",n common:[jquery,lodash]n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new WebpackMd5Hash(),n new webpack.optimize.CommonsChunkPlugin({n name: "common",n }),n ]n};n

這一次在入口處添加了一個common,common單獨指向了jquery和lodash,這一次我們執行打包

此時,輸出的內容沒有明顯變化,同樣是3個文件,大小也完全一致,hash也沒有問題

可以看到,common的大小是817k

如果這時,再應用了其他的包呢?例如引入react

修改one.js

import jquery from jquery;nimport lodash from lodash;nimport react from react;nconsole.log(引入react修改one);n

修改two.js

import jquery from jquery;nimport lodash from lodash;nimport react from react;nconsole.log(引入react修改one);n

執行webpack

問題來了,common的大小增加了,很顯然react被打包進去了,但如果我們此時,只想永久緩存jquery和lodash呢,這該怎麼辦?

修改webpack.config.js

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nmodule.exports = {n entry: {n two: "./src/two.js",n one: "./src/one.js",n common:[jquery,lodash]n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new WebpackMd5Hash(),n new webpack.optimize.CommonsChunkPlugin({n name: common,n minChunks:Infinityn })n ]n};n

這一次,添加了一句話minChunks:Infinity

minChunks屬性的可以設置為2,意思是引用次數為2的模塊就抽離出來,而Infinity則表示無限,無限就意味著不會有多餘的被打包進來

現在執行webpack打包

可以看到現在common又恢復了816k,當然react也沒有抽出來,還在兩個文件當中,接下來繼續抽離react

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nmodule.exports = {n entry: {n two: "./src/two.js",n one: "./src/one.js",n common:[jquery,lodash],n react:[react,react-redux]n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: [react,common], // 用於提取manifestn minChunks:Infinityn }),n new WebpackMd5Hash(),n ]n};n

通過上面的構建,我們已經將不會改動的類庫,單獨打包並維持住了hash。

7,引入HashedModuleIdsPlugin固定模塊id

前面看似完美,但如果我們現在改變一下入口的順序

entry: {n react:[react,react-redux], n two: "./src/two.js",n one: "./src/one.js",n common:[jquery,lodash], n}n

可以看到common和react公共庫的hash又變了,這是因為,模塊id是根據webpack的解析順序增量的,如果變換解析順序,那模塊id也會隨之改變。

所以就需要HashedModuleIdsPlugin了,它是根據模塊相對路徑生成模塊標識,如果模塊沒有改變,那模塊標識也不會改變

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nmodule.exports = {n entry: {n common:[jquery,lodash], n react:[react,react-redux], n two: "./src/two.js",n one: "./src/one.js",n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n plugins:[n new webpack.optimize.CommonsChunkPlugin({n name: [react,common], // 用於提取manifestn minChunks:Infinityn }),n new webpack.HashedModuleIdsPlugin(),n new WebpackMd5Hash(),n ]n};n

現在打包後,模塊的標識不再是id了,而是一個四位的編碼了,這樣就可以固定住ip地址了。

8,使用extract-text-webpack-plugin提取css文件

在src下創建

one.css:

body{n color:blue;n}n

two.css

h1{n font-size:24px;n}n

修改one.js和two.js引入css

import jquery from jquery;nimport lodash from lodash;nimport react from react;nimport ./one.cssnconsole.log(引入css修改one);n

修改webpack.config.js

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nvar ExtractTextPlugin = require("extract-text-webpack-plugin");nmodule.exports = {n entry: {n common: [jquery, lodash],n react: [react, react-redux],n two: "./src/two.js",n one: "./src/one.js",n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n module: {n rules: [n {n test: /.css$/,n use: ExtractTextPlugin.extract({n fallback: "style-loader",n use: "css-loader"n })n }n ]n },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n name: [react, common], // 用於提取manifestn minChunks: Infinityn }),n new ExtractTextPlugin("[name].[chunkhash:8].css"),n new webpack.HashedModuleIdsPlugin(),n new WebpackMd5Hash()n ]n};n

執行webpack:

可以看到,成功輸出了js和css,但是有點疑問的是,one.css和one.js的hash是一樣的,這樣的話,如果我們改變one.css呢?

修改one.css,再次打包:

發現css的hash沒有任何變化。

接著再修改one.js,再次打包:

這一次one.js和one.css的hash同時改變了。

9,使用contenthash提取固定css的hash

  • When using the ExtractTextWebpackPlugin, use [contenthash] to obtain a hash of the extracted file (neither [hash] nor [chunkhash] work).

webpack output文檔種有寫,當提取css後,用contenthash添加hash

var webpack = require("webpack");nvar WebpackMd5Hash = require(webpack-md5-hash);nvar path = require(path);nvar ExtractTextPlugin = require("extract-text-webpack-plugin");nmodule.exports = {n entry: {n common: [jquery, lodash],n react: [react, react-redux],n two: "./src/two.js",n one: "./src/one.js",n },n output: {n path: path.resolve(__dirname, dist),n filename: "[name].[chunkhash:8].js"n },n module: {n rules: [n {n test: /.css$/,n use: ExtractTextPlugin.extract({n fallback: "style-loader",n use: "css-loader"n })n }n ]n },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n name: [react, common], // 用於提取manifestn minChunks: Infinityn }),n new ExtractTextPlugin("[name].[contenthash:8].css"),n new webpack.HashedModuleIdsPlugin(),n new WebpackMd5Hash()n ]n};n

這一次,只是修改了輸出的hash,conenthash代表的是文本文件內容的hash值,也就是只有style文件的hash值。

執行webpack:

one.js和one.css的hash變的不一樣了

接下來,修改one.css

body{n color:white;n}n

再次執行webpack:

至此,只有one.css發生了變化,準備工作基本就到這裡了

四,優化多頁打包時間,穩定hash

1,約束入口

因為是多頁應用,是通過掃入口文件來進行的打包,規則為js文件為入口文件,jsx為引用的資源不被識別為入口

通過BundleAnalyzerPlugin插件分析,發現有部分組件被打包為了入口,梳理一遍後,重新打包,打包時間減少了2/3,當然這是在填以前的坑

生產打包時間是74578ms

此時壓縮和不壓縮的打包時間也是3倍的關係:

開發打包時間是24780ms

好的,圍繞這兩個時間,我們開始優化

2,使用UglifyjsWebpackPlugin開啟多線程打包

首先要做的其實是穩定hash,但因為生產環境的打包速度太慢,所以我們先優化打包速度,webpack默認提供的打包是單線程的

const UglifyJSPlugin = require(uglifyjs-webpack-plugin)nnmodule.exports = {n plugins: [n new UglifyJSPlugin({n parallel: truen })n ]n}n

這個插件是webpack3提供的,至於低版本webapck的話,需要謹慎處理,不過效果很明顯

現在生產打包時間是51690ms,比之前提速了1/3

3,使用HappyPack多線程加速loader

var HappyPack = require(happypack);nvar os = require(os);nvar happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });nn...nmodule: {n rules: [ {n test: /.js[x]?$/,n exclude: /(node_modules|bower_components)/,n loader: happypack/loader?id=happybabel,n include: path.join(__dirname, static/assets/js)n }n }nplugins: [n new HappyPack({n id: happybabel,n loaders: [babel-loader?cacheDirectory=true],n threadPool: happyThreadPool,n cache: true,n verbose: truen }),n

上面module的rules屬性中loader原本事babel-loader,現在將它變成了一個任務,其中有一個id,id對應的就是plugins中的happyPack實例

此時,我們開啟了babel-loader的多線程模式

現在生產打包時間是43855ms,比之前又提速了1/9,這只是babel-loader,我們還可以為其它的loader開啟

接著處理less,css,style等loader,這些結合可以一口氣搞定

module: {n rules: [{n test: require.resolve(zepto),n loader: exports-loader?window.Zepto!script-loadern }, {n test: /.js[x]?$/,n exclude: /(node_modules|bower_components)/,n loader: happypack/loader?id=happybabel,n include: path.join(__dirname, static/assets/js)n }, {n test: /.less$/,n use: extractTextPlugin.extract({n fallback: "style-loader",n // use: ["css-loader" + (ENV ? ?minimize : ), "less-loader", "postcss-loader"]n use: ["happypack/loader?id=postcss"]n })n }]n }nplugins: [n new HappyPack({n id: happybabel,n loaders: [babel-loader?cacheDirectory=true],n threadPool: happyThreadPool,n // cache: true,n verbose: truen }),n new HappyPack({n id: postcss,n loaders: ["css-loader" + (ENV ? ?minimize : ), "less-loader",postcss-loader],n threadPool: happyThreadPool,n // cache: true,n verbose: truen }),n

這樣,我們即處理了babel,同時也搞定了css,less,postcss這些loader

上圖happy[任務名],可以看到打包行為全都開啟了多線程,效果顯著

現在生產打包時間是35130ms,此時已經比第一此非優化的時候,提升了一倍的速度

4,使用dll拆分代碼

經過前面的過程,想必已經意識到了純靜態得庫和組件都需要與打包環節分離開,這就需要dll技術了

dll技術,其實就是將修改頻率低或基本不修改且引用次數多的內容,單獨打包

因為設計dll後,config文件的數量劇增,所以需要重新整理目錄結構

例如上圖,將每一個webpack拆分出去,把所有配置文件分離開,例webpack.dev.js:

var base = require(./webpack.base.js);nvar config = {n entry: require(./dev/entry.js),n output: require(./dev/output.js),n plugins: require(./dev/plugins.js),n devtool: eval-source-mapn}n//把配置文件暴露出去;nmodule.exports = Object.assign(base,config);n

ok,基礎拆分webpack完成後,我們創建一個webpack.dll.libs.js用於打包類庫

module.exports = {n libs: [n react,n react-dom,n react-motion,n react-redux,n redux,n axios,n prop-types,n classnames,n ]n}n

修改plugins插件:

var webpack = require(webpack);nvar dirVars = require(../common/dir.js);nvar path = require(path);nvar UglifyJsPlugin = require(uglifyjs-webpack-plugin);//多線程打包nvar getDefaultPlugins = require(../common/plugins.js).getDefaultPlugins;nvar AssetsPlugin = require(assets-webpack-plugin);//輸出映射表nnvar plugins =[n new webpack.DllPlugin({n path: dirVars.dllLibsManiFest,n }),n new UglifyJsPlugin({n parallel: true,n cache: truen }),n new AssetsPlugin({n filename: static/dll/libs-rev-manifest.jsonn }),n]nmodule.exports = plugins.concat(getDefaultPlugins())n

現在執行webpack

可以看到,只需要1s,就打包了所有的類庫,接下來,修改webpack.prod.js

在plugins中添加:

new webpack.DllReferencePlugin({n manifest: static/dll/libs-rev-manifest.jsonn}),n

此時當我們執行webpack.prod.js進行打包,當掃描到libs中的打包的內容時,就不會重複打包

4,開始繼續約束hash

前面已經徹底搞定了打包,但破壞性很大,所以需要系統的驗證hash是否存在問題

case1:js改變

修改一個業務代碼的js,添加一句注釋,再次打包

可以看到文件hash發生了改變,但很不幸,vendor也發生了改變

解決方案:添加webpack-md5-hash插件,使用之後,再次驗證,發現vendorjs的hash不再發生變化

case2:less改變

只有一個css的hash發生了變化,沒問題

case3:修改一個入口下自己封裝出去的公共方法

上面修改了一個入口內公共使用的tools插件,最終是入口的hash發生了改變,沒問題

case4:修改公共方法組件js

主要是多個入口都會引用的組件

測試,只有單獨打包出去的components的hash修改了

case5:修改公共方法組件less

只有一個hash發生了改變

case6:添加一個公共組件

只有components的hash發生了改變

未優化前打包時間180-200s

優化:

1,約束入口,嚴格明確入口文件篩選條件後n 生產打包:74578msn 開發打包:24780msn2,開啟多線程壓縮後n 生產打包:51690msn3,開啟多線程編譯n 生產打包:35130msn 開發打包:15031msn4,拆包n 分解了打包過程,類庫4s,組件4s,業務20s,總體30s左右n

最終,流程變得可控,打包實現了定製化,hash得到了保持。

轉自:webpack增量打包多頁應用 - 掘金


推薦閱讀:

create-react-boilerplate: 面向 React 技術棧的工程項目腳手架
重溫 Webpack, Babel 和 React
Webpack 3 的新功能:Scope Hoisting
【譯】webpack 小札: 充分利用 CommonsChunkPlugin()
webpack:從入門到真實項目配置

TAG:webpack |