我曾为配置 Webpack 感到痛不欲生,直到我遇到了这个流式配置方案

时间:2021-05-02

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

为什么出现 webpack-chain ?

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

  • 生态丰富。在社区有大量的 loader 和 plugin,想要的基本都能找到。
  • 可插拔的插件机制。基于 Tapable 实现的可扩展架构。
  • 文档成熟。有中文版,且一直在更新和维护。
  • 稳定性高。现在正式的前端项目生产环境下基本用 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);
  • });
  • returnthis;
  • }
  • 在ChainMap初始化的时候,会调用 extend 方法,然后把属性列表作为 methods参数直接传入,然后通过下面一行代码间接调用 set 方法:

  • this[method]=value=>this.set(method,value);
  • 这样的设计也是值得学习的。

    配置 Webpack

    首先,需要创建一个新的配置对象:

  • constConfig=require('webpack-chain');
  • constconfig=newConfig();
  • //一系列链式操作之后
  • //得到最后的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)
  • 举个例子:

  • constExtractTextPlugin=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 插件之前执行,那么这么写就行了:

  • consthtmlWebpackPlugin=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)=>newPlugin(...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=>{
  • //一系列
  • returnoptions;
  • })
  • .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 对象的方式,在方便复用配置的同时,也使代码更加优雅,无论是从代码质量,还是开发体验,相对于之前来说都是不错的提升,推荐大家上手使用。

    原文地址:https://mp.weixin.qq.com/s/KGa7jEUfBkciJ4tt-bEXYg

    声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

    相关文章