我曾為配置 Webpack 感到痛不欲生,直到我遇到了這個流式配置方案
今天給大家介紹社區當中一個 webpack 的流式配置方案——webpack-chain,這個方案現在已經在我目前所在的團隊落地,且帶來了一些正向的收益,現在就這個方案出現的背景、核心概念及日常使用姿勢給大家展開介紹。
為什么出現 webpack-chain ?
相信大家都對業界鼎鼎有名的構建工具Webpack并不陌生了,作為目前為止最穩定、生產環境應用最多的構建打包工具,它固然有著很多優勢,比如:
- 生態豐富。在社區有大量的 loader 和 plugin,想要的基本都能找到。
- 可插拔的插件機制?;?Tapable 實現的可擴展架構。
- 文檔成熟。有中文版,且一直在更新和維護。
- 穩定性高?,F在正式的前端項目生產環境下基本用 Webpack 來構建,經過這么多年業界的驗證,該踩的坑也都踩的差不多了。
但其實說了這么多優勢,大家估計還是對這個東西沒什么好感,因為還有最重要的一點不容忽視,那就是開發體驗。對于構建打包這個事情來說,本來就是工程化當中的一個細節極其復雜的環節,需要輸入大量的配置信息來保證打包結果符合預期。在Webpack當中,我們如果不用其他的方案,就只有手動地配置一個巨大的 JavaScript 對象,所有的配置信息都在這個對象當中,這樣原始的方式的確給人體驗很不好,歸納為以下幾個原因:
- 對象過于龐大,直觀上讓人看的眼花繚亂,盡管可以封裝一些邏輯,但還是避免不了深層的嵌套配置;
- 難以動態修改。舉個例子,如果通過腳本動態修改一些配置信息,比如刪除 babel-loader 的一個 plugin,那么需要從最頂層的配置對象,一步步找到到 babel-loader 的位置,然后遍歷插件列表,這個手動尋找和遍歷的過程比較繁瑣。
- 難以共享配置。如果你嘗試跨項目共享 webpack 配置對象,那后續的修改就會變的混亂不堪,因為你需要動態地修改原來的配置。
社區當中也有人發現了這些痛點,于是出現了針對Webpack的流式配置方案——webpack-chain。
webpack-chain 核心概念
其實真正學會 webpack-chain,我覺得首先不是去學習具體每個屬性的配置方法,而是理解webpack-chain核心的兩個對象——ChainedMap和ChainedSet。
什么是 ChainMap ?
比如我現在配置路徑別名:
- config.resolve.alias
- .set(key, value)
- .set(key, value)
- .delete(key)
- .clear()
那么,現在的 alis 對象就是一個ChainMap。如果一個屬性在webpack-chain當中標記為ChainMap之后,它會有一些額外的方法,并且允許這些鏈式調用(如上面的示例)。
接下來就來一個個認識這些方法:
- // 清空當前 Map 的所有屬性
- clear()
- // 通過鍵值從 Map 移除單個配置.
- delete(key)
- // Map中是否存在一個配置值的特定鍵,返回真或假
- has(key)
- // 返回 Map中已存儲的所有值的數組
- values()
- // 提供一個對象,這個對象的屬性和值將映射進 Map。第二個參數為一個數組,表示忽略哪些屬性
- merge(obj, omit)
- // handler: ChainedMap => ChainedMap
- // 一個把ChainedMap實例作為單個參數的函數
- batch(handler)
- // condition: Boolean
- // whenTruthy: ChainMap -> any, 條件為真時執行
- // whenFalsy: ChainSet -> any, 條件為假時執行
- when(condition, whenTruthy, whenFalsy)
- // 獲取 Map 中相應鍵的值
- get(key)
- // 先調用 get,如果找不到對應的值, 就返回 fn 函數返回的結果
- getOrCompute(key, fn)
- // 配置鍵值對
- set(key, value)
這些方法的返回對象也都是 ChainMap,這樣可以實現鏈式調用,簡化操作。在 Webpack中,大部分的對象都是 ChainMap,具體大家可以去源碼當中看看,實現并不復雜。
ChainMap 是webpack-chain當中非常重要的一個數據結構,封裝了鏈式調用的方法,以至于后面所有 ChainMap 類型的配置都可以直接復用ChainMap本身的這些方法,非常方便。
什么是 ChainSet ?
跟 ChainMap 類似,封裝了自己的一套 API:
- // 末尾增加一個值
- add(value)
- // 在開始位置增加一個值
- prepend(value)
- // 清空 set 內容
- clear()
- // 刪除某個值
- delete(value)
- // 判斷是否有某個值
- has(value)
- // 返回值列表
- values()
- // 合并給定的數組到 Set 尾部。
- merge(arr)
- // handler: ChainSet => ChainSet
- // 一個把 ChainSet 實例作為單個參數的函數
- batch(handler)
- // condition: Boolean
- // whenTruthy: ChainSet -> any, 條件為真時執行
- // whenFalsy: ChainSet -> any, 條件為假時執行
- when(condition, whenTruthy, whenFalsy)
ChainSet 的作用和ChainMap類似,也是封裝了底層鏈式調用的 API,在需要操作Webpack配置當中的數組類型的屬性時,通過調用ChainSet的方法即可完成。
速記方法
對于 ChainMap,有這樣一種簡化的寫法,官網稱之為速記寫法:
- devServer.hot(true);
- // 上述方法等效于:
- devServer.set('hot', true);
因此,在實際的webpack-chain配置中,可以經??吹街苯?.屬性()這樣調用方式,是不是感覺很巧妙?源碼當中的實現非常簡單:
- extend(methods) {
- this.shorthands = methods;
- methods.forEach(method => {
- this[method] = value => this.set(method, value);
- });
- return this;
- }
在ChainMap初始化的時候,會調用 extend 方法,然后把屬性列表作為 methods參數直接傳入,然后通過下面一行代碼間接調用 set 方法:
- this[method] = value => this.set(method, value);
這樣的設計也是值得學習的。
配置 Webpack
首先,需要創建一個新的配置對象:
- const Config = require('webpack-chain');
- const config = new Config();
- // 一系列鏈式操作之后
- // 得到最后的 webpack 對象
- console.log(config.toConfig())
然后依次配置 resolve、entry、output、module、plugins、optimization 對象,本文關鍵還是帶大家能夠落地 webpack-chain,因此詳細介紹一下各個配置的使用方法。
entry 和 output
這里列舉一個常用的配置,由于 Webpack 在 entry 和 output 掛了太多屬性,大家參考 Webpack 官方文檔照著如下的方式去配就好了。
- config.entryPoints.clear() // 會把默認的入口清空
- config.entry('entry1').add('./src/index1.tsx')//新增入口
- config.entry('entry2').add('./src/index2.tsx')//新增入口
- config.output
- .path("dist")
- .filename("[name].[chunkhash].js")
- .chunkFilename("chunks/[name].[chunkhash].js")
- .libraryTarget("umd")
alias
對于路徑別名的配置,也是幾乎所有項目必不可少的部分,配置方式如下:
- // 可以發現 resolve.alias 其實是一個 ChainMap 對象
- config.resolve.alias
- .set('assets',resolve('src/assets'))
- .set('components',resolve('src/components'))
- .set('static',resolve('src/static'))
- .delete('static') // 刪掉指定的別名
plugins
插件的配置可以說是相當重要的一個環節了,webpack-chain 當中的配置會和平時的配置有些不同,讓我們來具體看看。
1. 添加一個插件
- // 先指定名字(這個名字是自定義的),然后通過 use 添加插件
- config
- .plugin(name)
- .use(WebpackPlugin, args)
舉個例子:
- const ExtractTextPlugin = require('extract-text-webpack-plugin');
- // 先指定名字(這個名字可以自定義),然后通過 use 添加插件,use 的第二個參數為插件參數,必須是一個數組,也可以不傳
- config.plugin('extract')
- .use(ExtractTextPlugin, [{
- filename: 'build.min.css',
- allChunks: true,
- }])
2. 移除插件
移除一個插件很簡單,還記得添加插件時我們指定了每個插件的 name 嗎?現在通過這個 name 移除即可:
- config.plugins.delete('extract')
3. 指定插件在 xx 插件之前/之后調用
比如,我現在需要指定 html-webpack-plugin 這個插件在剛剛寫的 extract 插件之前執行,那么這么寫就行了:
- const htmlWebpackPlugin = require('html-webpack-plugin');
- config.plugin('html')
- .use(htmlWebpackPlugin)
- .before('extract')
通過 before 方法,傳入另一個插件的 name 即可,表示在另一個插件之前執行。
同樣,如果需要在 extract 插件之后執行,調用 after 方法:
- config.plugin('html')
- .use(htmlWebpackPlugin)
- .after('extract')
4. 動態修改插件參數
我們也可以用 webpack-chain 來動態修改插件的傳參,舉個例子:
- // 使用 tap 方法修改參數
- config
- .plugin(name)
- .tap(args => newArgs)
5. 修改插件初始化過程
我們也可以自定義插件的實例化的過程,比如下面這樣:
- // 通過 init 方法,返回一個實例,這將代替原有的實例化過程
- config
- .plugin(name)
- .init((Plugin, args) => new Plugin(...args));
loader
loader 是 Webpack 中必不可少的一個配置,下面我們來看看 loader 的相關操作。
1. 添加一個 loader
- config.module
- .rule(name)
- .use(name)
- .loader(loader)
- .options(options)
舉個例子:
- config.module
- .rule('ts')
- .test(/\.tsx?/)
- .use('ts-loader')
- .loader('ts-loader')
- .options({
- transpileOnly: true
- })
- .end()
2. 修改 loader 參數
可通過 tap 方法修改 loader 的參數:
- config.module
- .rule('ts')
- .test(/\.tsx?/)
- .use('ts-loader')
- .loader('ts-loader')
- .tap(option => {
- // 一系列
- return options;
- })
- .end()
在所有的配置完成之后,可以通過調用config.toConfig()來拿到最后的配置對象,可以直接作為webpack的配置。
3. 移除一個 loader
- // 通過 uses 對象的 delete 方法,根據 loader 的 name 刪除
- config.module
- .rule('ts')
- .test(/\.tsx?/)
- .uses.delete('ts-loader')
optimization
Webpack 中的optimization也是一個比較龐大的對象,參照官方文檔:https://webpack.js.org/configuration/optimization/。
這里以其中的 splitChunks 和 minimizer 為例來配置一下:
- config.optimization.splitChunks({
- chunks: "async",
- minChunks: 1, // 最小 chunk ,默認1
- maxAsyncRequests: 5, // 最大異步請求數, 默認5
- maxInitialRequests : 3, // 最大初始化請求數,默認3
- cacheGroups:{ // 這里開始設置緩存的 chunks
- priority: 0, // 緩存組優先級
- vendor: { // key 為entry中定義的 入口名稱
- chunks: "initial", // 必須三選一: "initial" | "all" | "async"(默認就是async)
- test: /react|vue/, // 正則規則驗證,如果符合就提取 chunk
- name: "vendor", // 要緩存的 分隔出來的 chunk 名稱
- minSize: 30000,
- minChunks: 1,
- }
- }
- });
- // 添加一個 minimizer
- config.optimization
- .minimizer('css')
- .use(OptimizeCSSAssetsPlugin, [{ cssProcessorOptions: {} }])
- // 移除 minimizer
- config.optimization.minimizers.delete('css')
- // 修改 minimizer 插件參數
- config.optimization
- .minimizer('css')
- .tap(args => [...args, { cssProcessorOptions: { safe: false } }])
善用條件
配置之前提到過,對于ChainSet和ChainMap對象都有條件配置方法when,可以在某些很多場景下取代 if-else,保持配置的鏈式調用,讓代碼更加優雅。
- config.when(
- process.env.NODE === 'production',
- config.plugin('size').use(SizeLimitPlugin)
- )
小結
webpack-chain作為 webpack 的流式配置方案,通過鏈式調用的方式操作配置對象,從而取代了以前手動操作 JavaScript 對象的方式,在方便復用配置的同時,也使代碼更加優雅,無論是從代碼質量,還是開發體驗,相對于之前來說都是不錯的提升,推薦大家上手使用。