做者:劉華css
隨着前端構架工具的不斷髮展,提供了不少提升咱們的開發體驗和開發效率的能力,同時構建已經成爲前端技術棧中常見的技術。html
webpack 也是衆多構建工具中嶄露頭角一員,早期的 webpack 配置複雜難懂,隨着其發展,相關配置也不斷簡化,性能也不斷提升,可是對於深刻使用的開發人員,一般它的默認配置並不適用於業務開發,須要針對本身業務調整適配。前端
你對 webpack 瞭解多少?如何針對業務集成最佳配置?如何優化開發體驗?如何開足馬力,實現極速的 webpack 的構建性能 🚀?又會有哪些坑 💣?本文帶你解答這些問題 🔭。node
本文涉及到的全部代碼片斷的完整代碼請參考a8k倉庫react
對構建有所瞭解的,可直接略過本節webpack
此處不會深刻介紹相關配置,更多的詳細說明與配置參見官方文檔,稍做介紹關鍵配置項鋪墊後面內容。git
webpack 查找依賴的入口文件配置,入口文件能夠有多個。es6
單頁面應用入口配置 一般作法配置:vendor.js 第三方依賴庫,polyfill.js 特性填充庫,index.js 單頁面應用入口文件github
// 導出配置
module.exports = {
entry: {
vendor: './src/vendor.js',
polyfill: './src/polyfill.js',
index: './src/index.js',
},
};
複製代碼
多頁面應用入口配置 和單頁面應用相似,但不一樣頁面會不一樣有入口文件,這種狀況高效的作法就不是直接寫死在 entry 裏面了,而是經過生成 webpack.config 時,掃描指定目錄肯定每一個頁面的入口文件以及全部的頁面。web
下面舉個例子
假定你的頁面都放置在 src/pages 目錄下面,而且你的每一個頁面單獨一個目錄,而且其中有 index.html 和 index.jsx
const path = require('path');
const fs = require('fs');
// 處理公共entry
const commonEntry = ['./src/vendor.js', './src/polyfill.js'];
// 頁面目錄
const PAGES_DIR = './src/pages/';
const entry = {};
// 遍歷頁面目錄
const getPages = () => {
return fs.readdirSync(PAGES_DIR).filter(item => {
let filepath = path.join(PAGES_DIR, item, 'index.js');
if (!fs.existsSync(filepath)) {
filepath = `${filepath}x`; // jsx
}
if (!fs.existsSync(filepath)) {
return false;
}
return true;
});
};
getPages(options).forEach(file => {
const name = path.basename(file);
// 加入頁面須要的公共入口
entry[name] = [...commonEntry, `${PAGES_DIR}/${file}/index`];
});
// 導出配置
module.exports = {
entry,
};
複製代碼
入口 boundle 如何插入對應的 html 中?
咱們一般須要這個插件HtmlWebpackPlugin
自動處理,具體代碼以下:
const plugins = [];
if (mode === 'single') {
// 單頁面只須要一次HtmlWebpackPlugin
plugins.push(
new HtmlWebpackPlugin({
minify: false,
filename: 'index.html',
template: './src/index.html',
})
);
}
if (mode === 'multi') {
// 多頁面遍歷目錄,使用目錄下面的html文件
// 不一樣頁面的配置不一樣,每一個頁面都單獨配置一個html
// 全部頁面的公共部分能夠抽離後,經過模版引擎編譯處理
// 具體的方式後面部分loader中提到
const files = getPages(options);
files.forEach(file => {
const name = path.basename(file);
file = `${PAGES_DIR}/${file}/index.html`;
// 添加runtime腳本,和頁面入口腳本
const chunks = [`runtime~${name}`, name];
plugins.push(
new HtmlWebpackPlugin({
minify: false,
filename: `${name}.html`,
template: file,
chunks,
})
);
});
}
// 導出配置
module.exports = {
plugins,
};
複製代碼
該項配置輸出的 bundle 的相關信息,比較經常使用的配置以下:
{
output:{
// name是你配置的entry中key名稱,或者優化後chunk的名稱
// hash是表示bundle文件名添加文件內容hash值,以便於實現瀏覽器持久化緩存支持
filename: '[name].[hash].js',
// 在script標籤上添加crossOrigin,以便於支持跨域腳本的錯誤堆棧捕獲
crossOriginLoading:'anonymous',
//靜態資源路徑,指的是輸出到html中的資源路徑前綴
publicPath:'https://7.ur.cn/fudao/pc/',
path: './dist/',//文件輸出路徑
}
}
複製代碼
該項配置主要用於解析模塊依賴的自定義項, 比較常規的配置項以下,modules用於加速絕對路徑查找效率,alias能夠用戶自定義模塊查找路徑。
resolve: {
modules: [
path.resolve(__dirname, 'src'),
path.resolve(__dirname,'node_modules'),
],
alias: {
components: path.resolve(__dirname, '/src/components'),
},
}
複製代碼
擴展 若是你使用了絕對路徑後,可能就發現vscode智能代碼導航就失效了,別慌!請在想目錄下面配置jsconfig.json
文件解決這個問題,配置和上面對應:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"],
"components/*": ["./src/components/*"],
"assets/*": ["./src/assets/*"],
"pages/*": ["./src/pages/*"]
}
},
"include": ["./src/**/*"]
}
複製代碼
這樣,你就能夠愉快的使用vscode的智能代碼提示和導航了!
該項主要配置就是rules了,rules中配置對於不一樣資源的處理器,是其核心之一,這裏簡單添加一個示例代碼
module: {
// 這些庫都是不依賴其它庫的庫 不須要解析他們能夠加快編譯速度
// 一般能夠將那些大型的庫且已經編譯好的庫排除,減小webpack對其解析耗時
noParse: /node_modules\/(moment|chart\.js)/,
rules: [
{
test: /\.jsx?$/,
use: resolve('babel-loader'),
// 須要被這個loader處理的資源
include: [
path.resolve(projectDir, 'src'),
path.resolve(projectDir, 'node_modules/@tencent'),
].filter(Boolean),
// 忽略哪些壓縮的文件
exclude: [/(.|_)min\.js$/],
}
]
複製代碼
該頂配項中最重要最經常使用的是:splitChunks
,minimizer
minimizer
能夠本身配置輸出的文件壓縮插件,js壓縮咱們可使用webpack集成的uglifyjs,也可使用Terser,Terser支持es6代碼的壓縮,同時支持多進程壓縮;css壓縮咱們可使用optimize-css-assets-webpack-plugin
壓縮,它使用cssnano做爲處理引擎,幫助咱們去除重複樣式.
splitChunks
是webpack4.x推出的重磅功能,優化的公共chunk提取策略,更高效的提取公共模塊,在後面性能優化中會詳細說明其使用方法。
plugin 能夠介入整個構建過程任何階段。例如:報告構建耗時、修改輸出代碼支持主域重試、添加構建進度報告、代碼壓縮、資源替換等不少能力都在這裏實現。
plugin不展開討論,由於插件太多了。對於項目須要本身實現插件的,須要注意一點,當你使用插件對輸出結果處理時,應當在文件輸出到磁盤以前處理,咱們之前的構建中主域重試插件就踩了這個坈,致使最終構建的代碼出現錯誤,緣由是該插件直接修改磁盤上面的文件,兩次構建同時啓動,結束時兩次構建的插件都修改了磁盤上同一個文件,最終致使bug,而且致使咱們須要強行清理髮布環境代碼才恢復正常發佈。
溫馨的開發體驗,有助於提升咱們的開發效率,優化開發體驗也相當重要
自從webpack推出熱刷新後,前端開發者在開環境下體驗大幅提升。 沒有熱刷新能力,咱們修改一個組件後
加入熱構建後:
主要看一下咱們業務基於React技術棧,如何在構建中接入熱刷新。
不管什麼技術棧,都須要在dev模式下加上webpack.HotModuleReplacementPlugin
插件
在全部entry中插入require.resolve('../utils/webpackHotDevClient')
,webpackHotDevClient
這份代碼是由react官方的create-react-app提供的
在webpack-dev-server
模塊的啓動參數中添加hot:true
在你須要熱加載的js文件中添加如下代碼(這段代碼在構建生產包會自動刪除):
if (process.env.NODE_ENV==='development' && module.hot) {
module.hot.accept()
}
複製代碼
注:也可使用react-hot-loader來實現,具體參考官方文檔
輔導的H5/PC項目都有部分頁面支持直出,之前直出調試方式是以下流程所示:
這種調試流程太長,每一次修改都須要從新構建靜態資源,並重啓node服務,很是耗時,其次直出模式下,非直出的頁面將沒法正常訪問,整個流程沒法走通。
所以, 提出了新的解決方案, 採用 webpack watch+nodemon
結合的模式實現對SSR熱調試的支持。node 服務須要的html/js經過webpack插件動態輸出,當nodemon檢測到變化後將自動重啓,html文件中的靜態資源所有替換爲dev模式下的資源,並保持socket鏈接自動更新頁面。
實現熱調試後,調試流程大幅縮短,和普通非直出模式調試體驗保持一致。在a8k
中經過k dev -s
命令便可開啓ssr調試模式。下面是SSR熱調試的流程圖:
問題:
給style-loader
開啓sourceMap後, sourceMap是內聯在style文件中的,須要經過link導入,這種方式是經過JavaScript生成blob後丟個link標籤解析。以後咱們能夠在dev工具中直接看到每一個樣式所在的源文件位置,方便快速的調試樣式。但也一樣引發一個問題FOUC(頁面加載後閃爍),可參見這個ssue
解決方法:
添加singleton: true
參數可解決這個問題,可是sourceMap就不能定位到源文件了,而是合併後的文件中的位置,兩者不可兼得。因此在a8k
工具中提供了可選項,默認開啓singleton:true
,經過k dev -c
可開啓cssSourceMap映射
輔導大多數項目node_modules依賴數量都很是驚人,輔導PC項目剔除構建相關依賴後,依賴包都1883個,依賴包的安裝耗時也就大幅增長,所以減小依賴包安裝耗時,對構建總體提高很是重要,方法那就是緩存。
JB系統編譯 每次編譯都會啓動一個新的目錄,這致使項目依賴的衆多node_modules沒法緩存,每次編譯從新安裝耗時很是長,針對JB的編譯,我開發了@tencent/im-build模塊自動緩存項目依賴的node_modules,大幅提高了編譯性能。
OCI編譯系統 OCI中不須要額外的插件支持,該系統自己已經能夠經過配置實現部分目錄緩存,二次利用的能力,使用方法以下:
在項目根目錄添加.orange-cache.cache
文件,並添加你須要緩存的目錄
/node_modules
/fudao_qq_com_pc_imt
複製代碼
修改.orange-ci.yml
配置,添加緩存配置文件路徑
push:
- cacheFrom: .orange-ci.cache
#其它配置省略
複製代碼
優化前
優化後
中間結果緩存優化一樣能大幅提高構建性能,對模塊的編譯自己就是CPU密集型任務。一般來講每次構建並不是全部模塊都須要被從新處理,能夠只考慮處理那些文件內容有變化的模塊,那麼文件內容沒有變化的模塊就能夠從緩存中獲取,一般經過文件內容hash值做爲緩存文件的名稱,這就是「熱構建」。
在webpack中,可以被緩存的內容有:loader處理結果、plugin處理結果、輸出文件結果。下面詳細說明不一樣資源不一樣階段的緩存方式。
test: /\.jsx?$/,
use: [
{
loader: resolve('babel-loader'),
options: {
babelrc: false,
// cacheDirectory 緩存babel編譯結果加快從新編譯速度
cacheDirectory: path.resolve(options.cache, 'babel-loader'),
presets: [[require('babel-preset-imt'), { isSSR }]],
},
},
],
複製代碼
test: /\.(js|mjs|jsx)$/,
enforce: 'pre',
use: [
{
options: {
cache: path.resolve(options.cache, 'eslint-loader'),
},
loader: require.resolve('eslint-loader'),
},
],
複製代碼
eslint-loader一般只須要在開發模式下開啓,方便及時的提醒開發者,存在eslint錯誤,及時修復
css-loader/sass-loader/postcss-loader自己並無提供緩存機制,這裏須要用到cache-loader輔助咱們實現對css/scss的構建結果緩存,具體使用方式以下:
{
loader: resolve('cache-loader'),
options: { cacheDirectory: path.join(cache, 'cache-loader-css') },
},
{
loader: resolve('css-loader'),
options: {
importLoaders: 2,
sourceMap,
},
},
...因爲篇幅緣由,這裏不展現其它更多loader
複製代碼
只須要將該loader添加到這個loader的最頭部便可,該loader不只能夠對於css緩存
JS代碼壓縮咱們採用了TerserPlugin
插件,具體配置以下:
{
// 設置緩存目錄
cache: path.resolve(cache, 'terser-webpack-plugin'),
parallel: true,// 開啓多進程壓縮
sourceMap,
terserOptions: {
compress: {
// 刪除全部的 `console` 語句
drop_console: true,
},
},
}
複製代碼
上面在不一樣的plugin和loader上面配置了cache目錄,對於CI系統來講你須要將cache目錄路徑固定,以便於重複使用緩存內容,使用方式:JB就配置/tmp/xxx
目錄,OCI系統可配置在項目目錄。
⚠️注意:因爲使用了緩存,當你修改你的編譯配置後,須要當即清理緩存結果,最好的作法是在構建工具中自動檢測相關配置是否有變化,自動清理緩存
resolve: {
//加快搜索速度
modules: [
'node_modules',
path.resolve(projectDir, 'src'),
path.resolve(projectDir, 'node_modules')
],
},
複製代碼
module: {
// 這些庫都是不依賴其它庫的庫 不須要解析他們能夠加快編譯速度
noParse: /node_modules\/(moment|chart\.js)/,
}
複製代碼
// 指處理指定目錄的文件
include: [
path.resolve(projectDir, 'src'),
path.resolve(projectDir, 'node_modules/@tencent'),
].filter(Boolean),
// 忽略哪些壓縮的文件
exclude: [/(.|_)min\.js$/],
複製代碼
咱們在使用lodash庫是,一般只會用到其中很是少的function,可是像下面這段代碼,將會致使lodash所有被打入最終的bundle中。
import _ from 'lodash'
_.difference(1, 2)
複製代碼
這種狀況幸虧有插件能夠幫咱們優化,經過lodashPlugin便可自動處理lodash的按需引用
使用方法以下:
const LodashPlugin = require('lodash-webpack-plugin');
plugins:[
// 支持lodash包 按需引用
new LodashPlugin(),
]
複製代碼
加入這個plugin後,上面的代碼自動處理爲以下代碼:
import difference from 'lodash/difference';
difference([1, 2], [1, 3]);
複製代碼
注意:導入代碼方式必須使用import,不能使用require
經過webpack-node-externals
插件實現這一點,具體使用方法以下:
const nodeExternals = require('webpack-node-externals');
module.export={
// 省略其它配置
externals: [
nodeExternals({
// 注意若是存在src下面其餘目錄的絕對引用,都須要添加到這裏
whitelist: [
/^components/, /^assets/, /^pages/, /^@tencent/, /\.(scss|css)$/
],
}),
],
// 省略其它配置
}
複製代碼
在webpack4以前,咱們處理公共模塊的方式都是使用CommonsChunkPlugin
,而後該插件的讓開發這配置繁瑣,而且公共代碼的抽離,不夠完全和細緻,所以新的splitChunks
改進了這些能力。使用的正確姿式以下:
splitChunks: {
chunks: 'all',
minSize: 10000, // 提升緩存利用率,這須要在http2/spdy
maxSize: 0,//沒有限制
minChunks: 3,// 共享最少的chunk數,使用次數超過這個值纔會被提取
maxAsyncRequests: 5,//最多的異步chunk數
maxInitialRequests: 5,// 最多的同步chunks數
automaticNameDelimiter: '~',// 多頁面共用chunk命名分隔符
name: true,
cacheGroups: {// 聲明的公共chunk
vendor: {
// 過濾須要打入的模塊
test: module => {
if (module.resource) {
const include = [/[\\/]node_modules[\\/]/].every(reg => {
return reg.test(module.resource);
});
const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {
return reg.test(module.resource);
});
return include && !exclude;
}
return false;
},
name: 'vendor',
priority: 50,// 肯定模塊打入的優先級
reuseExistingChunk: true,// 使用複用已經存在的模塊
},
react: {
test({ resource }) {
return /[\\/]node_modules[\\/](react|redux)/.test(resource);
},
name: 'react',
priority: 20,
reuseExistingChunk: true,
},
antd: {
test: /[\\/]node_modules[\\/]antd/,
name: 'antd',
priority: 15,
reuseExistingChunk: true,
},
},
},
複製代碼
簡要解釋上面這段配置
作了這麼多優化,下面是基於模塊超過2.5k的輔導h5項目,構建耗時對比,感覺一下效果
優化前:熱構建須要40s
優化後:只須要20s
構建的配置和優化的工做並不小,將最佳實踐收斂和集成爲獨立的模塊,在不一樣項目中複用,能夠大幅減小構建維護工做,以及後續升級優化工做難度。
IMWeb團隊的項目目前也獨立維護一套基於React技術棧的構建最佳實踐工具a8k
,在全部的項目中不會在看到複雜多樣的webpack配置,以及各類花樣的前置、後置腳本。各項目僅須要簡單的關鍵配置便可快速接入該構建工具,享受其帶來的開發體驗提高,和構建性能提高。
用過node-sass的童鞋應該遇到過,安裝node-sass遇到各類編譯錯誤、二進制文件下載錯誤、甚至文件寫入權限錯誤等等😟。也有各類騷操做解決這個問題,但終歸不能一勞永逸。
因而就出現想經過postcss插件去兼容sass語法,雖然經過插件可以兼容部分語法,可是想要在已經有必定量的業務代碼中,替換node-sass的風險是很是高的,本人親自測試各類坑💣
固然也有其餘途徑解決這個問題,不只讓你使用完整的sass語法,同時也免去各類安裝node-sass的問題,官方的sass-loader其實已經提供了dart-sass解析模塊的支持具體參見文檔,可能有人擔憂dart-sass的js模塊性能不高,本人親測在咱們項目中2000+的模塊中,dart-sass的編譯性能並無明顯降低的感受,同時咱們使用使用了緩存能力,一般只變異哪些變化的資源。
具體的配置入下:
{
loader: resolve('sass-loader'),
options: {
// 安裝dart-sass模塊:npm i -D sass
implementation: require('sass'),
includePaths: [
// 支持絕對路徑查找
path.resolve(projectDir, 'src'),
],
sourceMap,
},
},
複製代碼
node-sass 變量使用問題 我在H5中發現不少這種語法的代碼,可是實際上沒有生效,構建後,並無替換爲變量的值。
編譯後:
解決方法以下:
我的以爲postcss是css預處理器的將來,如今的postcss對於css就像babel對於JavaScript。postcss經過插件支持將來的css特性,於此同時你還能夠自定義插件實現想要的特性。但其餘的less、sass這種預處理器,就難以介入它的處理過程,只能按照它既定的規則處理。所以對於全新的項目建議直接使用postcss+postcss-preset-env
使用最新的css語法特性,同時以便於在將來瀏覽器全面支持相關特性後,快速接入支持。
💣若是你使用了css-loader
的import能力,同時有使用了post-css-import
插件的import能力,兩個插件會存在衝突,不建議同時使用!
若是使用了postcss-custom-properties
,須要注意在8.x版本中存在一個bug,沒法解析以下語法:
:root{
--green: var(--customGreen, #08cb6a);
// 8.x沒法正確處理該語法
--primary: var(--customPrimary, var(--green));
}
.test {
background: color(var(--primary) shade(5%));
// 上面面這句將會被轉換爲以下代碼,最終致使瀏覽器沒法解析該語法
background: var(--green);
background: var(--primary);
// 咱們指望轉換爲
background: #08cb6a;
}
複製代碼
解決方法:禁用 postcss-preset-env 中的custom-properties,安裝6.x版本的custom-properties,單獨添加該插件。
若是在開發模式下面啓用了eslint-loader
對jsx?
文件校驗,而且啓動了其緩存能力,當修改eslint校驗規則,你須要清理緩存文件而且從新啓動構建,不然規則修改不會生效!若是使用a8k
工具構建,可使用k clean
命令自動處理處理。
篇幅太長不詳細介紹了,有興趣的能夠在這裏看到相關源代碼webpack-retry-load-plugin, 後續輸入相關文章介紹如何實現CSS/JS同步異步代碼重試。