8

我曾为配置 webpack 感到痛不欲生,直到我遇到了 webpack-chain

 3 years ago
source link: https://zhuanlan.zhihu.com/p/364092415
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

我曾为配置 webpack 感到痛不欲生,直到我遇到了 webpack-chain

公众号「前端三元同学」,分享编程&个人成长干货

今天给大家介绍社区当中一个 webpack 的流式配置方案——webpack-chain,这个方案现在已经在我目前所在的团队落地,且带来了一些正向的收益,现在就这个方案出现的背景、核心概念及日常使用姿势给大家展开介绍。

为什么出现 webpack-chain ?

相信大家都对业界鼎鼎有名的构建工具Webpack并不陌生了,作为目前为止最稳定、生产环境应用最多的构建打包工具,它固然有着很多优势,比如:

  • 生态丰富。在社区有大量的 loader 和 plugin,想要的基本都能找到。
  • 可插拔的插件机制。基于 Tapable 实现的可扩展架构。
  • 文档成熟。有中文版,且一直在更新和维护。
  • 稳定性高。现在正式的前端项目生产环境下基本用 Webpack 来构建,经过这么多年业界的验证,该踩的坑也都踩的差不多了。

但其实说了这么多优势,大家估计还是对这个东西没什么好感,因为还有最重要的一点不容忽视,那就是开发体验。对于构建打包这个事情来说,本来就是工程化当中的一个细节极其复杂的环节,需要输入大量的配置信息来保证打包结果符合预期。在Webpack当中,我们如果不用其他的方案,就只有手动地配置一个巨大的 JavaScript 对象,所有的配置信息都在这个对象当中,这样原始的方式的确给人体验很不好,归纳为以下几个原因:

  1. 对象过于庞大,直观上让人看的眼花缭乱,尽管可以封装一些逻辑,但还是避免不了深层的嵌套配置;
  2. 难以动态修改。举个例子,如果通过脚本动态修改一些配置信息,比如删除 babel-loader 的一个 plugin,那么需要从最顶层的配置对象,一步步找到到 babel-loader 的位置,然后遍历插件列表,这个手动寻找和遍历的过程比较繁琐。
  3. 难以共享配置。如果你尝试跨项目共享 webpack 配置对象,那后续的修改就会变的混乱不堪,因为你需要动态地修改原来的配置。

社区当中也有人发现了这些痛点,于是出现了针对Webpack的流式配置方案——webpack-chain

webpack-chain 核心概念

其实真正学会 webpack-chain,我觉得首先不是去学习具体每个属性的配置方法,而是理解webpack-chain核心的两个对象——ChainedMapChainedSet

什么是 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())

然后依次配置 resolveentryoutputmodulepluginsoptimization 对象,本文关键还是带大家能够落地 webpack-chain,因此详细介绍一下各个配置的使用方法。

entry 和 output

这里列举一个常用的配置,由于 Webpack 在 entryoutput 挂了太多属性,大家参考 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/

这里以其中的 splitChunksminimizer 为例来配置一下:

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 } }])

善用条件配置

之前提到过,对于ChainSetChainMap对象都有条件配置方法when,可以在某些很多场景下取代 if-else,保持配置的链式调用,让代码更加优雅。

config.when(
  process.env.NODE === 'production',
  config.plugin('size').use(SizeLimitPlugin)
)

小结

webpack-chain作为 webpack 的流式配置方案,通过链式调用的方式操作配置对象,从而取代了以前手动操作 JavaScript 对象的方式,在方便复用配置的同时,也使代码更加优雅,无论是从代码质量,还是开发体验,相对于之前来说都是不错的提升,推荐大家上手使用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK