Webpack详解
一、为什么需要Webpack?
1. 背景
在最早的开发中,如果需要引入一个模块,那就需要借助script,就像下面这样:
1 |
|
这样做有一个致命的问题,那就是这些模块都是在全局工作的,这就导致模块成员之间会互相污染,模块与模块之间也看不出依赖关系,导致维护起来特别困难。于是后面就有人提出采用命名空间的方式,也就是每个模块只暴露一个全局成员,并挂在到window下面。
1 |
|
但这里还有一个问题,那就是这样的方式无法得知这个函数是依赖了哪些外部库,于是有人想到了用自执行函数来传入依赖库,比如下面这样
1 |
|
然而这样做依旧不够方便。于是有人就提出了一整套模块化规范,目前最流行的模块化规范有两套,即CommonJS(module.exports导出/require导入)和ES Modules(export导出/import导入)。
随着前端项目越来越大,前端开发也变得十分复杂,这时就出现了以下问题:
- 需要通过模块化的方式来开发功能
- 使用一些高级特性来加快开发效率,比如ES6+、TypeScript、Sass/Less等
- 监听文件变化并且反映到浏览器上,提高开发效率
- JS模块化之后,CSS跟HTML同样也需要模块化
- 开发完成之后需要对代码进行压缩、合并以及其他相关优化
为了解决这些问题,Webpack就诞生。Webpack是一个模块打包工具,开发者可以很方便地使用Webpack来管理依赖,并编译出模块们所需要的静态文件
2. Webpack
Webpack在处理程序的时候,会先将ES6的代码转化ES5代码,解决浏览器兼容性问题。接着会在内部生成构建一个依赖图,此依赖图对应映射到项目生成的每个模块,并生成一个或多个bundle.js,减少请求的次数。最后再对CSS,图片进行整合。
总的来说,Webpack起到了模块打包、编译兼容、能力扩展等功能,其中编译兼容就是借助Loader来实现的,而能力扩展是借助Plugin来实现的。
3.配置Webpack的基本要素
- entry(入口)
入口是关系依赖图的开始,Webpack从入口开始寻找依赖,打包构建。
Webpack允许有多个入口。
配置示例如下:
1 |
|
- output(输出)
用于指定最终输出文件的名称及位置
1 |
|
loader(加载器):用来翻译非JavaScript的文件,如CSS、HTML、图片等,具体参考下文
plugin(插件):用来扩展Webpack的功能,基础Tapable事件流框架,能够监听Webpack的生命周期函数,具体参考下文
mode(模式):用来区分开发环境跟生产环境
配置如下:1
2
3
4module.exports = {
mode: 'development' // 开发环境
mode: 'production' // 生产环境
}resolve(解析):用于设置模块如何解析,具体参考下文的优化章节
optimization(优化):Webpack内置的优化配置,一般用于生产模式提升性能。
常用配置如下:minimize:是否需要压缩bundle
minimizer:压缩工具。如TerserPlugin、OptimizeCSSAssetsPlugin
splitChunk:拆分bundle
runtimeChunk:是否需要将所有生成的chunk之间共享的运行时文件拆分出来
配置示例如下:
1 |
|
二、构建流程
Webpack构建主要有三大流程:
- 初始化阶段
从webpack.config.js中读取配置参数,启动webpack,加载所有Plugin,创建Compiler对象。
执行run函数,创建一个Compilation对象,这个对象包含了本次构建的所有数据。
- 编译构建阶段
从Entry出发,调用Loader对模块进行翻译,最终转为JavaScript文件
找到所有依赖的模块,递归解析,最终生成依赖关系树,并通过acorn库生成AST树
- 输出阶段
对编译后的Module组合成Chunk,把Chunk转换成文件,输出到文件系统中。最终打包出来的就是一个bundle.js,这个bundle是一个IIFE(自执行函数)
1. 初始化阶段
Webpack的用户配置一般都保存在webpack.config.js之中,常见样式如下:
1 |
|
Webpack在拿到webpack.config.js之后,会将其中的各个配置拷贝到options对象之中,然后加载用户配置的plugin。最后初始化Compiler编译对象,该对象一个全局单例,掌控着Webpack的生命周期。
1 |
|
2. 编译构建阶段
通过Compiler对象加载所有插件,执行run方法进行编译。主要编译流程如下:
- compile:开始编译
- make:从入口点分析模块及其依赖的模块,创建这些模块对象
- build-module:构建模块
- seal:封装构建结果
- emit:把各个chunk输出到结果文件
1)compile编译
执行run之后,首先触发compile方法,这个方法会构建一个compilation对象。该对象是编译阶段的主要执行者,它包含了当次构建所需要的信息。会依次执行以下流程:
- 执行模块创建
- 依赖收集
- 分块
- 打包
2)make编译模块
当完成compilation对象之后,就开始从entry入口文件开始读取代码,主要执行的是_addModuleChian函数,大致代码如下:
1 |
|
_addModuleChain接收参数dependency传入的入口依赖,使用对应的工厂函数ModuleFactory.create生成一个空的module对象。回调会把这个module对象存到compilation.modules和dependencies。由于是入口文件,还会被存到compilation.entries之中。最后执行buildModule进入真正构建模块内容的阶段。
3)build module完成模块编译
这个阶段主要调用开发者配置的loaders,将CSS、Html等模块转换成标准的JS模块。接着使用Acorn(一个解析JS代码的解析器)输出对应的AST树。
从AST树的入口开始,寻找所有引入语句,如require、import等。接着逐渐递归深入,最后生成一份依赖收集表,彻底搞清楚依赖关系
4)seal和emit输出
seal方法主要生成chunk,对chunks进行一系列优化操作,最终输出代码。这个chunk可以理解为模块。
根据入口与模块之间的关系,组装出一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出列表。在确定好输出内容之后,根据配置确定输出的路径和文件名即可,即以下这部分:
1 |
|
在Compiler开始生成文件之前,会触发emit钩子函数,这是我们最后一次修改处理结果的机会。
在Webpack5中,最终输出的bundle.js会像下面这样展示:
1 |
|
相比Webpack4,Webpack5做了相当的精简。整个bundle里面只有三个变量和一个函数方法。
其中**webpack_modules_**存放了编译后各个文件模块的内容。
**webpack_module_cache**用来做模块缓存。
**webpack_require**是Webpack内部实现的一套依赖引入函数。它会把我平常使用的ES Module或者CommonJS导入导出规范,替换成自己的__webpack_require__,以此来抹平不同规范之间的差异性。
最后一句是代码运行的起点,从入口开始,启动整个项目。
整个流程如下图所示:
三、Loader
1. 什么是Loader
Loader的本质是一个函数,在该函数对接收的内容进行转换,最终返回转换后的结果。由于Webpack只认识JavaScript,所以需要Loader充当翻译官,对其他类型的资源进行转译的预处理工作
2. 如何配置Loader
配置Loader常见的有三种方式:
- 配置方式(推荐),在webpack.config.js中配置Loader
- 内联方式,在每个import语句中显式指定Loader
- Cli方式,在Shell命令中指定它们
在配置方式中,我们通常将Loader写在module.rules之中,示例代码如下:
1 |
|
从上面的例子可以看出,rules是一个数组,里面存放着多个匹配规则,而用来匹配的属性叫做test,这个属性是通过正则表达式来匹配的。接着通过use来决定使用哪些loader。
3. Loader的特性
从上一段示例代码可以看到,同一个test匹配多个Loader,这就涉及到Loader的特性:链式调用,每个Loader都会处理前一个Loader返回的结果。值得注意的是,这个处理顺序是反着来的,也就是按sass-loader -> css-loader -> style-loader这个顺序来处理。
如果想要指定Loader的顺序,可以使用enfore属性。另外还有pre和post,pre代表在所有Loader之前执行,post则在所有Loader之后。
4. 常用Loader
以下列出常见的Lodaer:
- style-loader:将css添加到DOM的内联样式标签style里,然后通过DOM操作去加载css
- css-loader:允许css通过require的方式引入,并返回css代码
- less-loader:处理less代码,将less代码转换成css代码
- sass-loader:处理sass代码,将scss/sass代码转换成css代码
- file-loader:分发文件到output目录并返回相对路径
- url-loader:和file-loader类似,但是当文件小于设定的limit时可以返回一个DataUrl
- html-minify-loader:压缩HTML
- babel-loader:用babel来转换ES6文件到ES
- awesome-typescript-loader:将TypeScript代码转换成JavaScript,性能优于ts-loader
- eslint-loader:通过eslint检查JavaScript代码
- tslint-loader:通过tslint检查TypeScript代码
- cache-loader:可以在一些性能开销比较大的Loader之前添加,将结果缓存到磁盘中,下次无需重新计算
下面以css-loader为例,说明具体如何配置一个Loader
在配置之前,首先需要安装插件:
1 |
|
接着将规则配置到module.rules之中即可:
1 |
|
5.自定义Loader
Loader本质上是一个函数,但要特别注意的是它的this作为上下文会被Webpack填充,所以不能使用箭头函数,因为箭头函数无法改变this的指向。
Loader函数接收一个参数,Webpack会将原内容通过这个参数传递给Loader。
Loader有四种类型:
- 同步loader
一般loader都是同步的,可以像下面这样定义:
1 |
|
也可以通过this.callback返回,将return设定为undefined来告诉Webapck去callback中寻找结果。
this.callback的参数如下:
1 |
|
接下来展示一个使用实例:
1 |
|
- 异步loader
异步主要就是调用this.async来实现异步构建,具体代码如下:
1 |
|
- Raw Loader
传入参数content默认为UTF-8字符串,如果希望传入的是Buffer,那就需要设置一下raw。具体代码如下:
1 |
|
- Pitching Loader
这个有点类似冒泡捕获这个概念,一般我们loader是从右往左加载的,但是在此之前还有一个从左往右的pitch过程。
就像下图这样:
可以看到,先通过pitch逐个进行,再通过loader调用往回走。
这里的pitch有三个参数:
- remainingRequest:保存着loader链中排在自己后面的loader的绝对路径,以**!**作为字符串的连接符
- precedingRequest:保存着loader链中排在自己前面的loader的绝对路径,以**!**作为字符串的连接符
- data:每个loader存放的上下文字段,pitch可以通过这个传递数据给loader。
比如下面这段代码:如果我们在pitch中直接return,loader链会直接跳过后面部分,直接回头。1
2
3
4
5
6
7module.exports = function(content) {
return someSyncOperation(content, this.data.value)
}
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = 42
}
比如下面这段代码:这会导致下图这个结果:1
2
3
4
5
6
7module.exports = function(content) {
return someSyncOperation(content)
}
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
return '打断loader链'
}
也就是当开发者在b pitch中使用了return,那么就会直接回到a loader的调用上。不会执行后面的b loader和c loader
其他loader的API:
- this.addDependency:加入一个文件监听,一旦文件产生变化,就会调用这个loader进行处理
- this.cacheable:默认情况下loader的处理结果会有缓存,给这个方法传入false可以关闭效果
- this.clearDependencies:清除loader的所有依赖
- this.context:文件所在的目录(不包含文件名)
- this.data:pitch阶段和正常调用阶段共享的对象
- **this.getOptions()**:用来获取loader的参数选项
- this.resolve:像require表达式一样解析一个request。resolve(context: string, request: string, callback:function(err, result: string))
- this.loaders:所有loader组成的数组,它在pitch的时候可以写入
- this.resource:获取当前请求路径,包含参数:’/abc/resource.js?rrr’
- this.resourcePath:不包含参数的路径:’/abc/resource.js’
- this.sourceMap:bool类型,是否应该生成一个sourceMap
6.如何使用自定义的loader
主要有两种,一种是通过Npm link的方式。另一种是直接在项目中通过路径配置的方式。
这里主要将第二种,而这第二种又分为两种情况:
- 匹配单个loader
直接在rule中通过path.resolve来指定路径
1 |
|
- 匹配多个loader
可以使用resolveLoader.modules进行配置。Webpack将会从这些目录中搜索这些loaders。具体代码如下:
1 |
|
四、Plugin
1. 什么是Plugin
Plugin是基于Tapable事件流框架,在Webpack运行的生命周期中会广播许多事件,而Tapable可以通过监听钩子函数来获取这些事件,在合适的时机通过Webpack提供的API改变输出结果。Plugin常用来进行打包优化、资源管理、环境变量注入等工作。
使用的时候,先引入该plugin,然后在plugin属性中new一个实例即可
例如这样:
1 |
|
2. Plugin的特性
在Plugin中有一个apply方法,这个apply会被Compiler对象调用,并且Compiler会作为参数传入到apply里面,这意味着开发者可以访问到Compiler的各个时期的钩子函数。
1 |
|
其中run就是构建开始时的钩子函数,其他钩子函数可以参考官方文档
3. 常见的Plugin
常见的Plugin有下列这些:
- ignore-plugin:忽略部分文件
- html-webpack-plugin:简化HTML文件创建(依赖于html-loader)
- web-webpack-plugin:可以方便地为单页面应用输出HTML,比html-webpack-plugin好用
- terser-webpack-plugin:压缩代码,支持ES6
- webpack-paralle-uglify-plugin:多进程执行代码压缩,提升构建速度
- clean-webpack-plugin:自动清理目录
- speed-measure-webpack-plugin:可以看到每个Loader和Plugin的执行耗时
插件展示的大致内容如下图:
具体配置如下:
1 |
|
- webpack-bundle-analyzer:可视化Webpack文件输出的体积
配置如下:
1 |
|
实际上webpack-bundle-analyzer也是通过分析stats.json来得出数据的。直接生成stats.json的指令为:
1 |
|
- wepack-dashboard:可以更好的展示打包信息
- webpack-merge:提取公共配置,减少重复代码
- size-plugin:监控资源体积的变化
- progress-bar-webpack-plugin:用来查看编译的进度情况的
配置如下:
1 |
|
其中包含了内容、进度条、百分比以及用时,效果如下:
4. 自定义Plugin
Webpack编译的时候会创建两个对象:Compiler和Compilation
- Compiler:包含Webpack环境的所有配置信息,包括options、loader和plugin,以及Webpack整个生命周期的钩子
- Compilation:作为Plugin内置事件回调函数的参数,包含当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一个新的Compilation将被创建
自定义Plugin需要遵循以下规范:
- 插件要么是一个函数,要么必须包含是一个apply方法的对象,这样才能访问到Compiler实例
- 传给每个插件的Compilation和Compiler对象是同一个引用,不建议修改
- 异步事件需要在插件处理完任务时调用callback通知Webpack进入下一个流程,不然会卡住
下面是一个自定义Plugin的模板:
1 |
|
如果要用到异步,就需要使用tapAsync或者tapPromise
- tapAsync
使用tapAsync的时候,需要传入一个callback回调,告知Webpack这是段异步操作。
具体代码如下:
1 |
|
- tapPromise
使用tapPromise的时候,我们则需要返回一个Promise。
具体代码如下:
1 |
|
六、热更新
1.什么是热更新
热更新又称为热替换(Hot Module Replacement),缩写为HMR。这个机制可以做到修改本地文件之后,不需要刷新页面或者重启服务器,就能达到更新的效果。
2.如何开启
在webpack.config.js添加如下配置:
1 |
|
默认情况下,热更新只更新css部分,如果想要更新其他资源,比如js文件,需要做以下设置:
1 |
|
3.实现原理
首先来看一下HMR的架构图:
这里涉及到这些概念:
- Webpack Compile:将JS源代码编译成bundle.js
- HMR Server:用来将热更新的文件输出给HMR Runtime
- Bundle Server:静态资源文件服务器,提供文件访问路径
- HMR Runtime:socket服务器,会被注入到浏览器,更新文件变化
- 在HMR Runtime和HMR Server之间建立websocket,用于实时更新文件变化
整个流程我们可以分为两个阶段:启动阶段和更新阶段
启动阶段的工作是:
Webpack Compile将源代码和HMR Runtime一起编译成bundle文件,传输给Bundle Server静态资源服务器。
也就是express,这个express是随着WDS(Webpack Dev Server)启动的。
接着是更新阶段:
当webpack启动的时候,webpack会给编译后的文件生成一个唯一的hash值。
当发生更新的时候,HMR Server会向浏览器推送一条消息,消息包含文件改动后生成的hash。
浏览器接收到消息之后,就会创建一个ajax去请求变化的内容。
HMR Server会根据变化的内容生成两个补丁文件:manifest和chunk.js模块
其中manifest包含hash和chunkId,hash用来跟本地记录的hash值进行对比,chunkId则代表了变化模块的Id。
而chunk.js包含变化的内容。
浏览器的HMR Runtime根据manifest文件获取模块变化的内容,从而触发render函数,实现局部更新。
七、Proxy的工作原理
在开发中,我们不可避免会遇到跨域的问题,而Webpack在本地开发的环境下恰好提供了一套解决方案,那就是使用Proxy。
所谓Proxy,就是把发送给远程服务器的请求发给一个本地的服务器,让本地服务器代为转发,因为跨域规则只在浏览器中存在,本地服务器并没有跨域问题,并且本地服务器跟浏览器是同源的。
而要使用Proxy,就需要用到Webpack提供的本地开发工具,即Webpack Dev Server
1.什么是Webpack Dev Server
Webpack Dev Server是Webpack官方推出的一款开发工具,特点是能够自动编译和自动刷新浏览器,为开发提供了方便。其配置如下:
1 |
|
这里面的proxy属性就是我们说的代理,它可以将/api路径下面的请求转发到target指定的Url上,从而实现跨域的目的。
另外还可以给/api添加以下属性:
- target:目标地址
- pathRewrite:默认情况下,会把/api带上,如果希望修改或者删除,可以使用该属性
- secure:支持https请求
- changeOrigin:是否更新代理后请求的headers中host地址
2.Proxy的原理
proxy本质上是利用了http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器的功能。
我们可以通过下面的代码实现相似的功能:
1 |
|
八、Mock接口
Webpack中的Mock接口主要是利用了自定义中间件devServer.setupMiddleware,在middleware.unshift中的回调函数使用res.send将mock数据传递回去。
具体配置如下:
1 |
|
九、优化性能
1.JS代码压缩
TerserPlugin是Webpack内置的JS代码压缩工具,它的配置如下:
1 |
|
TerserPlugin常见的属性:
- extractComments:默认值为true,表示会将注释抽离到一个单独的文件中
- parallel:使用多进程并发运行提高构建速度,默认值为true,并发运行的数量可通过以下代码获取
1
os.cpus().length - 1
- terserOptions:设置Terser的相关配置
- compress:设置压缩相关选项
- mangle:设置丑化相关选项
- toplevel:底层变量是否进行转换
- keep_classnames:保留类的名称
- keep_fnames:保留函数的名称
2.CSS代码压缩
CSS压缩通常就是去掉一些无用的空格换行等,我们可以使用插件:css-minimizer-webpack-plugin。配置如下:
1 |
|
还可以使用插件optimize-css-assets-webpack-plugin,其默认使用的压缩引擎是cssnano。配置如下:
1 |
|
擦出无用的CSS可以使用PurgeCSS,配置如下:
1 |
|
3.HTML代码压缩
使用HtmlWebpackPlugin插件来生成HTML模板,主要通过属性minify来设置:
1 |
|
4.文件大小压缩
文件压缩可以使用插件:compression-webpack-plugin。配置如下:
1 |
|
5.图片压缩
图片压缩是体积优化的大头,可以使用file-loader跟image_webpack-loader这两个插件,配置如下:
1 |
|
6.Tree Shaking
Tree Shaking的作用是消除无用的代码,依赖于ES Module的静态语法分析。在Webpack中有两种实现Tree Shaking的方案:
- usedExports:通过标记函数是否使用,之后通过Terser进行优化
usedExposrts的配置如下:
1 |
|
- sideEffects:跳过整个模块/文件,看看是否有副作用
这里的副作用指的是除了module.exports之外做的事情,如果有哪些有副作用,但又不想被删除的代码,可以通过以下配置来避免删除:
1 |
|
7.代码分离/分割
代码分离用于将首包用不到的代码分开打包,减少首包的体积。通常使用splitChunkPlugin插件来实现,配置如下:
1 |
|
splitChunk有如下属性:
- Chunks:对同步还是异步代码进行处理
- minSize:拆分包的大小,不小于minSize,如果这个包小于minSize,则不会被拆分
- maxSize:将大于maxSize的包,拆分为不小于minSize的包
- minChunks:被引入的次数,默认是1
8.内联chunk
可以使用InlineChunkHtmlPlugin将一些代码量不大,但是必须加载的chunk模块内联到html中。
如runtime的代码、对模块进行解析,加载以及模块信息相关的代码。
配置如下:
1 |
|
9.提取公共资源
使用html-webpack-externals-plugin,将基础包通过CDN引入,不打入bundle中
10.合理使用sourceMap
sourceMap是一项将编译、打包、压缩之后的代码映射回源代码的技术。由于打包后的代码并没有可读性,一旦遇到报错,直接在混淆后的代码上debug是一件十分痛苦的事情,因此这个时候就需要sourceMap来告知我们这个报错在源代码的什么地方。
既然是一种映射,那就肯定有一份映射文件来标记源代码的位置,这份文件通常以.map命名,里面的数据结构大概长这样:
1 |
|
其中mappings数据就是混淆后的函数名,它有一套规则:
- 每个组用分号分割
- 每一段用逗号分隔
- 每个段由1、4、5个可变长度字段组成
有了这份文件,我们在我们压缩代码的最后端加上这句注视,即可让sourceMap生效:
1 |
|
sourceMap映射表有一套较复杂的规则,具体可以参考以下两篇文章:
Source Map的原理探究
Source Maps under the hood – VLQ, Base64 and Yoda
开发环境一般将sourceMap设置为eval-cheap-module-source-map,如下配置:
1 |
|
生产环境一般有三种方案:
- hidden-source-map:借助第三方监控平台Sentry使用
- nosources-source-map:只会显示具体行数以及查看源代码的错误栈,安全性比sourcemap高
- sourcemap:通过nginx设置将.map只对白名单开放(公司内网)
十、提高构建速度
1.优化Loader配置
在使用Loader的时候,可以用include和exclude来指定匹配哪些文件,加快构建速度,配置如下:
1 |
|
2.合理使用resolve.extensions
当import一个模块的时候,我们一般需要写出文件的后缀名,Webpack才能知道这是什么文件。
而extensions可以帮助我们省略后缀名,比如我们这样写:
1 |
|
这个时候file是一个js文件,那我们可以给extensions配置一个数组,Webpack就会从数组中查找可能的后缀名,从而识别改文件
具体配置如下:
1 |
|
3.优化resolve.modules
resolve.modules用于指明去哪寻找第三方模块,默认值为[‘node_modules’]。
1 |
|
4.优化resolve.alias
alias可以给路径起别名,减少查找的过程,配置如下:
1 |
|
5.使用DLLPlugin插件
DLL全称动态链接库,是Windows实现共享函数库的一种方式,而Webpack也内置了打包DLL的功能,可以将不常改动的代码抽成一个共享库,就不需要反复编译这个库了。
这里就涉及两个问题:如何打包成DLL和如何使用
- 如何打包成DLL
新建一个webpack_dll.config.js专门用来打包DLL
配置如下:
1 |
|
其中manifest.json代表模块的映射关系,解析的时候需要用到
需要注意的是DllPlugin的name属性必须跟output.library保持一致,并且生成的manifest文件也会用到output.library
最终构建出的文件:
1 |
|
- 如何使用
首先要用到Webpack自带的DllReferencePlugin对映射文件manifest.json进行分析,获取要使用的Dll库。然后通过AddAssetHtmlPlugin将Dll库引入Html模块。
配置如下:
1 |
|
6.开启缓存
- babel-loader开启缓存
将cacheDirectory设为true即可,配置如下:
1 |
|
- terser-webpack-plugin开启缓存
给cache属性设为true即可,配置如下:
1 |
|
- 使用cache-loader
有一些开销比较大的loader,我们可以把运行结果缓存在磁盘中,下次就不需要重新计算,实现这个功能的便是cache-loader。
具体配置如下:
1 |
|
这个功能在Webpack5已经实现了内置,我们只需要进行如下配置
1 |
|
7.terser启动多线程
这个在前面介绍TerserPlugin插件的时候说过,具体配置如下:
1 |
|
8.升级版本
Webpack4的构建速度比Webpack3快了60%-98%
Webpack4带来的优化:
- v8引擎带来的优化,如:for of替代forEach、Map和Set替代Object、includes替代indexOf
- 默认使用更快的md4 hash算法
- webpack AST可以从loader直接传递给AST,减少解析时间
- 使用字符串方法替代正则表达式
9.区分生产环境跟开发环境的配置
一般常用的webpack配置有以下三种:
- webpack.dev.js:开发环境
- webpack.prod.js:生产环境
- webpack.common.js:公共配置
然后安装插件webpack-merge来整合这些环境。
比如说在webpack.dev.js中整合webpack.common.js,就可以这样设置:
1 |
|
10.多进程构建
thread-loader是Webpack官方推出的一个多进程方案,用来替代HappyHack。
原理和HappyHack相似,webpack每解析一个模块,thread-loader会将它的依赖分配给worker线程中,从而达到多进程打包的目的。
具体配置如下:
1 |
|
11.使用noParse
如果有些第三方库没有依赖库,像JQuery。那就可以用module.noParse指定不用解析。具体用法如下:
1 |
|
十一、Chunk
1.什么是Chunk
Chunk在Webpack里面指一个代码块,是多个module的集合,也是多个bundle的集合。
2.Chunk与module的关系
Chunk是多个module的集合,当开发者在entry配置一个入口文件,Webpack就会寻着这个入口文件找到所有相关模块,最终打包成一个Chunk。多个entry就有多条Chunk。
3.Chunk与bundle的区别
通常我们会以为bundle等于Chunk,但它们其实是有区别的。在大多数情况下,Webpack构建完成之后只生成一个bundle.js,这让我们误以为bundle就是Chunk。
但当我们把devtool设置为source-map时,就会产生一份sourcemap文件。而这份sourcemap文件也属于一个Chunk里面的,如下图:
也就是说,Chunk包含bundle
4.产生Chunk的途径
- entry产生Chunk
entry的配置有三种方式:
- 传递一个字符串
比如这样:
1 |
|
这种情况只会产生一个Chunk
- 传递数组
像这样:
1 |
|
这种情况也只会产生一个Chunk。Webpack会将数组里的多个文件都打包成一个bundle。
- 传递对象
1 |
|
这里一个字段会产生一个Chunk,所以在output.filename中写死名称会报错,需要用[name]来区分不同的Chunk
- 异步产生的Chunk
异步加载的模块,也会单独生成一个Chunk,比如下面这样:
1 |
|
这里的chunkFileName就是用来定义异步加载生成的文件名称。
- 代码分割产生的Chunk
来看看以下代码会产生几个Chunk,其中main.js和two.js都引用了greeter.js,main.js使用了react.js:
1 |
|
答案是五个。
两个入口各一个
runtimeChunk: ‘single’会将Webpack浏览器端运行时需要的代码抽离到一个文件。
common配置下会产生一个Chunk
vendor配置下会产生一个Chunk。
具体如下图:
十二、FAQ
1.模块打包的原理是什么?
Webpack实际上为每个模块创造一个导出和导入的环境,并没有改变代码的执行逻辑,代码执行顺序和模块加载顺序跟原来一样。
2.如何对bundle体积进行监控和分析?
VSCode中有一个插件叫Import Cost,可以帮助我们对引入模块的大小进行实时地监控。
还可以使用webpack-bundle-analyzer生成bundle的模块组成图,显示所占的体积。
bundlesize工具包可以进行自动化资源体积管理
3.文件指纹是什么?怎么用?
文件指纹指的是文件名中包含的那串hash码,一般用来确定文件的唯一性。
- JS文件指纹设置
1 |
|
- CSS文件指纹设置
这里用到了插件MiniCssExtractPlugin
1 |
|
- 图片文件指纹设置
这里用到了file-loader,具体配置如下:
1 |
|
4. Babel的原理是什么?
大多数JavaScript Parser都遵循estree的规范,Babel最初基于acorn项目
Babel大概分为三个部分:
- 解析:将代码转成AST
- 词法分析:将代码分割为token流,即语法单元组成的数组
- 语法分析:分析token流,并生成AST
- 转换:访问AST节点进行变换操作生成新的AST
- Taro就是利用babel完成小程序的语法转换
- 生成:以新的AST为基础生成代码
十三、Webpack的简易实现
由于Vue遵循的是ES Module规范,而NodeJS遵循的是CommonJS规范。这些都不是所有浏览器都支持的,因此Webpack的主要工作就是将这些规范转换成浏览器可以理解的方式,最后使用<script>标签加载。
比如说有下面这三个文件:
src/index.js
1 |
|
src/a.js
1 |
|
src/b.js
1 |
|
执行npx webpack –mode development打包产出dist/main.js文件,如下图
这里面主要就三个属性一个函数。
第一段__webpack_modules__中,require被替换成__webpack_require__,这是Webpack内部实现的导入导出环境。而其他部分代码基本是不变的。
第二段的__webpack_module_cache,用来将缓存__webpack_require__加载的模块,加快二次运行的速度,也能避免加载两个互相引用的模块。
第三段就是__webpack_require__的实现。
最后一段就是入口文件的加载。
因此如果我们想实现一个Webpack,其核心工作就是找到所有依赖,然后将相关的导入导出语句给替换成Webpack自己的语句即可。最终生成类似上图的代码。
那么接下来就来动手实现一个简单的Webpack。
首先梳理一下思路,要做的事情有下面这些:
- 读取入口文件,并收集依赖信息
- 递归地读取所有依赖模块,产出完整的依赖列表
- 将各模块内容打包成一份完整的可运行的代码
一开始肯定需要创建一个项目,这个就不多说了。
接着安装依赖:
1 |
|
其中:
- @babel/parser:用于解析代码,生成AST
- @babel/traverse:用于遍历AST,将require语句改为_require_,将引入路径改为相对根路径
- @babel/core:用于将修改后的AST转换成新的代码输出
创建一个入口文件myPack.js并引入依赖紧接着,我们需要对某一个模块进行解析,并产出模块信息。包括模块路径、模块依赖、模块转换后的代码1
2
3
4
5const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
具体代码如下:接下来,从入口出发递归找到所有模块,并构建依赖树:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 保存根路径,所有模块根据根路径产出相对路径
let root = process.cwd()
function readModuleInfo(filePath) {
// 准备好相对路径作为module的key
filePath = './' + path.relative(root, path.resolve(filePath).replace(/\\+/g, '/'))
// 读取源码
const content = fs.readFileSync(filePath, 'utf-8')
// 解析源码,生成AST
const ast = parser.parse(content)
// 用于存储依赖的模块
const deps = []
traverse(ast, {
CallExpression: ({ node }) => {
// 如果是require语句,就收集依赖
if (node.callee.name === 'require') {
// 改造require关键字
node.callee.name = '_require_'
let moduleName = node.argument[0].value
moduleName += path.extname(moduleName) ? '' : '.js'
moduleName = path.join(path.dirname(filePath), moduleName)
moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
deps.push(moduleName)
// 改造依赖路径
node.argument[0].value = moduleName
}
}
})
// 编译回代码
const { code } = babel.tranformFromAstSync(ast)
return {
filePath,
deps,
code
}
}经过上一步,我们已经得到依赖树,接下来就是按照目标格式进行打包输出即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function buildDependencyGraph(entry) {
// 获取入口模块信息
const entryInfo = readModuleInfo(entry)
// 项目依赖树
const graphArr = []
graphArr.push(entryInfo)
// 从入口出发,递归查找所有依赖模块,保存到graphArr中,形成依赖树
// 原文这个写法有问题,不是真正的递归
for (const module of graphArr) {
module.deps.forEach(depPath => {
const moduleInfo = readModuleInfo(path.resolve(depPath))
graphArr.push(moduleInfo)
})
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30function pack(graph, entry) {
const moduleArr = graph.map(module => {
return {
`"${module.filePath}": function(module, exports, _require_) {
eval(\`` +
module.code +
`\`)
}`
}
})
const output = `;(() => {
var module = {
${moduleArr.join(',\n')}
}
var modules_cache = {}
var _require_ = function(moduleId) {
if (modules_cache[moduleId]) return modules_cache[moduleId].exports
var module = modules_cache[moduleId] = {
exports: {}
}
modules[moduleId](module, module.exports, _require_)
return module.exports
}
_require_('${entry}')
})()`
return output
}
最后编写一个入口函数main用以启动打包进程
1 |
|
参考:
Webpack常见面试题总结
使用 Acorn 来解析 JavaScript
当面试官问Webpack的时候他想知道什么
「吐血整理」再来一打Webpack面试题
浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)
webpack 最佳实践
学习 Webpack5 之路(基础篇)
Webpack 理解 Chunk
require.ensure简单理解
学习 Webpack5 之路(优化篇)
90 行代码的webpack,你确定不学吗?
Webpack - 手把手教你写一个 loader / plugin
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!