webpack作爲一款當下最流行的前端構建工具,一直以來以門檻過高而受人詬病,且沒有一個通用的配置適合全部的項目,爲此咱們不得不開心的(大誤:smiley:)手動配置咱們的項目,開啓咱們的配置工程師之路,本文主要是對webpack的一些基礎配置的解釋和咱們能作的一些優化工做,適合有使用過webpack的前端開發者閱讀,各位在閱讀過程當中不妨打開本身的項目來跟着我一塊兒優化。javascript
dev prod
一般咱們的項目會有開發環境和生產環境,而開發環境咱們配置的目標是構建更快,模塊熱替換,能從chrome控制檯報錯信息對應的源碼的錯誤處(source map)等。生產環境咱們更加關注chunk分離,緩存,安全,tree shaking等優化點。 固然對於開發和生產環境的配置文件確定是不徹底相同的,因此咱們將不一樣環境的配置文件分離開,同時將共用部分抽出來,最後使用webpack-merge插件整合。css
//參考使用方法
const webpackMerge = require('webpack-merge');
const additionalConfigPath = {
development: './webpack.dev.config.js',
production: './webpack.prod.config.js'
};
const baseConfig = {};
module.exports = webpackMerge(
baseConfig,
require(additionalConfigPath[process.env.NODE_ENV])
);
複製代碼
在下方的優化點中 我會標註這些點適用的環境 dev=開發環境 prod=生產環境html
prod
代碼分離可以將工程代碼分離到各個文件中,而後按需加載或並行加載這些文件,也用於獲取更小的 bundle,以及控制資源加載優先級前端
webpack有三種經常使用的代碼分離方法:java
//多入口配置 最終輸出兩個chunk
module.exports = {
entry: {
index: 'index.js',
module: 'module.js'
},
output: {
//對於多入口配置須要指定[name]不然會出現重名問題
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
複製代碼
問題所在:入口chunks中若是包含重複的模塊,如jquery,那些重複模塊都會被引入到各個bundle中node
對於webpack4 < 4 咱們使用CommonsChunkPlugin插件react
entry: {
"app": "entry-client.js"
}
//首先將app入口的公共模塊提取出來
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: '[name].[chunkHash:6].common.js',
chunks: ['app']
})
//將 vendor runtime分離出來
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor','runtime'],
filename: '[name].[chunkHash:6].bundle.js',
//這個配置保證沒其它的模塊會打包進 vendor chunk
minChunks: Infinity
}),
複製代碼
chunks 選項表明了從哪一個入口分離公共文件,這裏咱們使用咱們項目的主入口 app來分離自定義的公共模塊jquery
首先將自定義的公共模塊單獨抽離出來,這樣作的目的是方便咱們作緩存,當自定義模塊更新時不會影響到vendor文件的hash值。webpack
而後將第三方庫文件 和 webpack運行文件分離。web
這樣咱們的vendor就是很是乾淨的了,只包含第三方庫,能夠作長效緩存。這個地方須要分離webpack運行文件runtime,由於不管咱們是否修改了項目文件,每次build項目時的runtime文件的hash值老是會發生變化的,須要單獨打出來。
對於webpack4 咱們須要實現一樣的目標
// 提早自定義公共模塊
optimization: {
splitChunks: {
chunks: "all",
minSize: 20000,
//其餘入口chunk引用的次數
minChunks: 1,
//默認使用name + hash生成文件名
name: true,
//使用自定義緩存組
cacheGroups: {
//公共模塊
commons: {
name: 'common',
//緩存優先級設置
priority: 10,
//從入口chunk提取
chunks: 'initial'
},
//提取第三方庫
vendors: {
//符合條件的放入當前緩存組
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
},
}
}
}
//提取webpack運行文件
runtimeChunk: {
name: 'manifest'
},
複製代碼
上面的配置大概意思是對全部知足條件的chunk開啓代碼拆分。
優先級(priority):多個分組衝突時決定把代碼放在哪塊
經過splitChunks 和 runtimeChunk插件咱們能達到一樣的拆分和緩存目的
webpack能夠將異步加載的組件打包爲chunk,同時按需加載此chunk。最爲常見是咱們的路由組件,咱們能夠將路由入口組件動態導入以達到異步加載的目的。
異步組件的導入主要有兩種方式 1:ES6 stage3的提案 import() 2:webpack 特定的 require.ensure
//import 方式
const Login = () => {
import(/* webpackChunkName: 'Login' */'./Login')
}
//require.ensure 方式
require.ensure([], function(require){
require('./Login');
},"Login");
//異步組件打包的chunk命名 使用webpackChunkName 或者
//使用require.ensure的第三個參數
//對於webpack2/3須要配置chunkFilename來接收配置的name
output: {
//非入口 chunk 的名稱
chunkFilename: '[name].[chunkHash:6].chunk.js'
}
//webpack4配置name爲true(默認值)
optimization: {
splitChunks: {
//chunk name由塊名和hash值自動生成
name: true
}
}
複製代碼
require.ensure 如今依賴於原生的 Promise。若是在不支持 Promise 的環境裏使用 require.ensure,你須要添加 polyfill。
既然咱們可使用import()和require.ensure來動態導入組件那其實在項目中咱們更是作到能夠基於組件的異步加載。
好比咱們的首頁是由20個組件組成,可是首屏只會顯示15個組件,另外的5個組件多是一些彈窗,或底部才顯示的這種組件,那麼咱們徹底能夠將之使用動態導入抽離出來,在須要的時候加載它,如:
//在異步事件中去導入組件
<div onClick={this.showPopUps}>
show-popUps
</div>
showPopUps = () => {
this.popUps = ()=> {
import(/*webpackChunkName: 'PopUps'*/'./popUps')
}
}
複製代碼
這樣處理的問題在於,咱們在點擊按鈕彈出浮層時會有必定的延時,由於在點擊時咱們才從服務端去獲取組件,用戶體驗可能不太優秀。
這時咱們可使用一種預測加載的技術prefetch。prefetch提示瀏覽器這個資源未來可能須要,瀏覽器會選擇==空閒的時間==去下載此資源。prefetch經常用於加速下一次操做。
<link rel="prefetch">
複製代碼
兼容性問題:從上述能夠得知,prefetch時基於瀏覽器實現的,存在必定的兼容性問題,在safari上還沒由獲得支持。
在不支持的瀏覽器中並不會影響咱們頁面的功能
使用:preload-webpack-plugin 使用條件:webpack > 2.2 且須要使用 html-webpack-plugin
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: "prefetch",
as: 'script',
//包含了哪些chunk,默認值爲"asyncChunks"
include: 'asyncChunks'
})
]
//最終效果相似這樣
<link rel="prefetch" as="script" href="chunk.d15e.js">
複製代碼
對於.css結尾的文件 as=style,.woff2結尾的文件as=font,不然as=script,固然你能夠像上方那樣給一個肯定的值。
對於include的值,其實咱們並非須要全部的異步chunk都使用prefetch,因此咱們須要重設include的值,像這樣:
//1 給定須要prefetch的塊的名字(chunkName)
include: ["PopUps","Login"]
//2 或者在動態導入時指定Prefetch
this.popUps = () => {
import(/* webpackChunkName: 'PopUps', webpackPrefetch: true */'./PopUps')
}
複製代碼
dev prod
爲Loader指定做用範圍能夠加快咱們的構建速度,提高開發體驗。
{
test: /\.jsx?$/,
use: [
"babel-loader"
],
exclude: /node_modules/
}
//or
{
test: /\.jsx?$/,
use: [
"babel-loader"
],
include: /src/
}
複製代碼
dev
resolve的部分配置過多項,會致使webpack搜索範圍變大,效率降低,如沒有特殊需求,能夠保持默認配置保持開發體驗。
//解析模塊時應該搜索的目錄
modules: ["node_modules"] //default
//自動解析肯定的擴展,不宜過多
extensions: [".js", ".json"] //default
//解析目錄時要使用的文件名,不宜過多
mainFiles: ["index"] //default
複製代碼
dev prod
externals 配置選項提供了「從輸出的 bundle 中排除依賴」的方法,什麼意思呢?顧名思義,externals能夠排除掉咱們打包結果中某些依賴:
//排除react 咱們打包後的bundle中沒有react了
externals: {
//將react指向一個全局變量 react
react: "react"
}
//這裏的 "react" 會直接去全局變量中尋找
import react from "react"
複製代碼
這種設置一般用於==library==開發人員,你的library依賴了react,同時使用了你的庫的開發人員也依賴了react,最終會造依賴你的庫的用戶bundle中react被重複打包。
你應該作的是在你的library中將外部依賴放到externals中排除掉,而後將你的依賴暴露給使用你的庫的用戶,最後你的library應該從外部獲取這些擴展依賴,而不是一股腦打包進你本身的bundle中。固然這個依賴在用戶的環境中必須存在且可用。
問題所在:對於從一個依賴目錄中,調用的多個文件,使用externals 沒法一次排除;
import one from 'react/one';
import two from 'react/two';
//上面這種狀況只能一個個匹配,或者這經過正則匹配
externals: [
'react/one',
'react/two',
/^react\/.+$/
]
複製代碼
這裏不得不提的是npm依賴管理中的peerDependencies,它的做用正是將咱們library中依賴的庫暴露給用戶的,如果用戶沒有安裝咱們庫中的必需依賴,npm會拋出錯誤。
固然若是你不是一個library開發人員,你一樣可使用externals來排除項目中的外部依賴,而後經過其餘方式引入成爲一個全局變量的方式來優化打包速度。
dev
webpack在每次build時都會將整個項目從新構建一遍,無論這個文件是否發生了改變(固然若文件未更改hash值不變),可是其實咱們所依賴的三方庫更改並不頻繁,如果將三方庫抽離出來單獨構建,將構建好的目標和構建生成的json對照文件引如咱們的項目,這樣不用每次都去打包這些代碼。
使用 DllPlugin 將更改不頻繁的代碼進行單獨編譯。這將改善引用程序的編譯速度,即便它增長了構建過程的複雜性。
//新建 webpack.dll.conf.js
module.exports = {
entry: {
vendor: [react, ...]
},
output: {
filename: 'dll.[name].js'
},
plugins: [
new webpack.DllPlugin({
path: '[name]-manifest.json'
})
]
}
//webpack.base.conf.js
const manifest = require('./vendor-manifest.json')
plugins: [
new webpack.DllReferencePlugin({
manifest
})
]
複製代碼
最後須要手動將生成的dll.[name].js引入到html文件中去,固然這一步驟能夠用assets-webpack-plugin插件優化。