注:本文webpack版本为5.74.0
经过vite的学习,我们了解到no-bundle工具正在异军突起,百花齐放,因此webpack的地位也是遇到威胁,同时,经过多年的发展,webpack上手成本非常之高,学习曲线非常陡峭,因此越来越复杂,虽然官方和社区有着不少优质文章,但依然缺少体系化和深度化。
那么,webpack还有学习必要吗?
答案是肯定的,一方面,我们业务正是基于webpack来构建的,我们接触到的社区代码、生态,也有很大部分是基于webpack构建的,因此,我们在维护项目的时候,遇到一些针对性功能,或者构建的疑难杂症,或者是性能优化,常常束手无策,一个小问题,会导致花费大量时间去解决,效率低下。
另一方面,根据State-of-JS 2021 统计数据,在2021年,webpack依然占据着使用排行榜榜首的位置。
也就是说,未来有可能是no-bundle的天下,但目前来看,近几年,webpack依然是主流中的主流。
同时,no-bundle工具目前依然是一个正在发展的阶段,生态并没有webpack一样强大,经过几年的发展,webapck功能覆盖小程序、桌面应用、微前端等场景,作为构建工具的主流,他的逻辑也被借鉴到各个构建工具中。
现在,webpack依然在迭代,同时推出了持久化缓存,懒编译等特性,虽然依然不及no-bundle,但还没有到达被立刻抛弃的程度。
因此,webpack依然值得学习。
webpack配置
webpack的配置项繁多,这么多的配置所带来的的,就是流程上的细化,而这流程,其实也就是绝大部分构建工具的流程,我们简化一下,就是以下的样子。
入口文件:指的是构建工具需要知道从哪开始进行构建,通过这个入口文件,逐步查找到整个项目代码。
模块处理:指的是针对模块转译,在webpack中,是使用loader,把模块转译为AST,然后分析依赖关系,然后进一步递归处理模块,在vite中,就是预构建和vite将业务代码处理成ESM。
后处理:所有模块解析完毕,在内存中生成了产物,这个时候需要对总体产物进行处理,比如注入运行时、模块合并与拆包等。
打包成产物:就是将内存中的产物写入文件系统。
因此,webapck的配置项,大体分为两类:
流程类
工具类
流程类
入口文件:
entry:定义项目入口。
context:项目执行的上下文路径。
模块处理:
resolve:用于模块路径的解析,在vite中我们经常遇到的resolveId和onResolve属于这种。
module:模块加载规则,这个在vite中我们并没有遇到类似的,因为vite依赖Rollup,已经进行了内置。
externals:声明外部资源,webpack解析、打包的时候会忽略到这些资源。
后处理:
optimization:控制产物的体积、分包、代码压缩、混淆,在vite那里,我们在类似的build.rollupOptions.output实现了自定义分包。
target:用于配置产物的运行环境,不同的环境产物会有所不同。
mode:编译模式,与vite的mode类似。
plugin:插件。
打包成为产物
output:配置产物的路径、名称,与vite的build.rollupOptions.output类似
工具类
webpack并非只有上述配置,还有着开发环境、性能优化等配置。
开发效率:
watch:用于监听文件的变化,持续构建。
devtool: 用于配置产物的Sourcemap。
devServer:配置开发服务器。
性能优化类:
cache:该选项用于控制如何缓存编译过程和编译结果。
performance:配置产物大小的阈值。
日志类
stats: 控制编译过程的日志内容,在性能调试的时候使用。
infrastructureLogging:日志输出方式。
我们注意到。以上的配置选项,我们或多或少在vite中寻找到类似的痕迹,这并非是webpack像vite,而是webpack建立、稳固了这种模式,并通过增加上百项配置,来细化控制各个情况下的表现。
配置详解
webpack的配置除了对象的方式外,还支持数组对象、函数的方式。
对象是一种比较常见的配置方式,逻辑简单,因此适合大部分简单的项目。
数组对象是针对某些特定情况才会使用的配置,但数组项都会创建多个构建实例,这些实例不会进行共享,因此不会复用之间的结果,所以不会带来任何性能上的优化。
函数是根据环境,进行动态调整的配置对象。
这里需要注意的是,如果使用了数组对象作为配置项,同时使用了webpack --config-name="xxx"来进行构建,那么构建逻辑仅仅使用xxx的,而非整个数组。
如果我们遇到数组的情况,可以借助webpack-merge 来简化配置逻辑。这个工具是专门用来合并配置对象的工具,这个工具的逻辑与lodash的merge类似,但也支持更多的特性。
支持数组的合并
支持函数属性的合并
支持合并策略
支持自定义对象合并函数
因此,这个工具不光使用于整个webpack的配置项,还可用于rules、entry、output的配置。
entry
正常来说,入口文件是多种多样的,而针对入口文件的处理也体现了整个构建工具的特性。
在webpack中,入口文件可以是一个字符串,简单粗暴地指定入口文件的路径,也可以是对象。
这里需要注意的是,如果是一个对象,那么这个对象的key就是包名,而value值才是entry作为对象的时候的配置项。
这个配置项可以是一个字符串,也可以是一个对象,对象的配置项比较多。还可以是函数,可以动态生成entry的信息,而返回值也可以是字符串、对象或者数组。当然,也支持数组,数组每一项也可以是上面说的字符串、对象、函数。
对象的的配置最为复杂,包含以下属性。
import: 入口文件,如果是数组,说明是多入口。
dependOn:前置依赖bundle。
runtime: 设置该入口的runtime chunk,如果不为空,webpack就会将该入口运行时的代码进行抽离。
filename:声明构建产物模块。
library:构建NPM Library使用。
publicPath:声明入口发布的URL。
chunkLoading:异步加载方技术方案,比如jsonp、require、import。
asyncChunks:声明模块是否异步加载。
这些大概都能理解,不过其中dependOn和runtime可能需要花一些时间。
我们来看看设置好dependOn,会发生什么。
我们这里设置了两个入口,其中一个入口设置了dependOn,指向第一个入口文件。
此时 Webpack 认为,项目在加载 foo 产物之前一定会加载 main,因此可以将重复的模块代码、运行时代码等都放到 main 产物,这样会减少不必要的重复,最终打包结果:
我们可以看到,main.js中的代码比较臃肿,是较为平常的bundle,而相比而言,foo.js中的代码较少。
dependOn 适用于那些有明确入口依赖的场景,比如我们构建了一个主框架 Bundle,其中包含了项目基本框架,还需要为每个页面单独构建 Bundle,这些业务代码也都依赖于主框架代码,此时可用 dependOn 属性优化产物内容,减少代码重复。
接下来,我们看一下runtime。
为了支持产物在各种环境中运行,webpack会在产物中注入一系列代码,这些代码数量与使用wepack的特性数量相关,有的时候,这些注入代码会超过业务代码,因此,为了产物缓存,我们可以把这些代码抽离出来。
我们把runtime在各个入口设置为同一个名称。
然后进行打包,可以看到,每个入口的代码都相对清爽,而大部分代码都被提取到runtime中。
因此,runtime也是一个常用的性能优化手段。
output
output是输出构建结果,比如产物的位置,文件分包,名字是什么,其中,他还支持多个配置项。
path:产物的位置。
filename:产物的命名规则,支持[name]、[hash]的占位符。
publicPath:文件发布路径。这个会在cdn中使用。
clean:是否会清除path目录下的内容。
library:构建NPM Library使用。
chunkLoading:异步加载方技术方案,比如jsonp、require、import。
这里我们注意到,output的配置项与entry大部分是重合的,如果output和entry的配置相同,那么优先级以entry为主。
target
大多数的时候,webpack打包web应用,但实际上还支持Node、electron、NW.js等形态,这个特性主要是target配置控制的。他主要支持以下数值:
node[[X].Y]:编译为Node应用,此时将使用Node的require方法加载其他Chunk,还支持指定版本。
async-node[[X].Y]:编译为Node应用,但跟node的区别在于,async-node是以异步进行加载模块的。
nwjs[[X],Y]:编译为NW.js应用。
electron[[X].Y]-main: 构建Electron主进程。
electron[[X].Y]-renderer: 构建Electron渲染进程。
electron[[X].Y]-preload: 构建Electron Preload脚本。
web:构建web应用
esX:构建为特定ECMAScript兼容代码。
browserslist:使用browserslist语法。
不同构建目标在打包的时候会出现差异的结果。如下:
可以看到,在web版本跟node版本是不同的,node版本增加了exports相关的兼容。
mode
mode本身使用很简单,但同时也算一个非常关键的配置项,webpack内置了跟多构建优化策略,具体如下:
production:默认值,生产模式,使用该值,webpack会开启一系列优化措施,比如树摇、代码压缩,代码分割。
development: 开发模式,使用此值,会保留语义化的代码,更助于调试。
none: 关闭所有内置优化规则。
模块联邦
在使用vite的时候,我们提到模块联邦是webpack提出来的一个新特性,那么在webpack这里,我们来看一下,他们是如何进行操作的。
模块联邦最常见的应用是微前端。那么我们来稍微了解一下微前端。
在微前端中,会存在一个容器应用,他的任务就是加载各个微应用。
而微应用需要做到以下事情:
提供两个方法,一个是挂载方法,容器应用会调用他来渲染应用,另一个个是卸载方法,用于卸载微应用,他们都以接口的形式提供给容器。
远程入口文件的地址,容器应该在合适的时候动态记载文件,同时获得花在方法后,进行微应用的渲染。
提给微应用的ID,用于标识自己。
路由首地址,此地址决定微应用的视图展示。
而容器需要做到以下事情:
加载远程的微应用,进行渲染。
在合适的时机,卸载微应用。
而模块联邦,让打包的产物,也有了动态加载功能,我们注意到,当一个微应用有了动态加载功能,那么他就可以成为一个容器应用。如果我们确立了只有一个容器应用的结构后,上图会变成这样:
可以看到,使用了模块联邦,每个应用在架构上面是平等的,应用也会进行相互依赖和相互加载。换句话说,每一个都是容器应用,也就是说,在微前端上,并没有进行任何架构上的约束,但我们可以从业务上规划他,来规划处容器应用和微应用的区别,从而让整个架构是一个有价值的、可复用的模块,而不是脱缰的野马。
如何使用
那么,webpack是怎么实现模块联邦的呢?
webpack内置了一个ModuleFederationPlugin 插件,不管是导出模块还是导入模块,都需要使用此插件,针对生成方,插件的expose参数,确定了需要导出的列表,filename参数,定义了导出入口文件,针对模块使用方,插件的remotes参数确定了引入远程模块的位置。
针对导出方,我们可以这么使用
而针对使用方,我们可以这么使用
然后我们就可以在业务文件中使用了。
// 正常引入
import { sayHello } from "RemoteApp/utils"
// 异步引入
const { sayHello } = await import("RemoteApp/utils");
这里需要注意的是,webpack的模块联邦的remotes与vite的有些许不同。
webpack需要定义的格式是LocalModuleName: "RemoteModuleName@Host",也就是说,这里并不能单纯填写入口文件,而是需要远程模块的名称,同时确定本地使用的时候的模块名称,在大部分情况下,远程模块和和被使用的时候的名称是相同的,但也不排除名称冲突的情况。
同时,remotes还接受[XXX]格式的占位符,XXX的值会从全局下取。也就是说[XXX] === window.XXX
共享模块
跟vite的模块联邦一样,webpack也支持模块的共享,通过shared字段进行控制。shared接受对象和数组,当是数组的时候,每一项都是包名,而是对象的的时候,key作为包名,而value是详细的配置项。
共享模块通常用来配置常服的基础依赖库,因此在引入的远程模块的产物中,并不会打包shared所指向的基础库的代码内容。而是让他们作为异步模块加载。同时,模块联邦对共享模块做了版本化管理,从防止共享模块的版本不同,而导致不必要的冲突和错误。
同时,如果本地启动项目的时候,使用共享模块,需要把eager置为true,否则会出现以下错误。
Uncaught Error: Shared module is not available for eager consumption
该选项会将模块打入容器文件中,作为同步模块加载并使用。或者在项目对应地方进行修改,使用异步加载共享依赖也可以进行解决。
我们还可以使用requiredVersion,来指定共享模块的版本,当接受字符串的时候,我们可以引入package.json的对应版本,来让我们的依赖保持一致。而如果是布尔值的时候,会根据此值来判断,是否开启模块联邦的版本号自动推断。此配置项限制依赖版本的上线下,因此极为实用。
shared还提供了其他配置:
shareScope:任意字符串,如果本地模块和引用模块使用了相同的依赖,但只想针对一定范围内的微应用共享依赖,那么就可以共同配置相同的字符串,从而使相同依赖在更精确的范围内进行共享。
version: 依赖包的版本,默认会从对应包的package.json的version获取。
packageName:包名,当无法从请求中自动确定包名的时候,才需要配置此选项。
import: 声明如何导入该模块,实用性不高。
优化性能
持久化缓存
缓存是一门提升性能的技术,不仅仅在前端,在计算机领域也无处不在。我们在vite中遇到的预构建和metafile.json,也是属于缓存的一种,而webpack中,也使用了各种各样的缓存,也就是说,缓存是一种牺牲空间来换取时间、提升效率的方法。
持久化缓存是webpack5提出来的,他能将首次构建的过程和结果数据保存在本地文件系统、或者内存中,在下次执行构建的时候跳过机解析、链接、编译一一些列耗费性能的操作,甚至可以直接复用chunk,迅速构建出最终产物。
而相关的配置项就是cache,他提供了几个相关的配置项,用来配置缓存效果和周期。
type:缓存类型,支持 memory 和 filesystem,其中设置为filesystem即可开启持久化缓存。
cacheDirectory:缓存文件路径,默认是node_modules/.cache/webpack。
buildDependencies: 额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,类似vite中,pacgeage-lock.json变动导致预构建失效。
managedPaths: 受控目录,使用缓存的时候,会进行新旧代码的哈希值和时间戳的对比,在此选项中的目录会跳过对比,直接使用缓存,默认值为 ['./node_modules']。
profile:是否输出缓存处理过程的详细日志,默认为fasle。
maxAge:缓存失效时间,默认为5184000000。也就是60天。
那么,这个持久化缓存,所节约的时间,大概有多少呢?
我们请出构建工具的劳模——Three.js来给大家现身说法。
在three.js的dev分支上,目前有368个js文件,合计31653行代码。
顺带一提,Three.js使用的Rollup进行构建,因此我们需要把他改造成webpack,同时也增加了babel-loader和@babel/preset-env来模拟构建环境。
然后执行webpack的构建,发现时间大概在3500ms左右。
然后,使用cache特性,进行构建,由于是第二次构建cahce才会起到作用,因此我们构建两次,发现第二次时间为360ms。
从结果可以看出来,cache的的确确可以减少构建时间,提升十倍左右的性能。
那么,问题来了,cache如何提升这么大的性能呢?
那么我们就需要先从webpack的构建流程说起。
webpack的构建流程
我们前面提到过流程,但那个是webpack的配置的分类,现在这个流程是构建流程,我们知道webpack最核心功能,是通过loader将任意文件转译成为可识别代码,比如css转译为js字符串,图像转译为base64,然后对这些产物进行合并、打包、兼容。
这里需要提到的一点是,webpack是不识别css文件的,他通过css-loader把css转译为js,然后在打包的时候,如果是开发环境就通过style-loader注入到html中,如果是生产环境就使用mini-css-extract-plugin,生成单独的css文件,注入到html文件。
webpack的工作流程分为以下几个阶段:
初始阶段:
初始化参数,从配置文件,配置对象,命令行,获取最终的配置,在类似于vite中的configResolved钩子。
创建编译器对象,以最终配置,创建编译器,如果最终配置是一个数组,那么就有多个编译器对象。
初始化编译环境,包括注入内部插件、注册模块工厂、加载配置插件。
开始编译:根据入口文件,调用compilition.addEntry,把入口文件转换为dependence对象。也就是记录依赖关系,类似vite的记录模块之间的依赖图。
构建阶段:
编译模块,从入口文件开始,使用loader把模块转译为标准js内容,然后再转译为AST,再从中找出依赖模块,然后递归操作,直到所有可到达的模块都经过了处理。
完成模块编译,所有模块都经过处理后,我们也同时得到了每个模块被翻译后的内容,以及他们只见的依赖关系图。
生成阶段:
合并,根据入口和模块之间依赖关系,对每一个模块,进行代码转译,然后分析依赖,合并代码组成一个个chunk。
优化,对这些chunk进行优化,比如树摇、压缩等。
写入文件系统,跟vite大多数打包工具一样,得出的chunk一开始都在内存中,写入磁盘才是最后的操作。
我们注意到,以上会有不少可能有性能问题的地方,比如构建阶段,这里涉及到多次文件的读取,也就是IO操作。还有是loader转译,这里涉及到密集的CPU操作,递归也会因为模块的数量较多,而消耗大量资源。在生成阶段,树摇、分包也涉及到AST和算法,因此也会消耗大量的CPU资源。
而webpack的持久化缓存功能,就是将以上流程的部分结果,进行缓存,当下次构建的时候,尝试从这些缓存中进行恢复,如果哈希值和时间戳一致,从而跳过初识阶段和构建阶段,直接从生成阶段开始。
这个与vite的预构建类似,但区别在于,持久化缓存是构建完成、第二次构建才会生效,但vite的预构建是在第一次构建的时候,就开始进行了。
并行构建
我们都知道,Node.js是单线程,那么意味着webpack针对资源的解析、转译、合并可能会进行相互阻塞,所以导致CPU利用率极低,因此社区中出现了一些以多线程方式运行webpack,从而提升效率。这些方案核心都很类似,那就是针对某周计算任务创建子进程,然后通过IPC回传给主进程,最后把结果给Webpack。
比如Thread-loader就是其中一个方案:
Thread-loader 提供了一个loader,只需要放入loader最开头即可。
Thread-loader是官方提供,因此维护、迭代稳定。
我们简单配置一下,使用three.js无缓存进行一次构建。
可以看到,使用了并行构建,webpack的构建时间少了30%左右。虽然这只是加载单一资源的场景。
如果想加载多种资源类型,只需要根据不同资源,在loader之前配置这个loader就可以,启动后,他会在加载文件时创建新的进程,在子进程运行之后的loader,执行完毕后再回传给webpack。
不过有一点需要注意的是,Thread-loader不能调用emitAsset等接口,这意味着style-loader 这一类加载器无法正常工作,解决方案是将这类组件放置在 thread-loader 之前,如 ['style-loader', 'thread-loader', 'css-loader']。
并且loader不能获取compilation、compiler 等实例对象,也无法获取 Webpack 配置。
当然除了这些缺点,他具有较高配置自由度,因为是一个loader,因此我们可以使用options进行配置:
workber:子进程总数,默认比cpu 少 1
workerParallelJobs: 单个进程并发执行的任务数
poolTimeout: 空闲进程关闭时间
poolRespawn:是否运行子进程关闭后重新创建子进程,一般情况是fasle。
workerNodeArgs:启动子进程的额外参数。
虽然并行构建会节约时间,但由于频繁创建、销毁进程,会带来新的性能损耗,因此,Thread-loader提供了warmup接口,用来提前创建工作子进程,需要注意的是,子进程需要预加载对应的loader模块。
lazyCompilation
lazyCompilation是webpack5另一个新特性,用于实现入口或者异步引用模块的按需编译。
这个在vite中自带支持的,也正是这个原因,导致我们在vite中使用异步路由,产生了巨量的chunk,然后进行合包。
但不可否认,异步加载、异步编译是一个非常实用的特性,lazyCompilation正是webpack的解决方案。
我们只需要在配置中,将experiments.lazyCompilation设置为true,即可开启这个特性。
开启后,代码中的异步引用语句、异步导入模块,不会被立即编译,而是等到页面正式请求该模块,才开始构建,因此极大提升了冷启动的速度。
同时lazyCompilation还支持如下参数:
backend:设置后端服务器信息,这个一般是默认值即可。
entries:设置是否对entry启动进行按需编译。
imports:设置是否对异步模块进行按需编译。
test:支持正则表达式,用来声明哪些异步模块启用按需编译的新特性。
noParse
我们都了解,webpack的loader是进行代码转换,但,有的代码是已经做好转换的,不需要进行二次编译就可以在浏览器进行运行了,因此如果再次转换,反而是一种浪费性能的方式,因此,module.noParse可以用来设置,哪些资源没必要做重复的代码解析、以来分析、转译功能。
这个配置项支持正则、函数、字符串、字符串数组等性能。
配置后,所有匹配到的文件都会跳过前置构建、分析动作,并把内容直接合并到Chunk,提升构建速度,比如vue.runtime.esm.js、lodsah.js
但这里需要注意的是:
由于跳过了AST分析,因此无法在文件中发现可能的语法错误,直到压缩甚至运行的时候才发现问题,所以必须确保所匹配的文件的内容的正确性。
由于跳过了依赖分析过程,所以文件的依赖无法进行分析,所以此文件不能依赖其他模块,否则会出现错误。
并且跳过了内容分析过程,因此无法无法实现树摇,从而减小产物的体积。比如第三方库有通过判断环境,来加载min版本还是unbundle版本,如果使用此配置项,那么将会在chunk中打包进两份一样的代码。
所以使用此配置项,需要慎重考虑以上几点,需要使用的库是否符合,以及带来的性能提升和需要承担的风险是否一致。
设置resolve
同大多数构建工具一样,webpack提供了一套同时兼容 CMD、AMD、ESM 等模块化方案的资源搜索规则,enhanced-resolve,能将各种模块导入语句准确定位到模块对应的物理资源路径:
import 'lodash' 这一类引入 NPM 包的语句会被 enhanced-resolve 定位到对应包体文件路径 node_modules/lodash/index.js
import './a' 这类不带文件后缀名的语句,则可能被定位到 ./a.js 文件
import '@/a' 这类化名路径的引用,则可能被定位到 $PROJECT_ROOT/src/a.js 文件
但这里需要注意的是,如果导入的语句并没有携带后缀,webpack就会遍历resolve.extensions ,尝试在路径追加后缀名,搜索对应物理文件,同时,webpack5中resolve.extensions 默认值为 ['.js', '.json', '.wasm'] ,那么如果是一个ts文件,针对extensions进行配置的时候,如果配置不当,可能会引起多次判断才能完成文件搜索。那么我们就需要进行优化了:
修改extensions配置项,减少匹配次数
代码中尽量补齐文件名称
设置 resolve.enforceExtension = true,这种方式属于强制更改项目规则,针对与多人协同项目弊大于利,不推荐。
我们还记得,在import第三方包的规则中,如果当前项目并没有所需资源,会逐层尝试,如果还找不到,就在全局进行搜索,这个兜底逻辑是没有问题的,但在一个良好的业务中,第三方依赖一般出于当前项目中,因此我们可以通过修改resolve.modules配置,关闭逐层搜索功能。
在实际项目中,我们甚至可能遇到使用resolve.mainFiles的情况,这个配置属于自定义文件夹的默认文件名,比如resolve.mainFiles = ['index', 'home'],import './a' 请求,会依次测试./a/index 与 ./a/home 文件是否存在,所以这个配置项同resolve.extensions 类似,应该尽可能减少其中的数量。
善用exclude
loader组件一般需要处理对应资源文件,但有些资源文件,经过我们的确认,认为不需要这些loader进行加载,比如node_modules中的文件,因此,我们可以根据开发场景,通过rules中的include和exclude等配置,限定loader的执行范围,通常排除node_modules文件夹。
但是,如果node_modules中,的确有我们需要经过loader的模块怎么办呢?
此时include和exclude还支持类似MongoDB参数风格的值,也就是通过and、not、or等属性组合过滤逻辑。
比如我想把lodash加入loader逻辑中,但还要排除node_modules文件。
使用这种能力,我们可以适当将部分需要转译处理的 NPM 包(例如代码中包含 ES6 语法)纳入 Loader 处理范围中。
TS优化
JavaScript 本身是一门弱类型语言,这在多人协作项目中经常会引起一些不必要的类型错误,影响开发效率。随前端能力与职能范围的不断扩展,前端项目的复杂性与协作难度也在不断上升,TypeScript 所提供的静态类型检查能力也就被越来越多人所采纳。
不过,类型检查涉及 AST 解析、遍历以及其它非常消耗 CPU 的操作,会给工程化流程带来比较大的性能负担,因此我们可以选择关闭 ts-loader 的类型检查功能,将transpileOnly设置为true。
但这样子带来了弊端,那就是ts的类型检查就没有用了,因此我们需要用其他方式实现ts的类型检查,而非启动的时候。
vue-cli的ts插件,就依赖了fork-ts-checker-webpack-plugin插件,将类型检查剥离到子进程执行,这样即获得了ts的类型检查能力,有提升了整体编译速度。
小结
我们在这里了解了webpack的核心配置,然后了解到了webpack提出来的模块联邦、持久化缓存、懒构建,然后通过进行合理配置的方式,来优化webpack的打包流程和速度,不过,这并非webpack的全部,甚至,我们到目前为止,仅仅了解到了webpack的皮毛,我们将以这个作为新的基础,来拓展我们学习webpack的眼界,规划学习的路线。
这并非我们的终点,而是我们新的起点。