一,webpack打包存在的問題
webpack的打包順序:
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].js"
}
};
複製程式碼
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';
console.log('one');
複製程式碼
src/two.js:
import jquery from 'jquery';
console.log('two');
複製程式碼
webpack.config.js
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].js"
}
};
複製程式碼
執行webpack
輸出了2個檔案,大小都是271kb,這是因為one.js和two.js都引用了jquery,jquery打包了2次,分別打包到了兩個檔案中
這樣顯然不是很友好,像jquery這種檔案,顯然平時不會改動,還是快取起來比較好,修改webpack.config.js
var webpack = require("webpack");
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), ]
};
複製程式碼
現在我們新增了CommonsChunkPlugin外掛,它的作用是提取公共js,再次執行webpack
可以看到one.js和two.js的大小已經不到1k了,而common則274k,可以看到jquery已經被打包到了common.js當中
2,為檔案新增hash
var webpack = require("webpack");
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[hash:6].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), ]
};
複製程式碼
上面修改了output的輸出內容[name].[hash].js
現在執行webpack:
可以看到打包的三個檔案都有了hash,但需要主意,此時每個檔案的hash都是一樣的
再次執行一遍webpack:
可以看到,兩次構建輸出的結果一致,這很好,因為沒有修改檔案,自然不希望hash發生改變
那麼接下來,修改一下檔案:one.js
import jquery from 'jquery';
console.log('修改one');
複製程式碼
悲劇了,所有檔案全部修改了hash,檢視輸出的結果:
可以發現只修改一個檔案,卻修改了全部檔案的hash,這個問題很嚴重,顯然不是我們想要的
3,使用chunkhash替代hash
webpack中關於快取,提供了好幾種新增hash的方法,其中就有chunkhash
chunkhash簡單來說,就是根據模組內容來新增hash,既然這樣的話,只要檔案沒有改變,就不會生成新的hash
var webpack = require("webpack");
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), ]
};
複製程式碼
如上圖,修改filename:[name].[chunkhash:8]/js
執行webpack
可以看到這一次生成的hash是4897….
但是輸出的每個檔案的hash卻不是4897….
很好,接下來再執行一次webpack:
可以看到兩次輸出之間hash並沒有發生變化
現在,修改one.js,再執行webapck
import jquery from 'jquery';
console.log('使用chunkhash後修改one');
複製程式碼
可以看到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");
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), new webpack.optimize.CommonsChunkPlugin({
name: 'manifest' // 用於提取manifest
}) ]
};
複製程式碼
這裡主要是利用CommonsChunkPlugin的一個功能,通過預設的名字,來提取公共程式碼,因為webpack打包的是有有一個預設模組就是manifest,所以我們可以通過這個來實現
現在我們執行webpack:
可以看到,多輸出了一個manifest.js
接下來,再修改one.js
import jquery from 'jquery';
console.log('分離manifest後修改one');
複製程式碼
可以看到,現在只有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");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
entry: {
one: "./src/one.js", two: "./src/two.js"
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new WebpackMd5Hash(), new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), ]
};
複製程式碼
執行一次打包:
沒有manifest輸出,修改one.js
import jquery from 'jquery';
console.log('使用WebpackMd5Hash修改one');
複製程式碼
再次打包:
這一次僅有one.js的hash發生了改變
雖然webpack-md5-hash解決了我們的問題,但這也讓打包的模組關係變成了黑盒,存在一定的未知風險,還需要仔細實踐評估是否有問題
6,打包修改頻率超級低的庫
前面已經抽離出來了公共程式碼,但是還存在問題,假如這時候又需要引入lodash,那common的hash是否會改變?
修改one.js
import jquery from 'jquery';
import lodash from 'lodash';
console.log('引入lodash修改one');
複製程式碼
修改two.js
import jquery from 'jquery';
import lodash from 'lodash';
console.log('引入lodash修改two');
複製程式碼
這一次,所有檔案的hash都發生了改變,不僅如此,而且更顯著的是common的體積增大了
這就意味者lodash也被打進了common當中,但這本身是一個錯誤的行為,lodash和jquery,平時根本不會對其進行修改,既然如此,那還需要優化,把他們單獨打包出去
現在修改webapack.config.js
var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
entry: {
two: "./src/two.js", one: "./src/one.js", common:['jquery','lodash']
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new WebpackMd5Hash(), new webpack.optimize.CommonsChunkPlugin({
name: "common",
}), ]
};
複製程式碼
這一次在入口處新增了一個common,common單獨指向了jquery和lodash,這一次我們執行打包
此時,輸出的內容沒有明顯變化,同樣是3個檔案,大小也完全一致,hash也沒有問題
可以看到,common的大小是817k
如果這時,再應用了其他的包呢?例如引入react
修改one.js
import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
console.log('引入react修改one');
複製程式碼
修改two.js
import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
console.log('引入react修改one');
複製程式碼
執行webpack
問題來了,common的大小增加了,很顯然react被打包進去了,但如果我們此時,只想永久快取jquery和lodash呢,這該怎麼辦?
修改webpack.config.js
var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
entry: {
two: "./src/two.js", one: "./src/one.js", common:['jquery','lodash']
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new WebpackMd5Hash(), new webpack.optimize.CommonsChunkPlugin({
name: 'common', minChunks:Infinity
}) ]
};
複製程式碼
這一次,新增了一句話minChunks:Infinity
minChunks屬性的可以設定為2,意思是引用次數為2的模組就抽離出來,而Infinity
則表示無限,無限就意味著不會有多餘的被打包進來
現在執行webpack打包
可以看到現在common又恢復了816k,當然react也沒有抽出來,還在兩個檔案當中,接下來繼續抽離react
var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
entry: {
two: "./src/two.js", one: "./src/one.js", common:['jquery','lodash'], react:['react','react-redux']
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: ['react','common'], // 用於提取manifest minChunks:Infinity
}), new WebpackMd5Hash(), ]
};
複製程式碼
通過上面的構建,我們已經將不會改動的類庫,單獨打包並維持住了hash。
7,引入HashedModuleIdsPlugin固定模組id
前面看似完美,但如果我們現在改變一下入口的順序
entry: {
react:['react','react-redux'], two: "./src/two.js", one: "./src/one.js", common:['jquery','lodash'],
}複製程式碼
可以看到common和react公共庫的hash又變了,這是因為,模組id是根據webpack的解析順序增量的,如果變換解析順序,那模組id也會隨之改變。
所以就需要HashedModuleIdsPlugin了,它是根據模組相對路徑生成模組標識,如果模組沒有改變,那模組標識也不會改變
var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
entry: {
common:['jquery','lodash'], react:['react','react-redux'], two: "./src/two.js", one: "./src/one.js",
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, plugins:[ new webpack.optimize.CommonsChunkPlugin({
name: ['react','common'], // 用於提取manifest minChunks:Infinity
}), new webpack.HashedModuleIdsPlugin(), new WebpackMd5Hash(), ]
};
複製程式碼
現在打包後,模組的標識不再是id了,而是一個四位的編碼了,這樣就可以固定住ip地址了。
8,使用extract-text-webpack-plugin提取css檔案
在src下建立one.css:
body{
color:blue;
}複製程式碼
two.css
h1{
font-size:24px;
}複製程式碼
修改one.js和two.js引入css
import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
import './one.css'console.log('引入css修改one');
複製程式碼
修改webpack.config.js
var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
common: ['jquery', 'lodash'], react: ['react', 'react-redux'], two: "./src/two.js", one: "./src/one.js",
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, module: {
rules: [ {
test: /\.css$/, use: ExtractTextPlugin.extract({
fallback: "style-loader", use: "css-loader"
})
} ]
}, plugins: [ new webpack.optimize.CommonsChunkPlugin({
name: ['react', 'common'], // 用於提取manifest minChunks: Infinity
}), new ExtractTextPlugin("[name].[chunkhash:8].css"), new webpack.HashedModuleIdsPlugin(), new WebpackMd5Hash() ]
};
複製程式碼
執行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");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
common: ['jquery', 'lodash'], react: ['react', 'react-redux'], two: "./src/two.js", one: "./src/one.js",
}, output: {
path: path.resolve(__dirname, 'dist'), filename: "[name].[chunkhash:8].js"
}, module: {
rules: [ {
test: /\.css$/, use: ExtractTextPlugin.extract({
fallback: "style-loader", use: "css-loader"
})
} ]
}, plugins: [ new webpack.optimize.CommonsChunkPlugin({
name: ['react', 'common'], // 用於提取manifest minChunks: Infinity
}), new ExtractTextPlugin("[name].[contenthash:8].css"), new webpack.HashedModuleIdsPlugin(), new WebpackMd5Hash() ]
};
複製程式碼
這一次,只是修改了輸出的hash,conenthash代表的是文字檔案內容的hash值,也就是隻有style檔案的hash值。
執行webpack:
one.js和one.css的hash變的不一樣了
接下來,修改one.css
body{
color:white;
}複製程式碼
再次執行webpack:
至此,只有one.css發生了變化,準備工作基本就到這裡了
四,優化多頁打包時間,穩定hash
1,約束入口
因為是多頁應用,是通過掃入口檔案來進行的打包,規則為js檔案為入口檔案,jsx為引用的資源不被識別為入口
通過BundleAnalyzerPlugin外掛分析,發現有部分元件被打包為了入口,梳理一遍後,重新打包,打包時間減少了2/3,當然這是在填以前的坑
生產打包時間是74578ms
此時壓縮和不壓縮的打包時間也是3倍的關係:
開發打包時間是24780ms
好的,圍繞這兩個時間,我們開始優化
2,使用UglifyjsWebpackPlugin開啟多執行緒打包
首先要做的其實是穩定hash,但因為生產環境的打包速度太慢,所以我們先優化打包速度,webpack預設提供的打包是單執行緒的
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')module.exports = {
plugins: [ new UglifyJSPlugin({
parallel: true
}) ]
}複製程式碼
這個外掛是webpack3提供的,至於低版本webapck的話,需要謹慎處理,不過效果很明顯
現在生產打包時間是51690ms
,比之前提速了1/3
3,使用HappyPack多執行緒加速loader
var HappyPack = require('happypack');
var os = require('os');
var happyThreadPool = HappyPack.ThreadPool({
size: os.cpus().length
});
...module: {
rules: [ {
test: /\.js[x]?$/, exclude: /(node_modules|bower_components)/, loader: 'happypack/loader?id=happybabel', include: path.join(__dirname, 'static/assets/js')
}
}plugins: [ new HappyPack({
id: 'happybabel', loaders: ['babel-loader?cacheDirectory=true'], threadPool: happyThreadPool, cache: true, verbose: true
}),複製程式碼
上面module的rules屬性中loader原本事babel-loader,現在將它變成了一個任務,其中有一個id,id對應的就是plugins中的happyPack例項
此時,我們開啟了babel-loader的多執行緒模式
現在生產打包時間是43855ms
,比之前又提速了1/9,這只是babel-loader,我們還可以為其它的loader開啟
接著處理less,css,style等loader,這些結合可以一口氣搞定
module: {
rules: [{
test: require.resolve('zepto'), loader: 'exports-loader?window.Zepto!script-loader'
}, {
test: /\.js[x]?$/, exclude: /(node_modules|bower_components)/, loader: 'happypack/loader?id=happybabel', include: path.join(__dirname, 'static/assets/js')
}, {
test: /\.less$/, use: extractTextPlugin.extract({
fallback: "style-loader", // use: ["css-loader" + (ENV ? '?minimize' : ''), "less-loader", "postcss-loader"] use: ["happypack/loader?id=postcss"]
})
}]
}plugins: [ new HappyPack({
id: 'happybabel', loaders: ['babel-loader?cacheDirectory=true'], threadPool: happyThreadPool, // cache: true, verbose: true
}), new HappyPack({
id: 'postcss', loaders: ["css-loader" + (ENV ? '?minimize' : ''), "less-loader",'postcss-loader'], threadPool: happyThreadPool, // cache: true, verbose: true
}),複製程式碼
這樣,我們即處理了babel,同時也搞定了css,less,postcss這些loader
上圖happy[任務名],可以看到打包行為全都開啟了多執行緒,效果顯著
現在生產打包時間是35130ms
,此時已經比第一此非優化的時候,提升了一倍的速度
4,使用dll拆分程式碼
經過前面的過程,想必已經意識到了純靜態得庫和元件都需要與打包環節分離開,這就需要dll技術了
dll技術,其實就是將修改頻率低或基本不修改且引用次數多的內容,單獨打包
因為設計dll後,config檔案的數量劇增,所以需要重新整理目錄結構
例如上圖,將每一個webpack拆分出去,把所有配置檔案分離開,例webpack.dev.js:
var base = require('./webpack.base.js');
var config = {
entry: require('./dev/entry.js'), output: require('./dev/output.js'), plugins: require('./dev/plugins.js'), devtool: 'eval-source-map'
}//把配置檔案暴露出去;
module.exports = Object.assign(base,config);
複製程式碼
ok,基礎拆分webpack完成後,我們建立一個webpack.dll.libs.js用於打包類庫
module.exports = {
libs: [ 'react', 'react-dom', 'react-motion', 'react-redux', 'redux', 'axios', 'prop-types', 'classnames', ]
}複製程式碼
修改plugins外掛:
var webpack = require('webpack');
var dirVars = require('../common/dir.js');
var path = require('path');
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
//多執行緒打包var getDefaultPlugins = require('../common/plugins.js').getDefaultPlugins;
var AssetsPlugin = require('assets-webpack-plugin');
//輸出對映表var plugins =[ new webpack.DllPlugin({
path: dirVars.dllLibsManiFest,
}), new UglifyJsPlugin({
parallel: true, cache: true
}), new AssetsPlugin({
filename: 'static/dll/libs-rev-manifest.json'
}),]module.exports = plugins.concat(getDefaultPlugins())複製程式碼
現在執行webpack
可以看到,只需要1s,就打包了所有的類庫,接下來,修改webpack.prod.js
在plugins中新增:
new webpack.DllReferencePlugin({
manifest: 'static/dll/libs-rev-manifest.json'
}),複製程式碼
此時當我們執行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,約束入口,嚴格明確入口檔案篩選條件後 生產打包:74578ms 開發打包:24780ms2,開啟多執行緒壓縮後 生產打包:51690ms3,開啟多執行緒編譯 生產打包:35130ms 開發打包:15031ms4,拆包 分解了打包過程,類庫4s,元件4s,業務20s,總體30s左右複製程式碼
最終,流程變得可控,打包實現了定製化,hash得到了保持。