当前位置: 首页 > 科技观察

之前一直觉得配置Webpack苦不堪言,直到遇到这个流式配置方案

时间:2023-03-12 14:13:51 科技观察

今天在社区介绍一个webpack流式配置方案——webpack-chain,目前已经在我现在的团队实现了,并带来了一些积极的收益,现在我将介绍这个解决方案的背景、核心概念和日常使用姿势。为什么会出现webpack-chain?相信大家对业界知名的构建工具Webpack都不陌生。作为目前生产环境中最稳定、应用最广泛的构建打包工具,当然也有很多优势,比如:丰富的生态。社区里有很多加载器和插件,基本上可以找到你想要的。可插拔的插件机制。基于Tapable实现的可扩展架构。文档成熟。有中文版,一直在更新维护。稳定性高。现在官方的前端项目生产环境基本都是用Webpack搭建的。经过这么多年的行业验证,该踩的坑都差不多。但其实说了那么多优点,大家大概都不太喜欢这个东西,因为最不能忽视的一点就是开发体验。对于构建和打包这件事,在工程上是一个极其复杂的环节,需要输入大量的配置信息来保证打包结果达到预期。在Webpack中,如果不使用其他方案,我们必须手动配置一个庞大的JavaScript对象,所有的配置信息都在这个对象中。这样原始的方式确实对人们的体验非常不利。归纳起来有以下几个原因:对象太大,直观上眼花缭乱。虽然可以封装一些逻辑,但是避免不了深度嵌套的配置;很难动态修改。比如通过脚本动态修改一些配置信息,比如删除babel-loader的一个插件,需要从顶层配置对象一步步找到babel-loader的位置,然后遍历列表插件。这种人工查找和遍历的过程比较繁琐。难以共享配置。如果尝试跨项目共享webpack配置对象,后续的修改会变得杂乱无章,因为需要动态修改原有的配置。社区也有人发现了这些痛点,于是出现了一个Webpack的流式配置方案——webpack-chain。webpack-chain的核心概念其实就是学习webpack-chain。我觉得首先不是学习每个属性的配置方法,而是了解webpack-chain-ChainedMap和ChainedSet这两个核心对象。什么是链图?比如我现在配置路径别名:config.resolve.alias.set(key,value).set(key,value).delete(key).clear()那么,当前的alis对象就是一个ChainMap。如果一个属性在webpack-chain中被标记为ChainMap,它将有一些额外的方法并允许这些链调用(如上例)。接下来我们一一认识一下这几个方法//清除当前Map的所有属性clear()//通过key从Map中移除单个配置value.delete(key)//是否有特定的keyMap中的一个配置值,返回true或falsehas(key)//返回Map中存储的所有值的数组values()//提供一个对象,其属性和值将被映射到Map中。第二个参数是一个数组,表示忽略哪些属性whenTruthy:ChainMap->any,条件为真时执行//whenFalsy:ChainSet->any,条件为假时执行when(condition,whenTruthy,whenFalsy)//获取Map中对应key的值get(key)//先调用get,如果找不到对应的值,则返回fn函数返回的结果,简化操作。在Webpack中,大部分对象都是ChainMap。具体可以去源码看看,实现并不复杂。ChainMap是webpack-chain中一个非常重要的数据结构,它封装了chain调用的方法,使得后续所有的ChainMap类型配置都可以直接复用ChainMap本身的这些方法,非常方便。什么是链集?和ChainMap类似,封装了自己的一套API//末尾加一个值add(value)//开头加一个值prepend(value)//清除设置的内容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配置中,经常可以看到direct.property()的调用方法。是不是感觉很聪明?源码中的实现很简单:extend(methods){this.shorthands=methods;methods.forEach(method=>{this[method]=value=>this.set(method,value);});returnthis;}在初始化ChainMap的时候会调用extend方法,然后直接将属性列表作为methods参数传入,然后通过下面一行代码间接调用set方法:this[method]=值=>this.set(方法,值);这个设计也值得学习。配置Webpack首先需要新建一个配置对象:constConfig=require('webpack-chain');constconfig=newConfig();//经过一系列链式操作//得到最终的webpack对象console.log(config.toConfig())然后依次配置resolve、entry、output、module、plugins、optimization对象。这篇文章的重点是带大家进入webpack-chain,所以我来详细介绍一下各个配置的使用方法。输入和输出这里是一个常见的配置。由于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.添加插件//首先指定名称(这个名称是自定义的),然后使用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.移除一个插件移除一个插件很简单,记得在添加一个插件的时候,我们指定了每个插件的名称?现在你可以通过这个名字删除它:config.plugins.delete('extract')3。指定xx插件之前/之后调用的插件比如我现在需要指定html-webpack-plugin这个插件是刚才写的extractplugin之前执行过,所以这样写就可以了:consthtmlWebpackPlugin=require('html-webpack-plugin');config.plugin('html').use(htmlWebpackPlugin).before('extract')方法之前传入另一个插件的名字就可以了,说明在另一个插件之前执行-在。同样,如果需要在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));loaderloader是Webpack中必不可少的一个配置,我们来看看loader的相关操作。1.添加一个loaderconfig.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=>{//返回选项列表;}).end()所有配置完成后,可以调用config.toConfig()得到最终的配置对象,可以直接作为webpack的配置。3.删除一个loader//通过uses对象的delete方法,删除config.module.rule('ts').test(/\.tsx?/).uses.delete('ts-loader'根据loader的名称)optimizationWebpack中的优化也是一个比较大的对象,参考官方文档:https://webpack.js.org/configuration/optimization/。这里以splitChunks和minimizer为例进行配置:config.optimization.splitChunks({chunks:"async",minChunks:1,//最小chunk,默认1maxAsyncRequests:5,//最大异步请求数,默认5maxInitialRequests:3,//初始化请求的最大个数,默认3cacheGroups:{//Cachechunkspriority:0,//缓存组优先级vendor:{//key为entrychunks中定义的entryname:"initial",//必须选择三选一:"initial"|"all"|"async"(默认为async)test:/react|vue/,//正则规则校验,如果匹配则提取chunkname:"vendor",//到缓存中separatedchunknameminSize:30000,minChunks:1,}}});//添加一个minimizerconfig.optimization.minimizer('css').use(OptimizeCSSAssetsPlugin,[{cssProcessorOptions:{}}])//shiftminimizerconfig除外。optimization.minimizers.delete('css')//修改最小化插件参数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对象的方式。在方便复用配置的同时,也让代码更加优雅。无论是在代码质量还是开发体验上,相比上一个都有不错的提升。我把它推荐给了每一个人。开始吧。