webpack相关汇总
# 流程
# webpack与gulp、grunt的区别
Grunt:大量现成的插件封装了常见的任务(Task),也能管理任务之间的依赖关系,自动化执行依赖的任务 Gulp:是一个基于流(Stream)的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。 Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:【WRTSD】
- 通过 gulp.task 注册一个任务;
- 通过 gulp.run 执行任务;
- 通过 gulp.watch 监听文件变化;
- 通过 gulp.src 读取文件;
- 通过 gulp.dest 写文件。
Webpack:webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。
总括:
- 从构建思路来说
gulp和grunt需要开发者将整个前端构建过程拆分成多个
Task
,并合理控制所有Task
的调用关系; webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工; - 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路
# 与其他类似工具比较
从应用场景上来看:
webpack
适用于大型复杂的前端站点构建;rollup
适用于基础库的打包,如vue、react;parcel
适用于简单的实验性项目,他可以满足低门槛的快速看到效果;由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel
# Webpack构建流程简单说一下
# 工作流程
webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现的核心就是 tapable
webpack中最核心的负责编译的Compiler
和负责创建bundles的Compilation
都是Tapable的实例。
# 简述【要点】
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
# 完整流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数
:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数开始编译
:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口
:根据配置中的 entry 找出所有的入口文件编译模块
:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译
:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源
:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成
:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
# webpack的流程
- 初始化参数:从配置文件和Shell语句中读取和合并参数;
- 开始编译:用上一步得到的初始化Compiler对象,加载所有配置的插件,执行对象的run的方法开始执行编译;
- 确定入口:根据配置中的entry找到所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的Loader 对模块进行编译,再找出该模块依赖的模块,在递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
# 编译钩子顺序
按照下面的钩子调用顺序执行。
- before-run 清除缓存
- run 注册缓存数据钩子
- before-compile
- compile 开始编译
- make 从入口分析依赖以及间接依赖模块,创建模块对象
- build-module 模块构建
- seal 构建结果封装, 不可再更改
- after-compile 完成构建,缓存数据
- emit 输出到dist目录
# loader&plugin
# 那你再说一说Loader和Plugin的区别?【要点】
# 原理方面
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
# 配置方面
Loader
在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
示范
module: { //启用happypack多核构建项目
rules: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// loader: 'babel-loader?cacheDirectory',
loader: 'happypack/loader?id=babel'
},
{
test: /\.vue$/,
include: [resolve('src')],
exclude: /node_modules/,
loader: 'happypack/loader?id=vue'
},
]
},
plugins: [
new HappyPack({
id: 'babel', // HappyPack的id,和上面module里rules的查询参数一致
loaders: ['babel-loader?cacheDirectory=true'],
threads: 4, // 开启 4 个线程
threadPool: happyThreadPool, // 指定进程池
verbose: true
}),]
}
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
# 有哪些常见的Loader?你用过哪些Loader?
raw-loader
:加载文件原始内容(utf-8)file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)url-loader
:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)source-map-loader
:加载额外的 Source Map 文件,以方便断点调试svg-inline-loader
:将压缩后的 SVG 内容注入代码中image-loader
:加载并且压缩图片文件json-loader
加载 JSON 文件(默认包含)handlebars-loader
: 将 Handlebars 模版编译成函数并返回babel-loader
:把 ES6 转换成 ES5ts-loader
: 将 TypeScript 转换成 JavaScriptawesome-typescript-loader
:将 TypeScript 转换成 JavaScript,性能优于 ts-loadersass-loader
:将SCSS/SASS代码转换成CSScss-loader
:加载 CSS,支持模块化、压缩、文件导入等特性style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSSpostcss-loader
:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀eslint-loader
:通过 ESLint 检查 JavaScript 代码tslint-loader
:通过 TSLint检查 TypeScript 代码mocha-loader
:加载 Mocha 测试用例的代码coverjs-loader
:计算测试的覆盖率vue-loader
:加载 Vue.js 单文件组件i18n-loader
: 国际化cache-loader
: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
更多 Loader 请参考官网 (opens new window)
# 有哪些常见的Plugin?你用过哪些Plugin?
define-plugin
:定义环境变量 (Webpack4 之后指定 mode 会自动配置)ignore-plugin
:忽略部分文件uglifyjs-webpack-plugin
:不支持 ES6 压缩 (Webpack4 以前)mini-css-extract-plugin
: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)speed-measure-webpack-plugin
: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)webpack-bundle-analyzer
: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)terser-webpack-plugin
: 支持压缩 ES6 (Webpack4)webpack-parallel-uglify-plugin
: 多进程执行代码压缩,提升构建速度html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)web-webpack-plugin
:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用serviceworker-webpack-plugin
:为网页应用增加离线缓存功能clean-webpack-plugin
: 目录清理ModuleConcatenationPlugin
: 开启 Scope Hoisting
更多 Plugin 请参考官网 (opens new window)
# 自定义loader*plugin【要点】
# 是否写过Loader?简单描述一下编写loader的思路?
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的API (opens new window) 可以去官网查阅
Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。
Loader 为模块转换器,用于将模块的原内容按照需求转换成新内容;
Loader 的职责是单一的,只需要完成一种转换,遵守单一职责原则;
Webpack 为 Loader 提供了一系列 API 供 Loader 调用,例如:
- loader-utils.getOptions( this ) 获取用户传入的 options,
- this.callback( ) 自定义返回结果,
- this.async( ) 支持异步操作;
- this.context 当前文件所在的目录;
- this.resource 当前处理文件的完整请求路径;
- 其它等等
Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
尽可能的异步化 Loader,如果计算量很小,同步也可以
Loader 是无状态的,我们不应该在 Loader 中保留状态
使用 loader-utils 和 schema-utils 为我们提供的实用工具
加载本地 Loader 方法
- Npm link
- ResolveLoader
# 是否写过Plugin?简单描述一下编写Plugin的思路?
webpack在运行的生命周期中会广播出许多事件,**Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,**使得整个系统扩展性良好。
Plugin的API (opens new window) 可以去官网查阅
- compiler 暴露了和 Webpack 整个生命周期相关的钩子
- compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
- 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
- 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
- 找出合适的事件点去完成想要的功能
- emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
- watch-run 当依赖的文件发生变化时会触发
- 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
# 自定义示范
# 自定义loader
注意,第一个 loader 的输入时源文件,之后所有 loader 的输入是上一个 loader 的输出,最后一个 loader 输出给 webpack。
一个loader是一个导出为函数的 js 模块,这个函数有三个参数:content, map, meta
- content: 表示源文件字符串或者buffer
- map: 表示sourcemap对象
- meta: 表示元数据,辅助对象
实现要点:【3步骤】
获取loader配置;
const { getOptions } = require("loader-utils")
默认三个参数 ;
module.exports = function(content, map, meta) {}
转换后的内容,source-map, AST;
处理content返回及判断即可;
示例:手写一个 loader 源码,其功能是将 /hello/gi 转换成 HELLO
function convert(source){
return source && source.replace(/hello/gi,'HELLO');
}
//module.exports = function(content, map, meta) {}
module.exports = function(content){
return convert(content);
}//custom-loader
//使用;Npm link 模块注册; npm link custom-loader
module:{
rules:[
{
test:/\.js/,
use:['custom-loader'],
include:path.resolve(__dirname,'show')
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 自定义plugin
实现要点:【3步骤】
- 是否构造传递初始化函数;
- 调用插件
apply 函数传入 compiler 对象
- 通过
compiler 对象监听事件
示例:手写一个 plugin 源码,其功能是在 Webpack 编译成功或者失败时输出提示;
class CustomPlugin{
constructor(doneCallback, failCallback){// 保存在创建插件实例时传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler){
compiler.plugin('done',(stats)=>{// 成功完成一次完整的编译和输出流程时,会触发 done 事件
this.doneCallback(stats);
})
compiler.plugin('failed',(err)=>{// 在编译和输出的流程中遇到异常时,会触发 failed 事件
this.failCallback(err);
})
}
}
module.exports = CustomPlugin;
//使用;npm link custom-plugin
plugins:[
new CustomPlugin(
stats => {console.info('编译成功!')},
err => {console.error('编译失败!')}
),
],
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?
可以使用 enforce
强制执行 loader
的作用顺序;
enforce:用来指定一个 loader 的种类,其默认值为 normal,可选值为
- pre,在 use 配置的所有 loader 之前执行,比如下面就是保证检测的代码不是其他 loader 更改过来的;
- post,在 use 配置的所有 loader 之后执行;
- inline,官方不推荐使用;
使用示范:
module.exports = {
module: {
rules: [
{
use: ['style-loader', 'css-loader'],
resource: {
test: /\.css$/,
exclude: /node_modules/
},
issuer: {
test: /\.js$/,
exclude: '/node_modules/',
include: '/src/pages/'
}
},
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader'
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用webpack开发时,你用过哪些可以提高效率的插件?
(这道题还蛮注重实际,用户的体验还是要从小抓起的)
webpack-dashboard
:可以更友好的展示相关打包信息。webpack-merge
:提取公共配置,减少重复配置代码speed-measure-webpack-plugin
:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。size-plugin
:监控资源体积变化,尽早发现问题HotModuleReplacementPlugin
:模块热替换
# 基础
# source map是什么?生产环境怎么用?【要点】
source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
hidden-source-map
:借助第三方错误监控平台 Sentry 使用;nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高;sourcemap
:通过 nginx 设置将 .map 文件只对白名单开放(公司内网);
注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle 体积大小,并降低整体性能。
module.exports = {
resolve: {
extensions: ['.js', '.css', '.json'],// 文件扩展名,写明以后就不需要每个文件写后缀
//config.resolve.alias.set('@$', resolve('src')) //vue.config.js的使用方式;
alias: {// 路径别名,比如这里可以使用 css 指向 static/css 路径
'@': resolve('src'),
'css': resolve('static/css')
}
},
//要点
//cheap不包含列信息(关于列信息的解释下面会有详细介绍)也不包含loader的sourcemap
//module包含loader的sourcemap(比如jsx to js ,babel的sourcemap),否则无法定义源文件
//eval使用eval包裹模块代码
//source-map产生.map文件;正式环境不上传.map文件,放到监控平台上处理即可;
devtool: 'cheap-module-eval-source-map', //开发环境
devtool: 'cheap-module-source-map', //正式环境
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 如何对bundle体积进行监控和分析?
VSCode
中有一个插件 Import Cost
可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer
生成 bundle
的模块组成图,显示所占体积。
bundlesize
工具包可以进行自动化资源体积监控。
# 文件指纹是什么?怎么用?
文件指纹是打包后输出的文件名的后缀。
Hash
:和整个项目的构建相关,**只要项目文件有修改,整个项目构建的 hash 值就会更改;**比如:图片处理file-loader;Chunkhash
:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash;比如:js处理;Contenthash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变;比如:样式处理;
# 图片的文件指纹设置
设置file-loader的name,使用hash。
占位符名称及含义
- ext 资源后缀名
- name 文件名称
- path 文件的相对路径
- folder 文件所在的文件夹
- contenthash 文件的内容hash,默认是md5生成
- hash 文件内容的hash,默认是md5生成
- emoji 一个随机的指代文件内容的emoji
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
},
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# JS的文件指纹设置
设置 output 的 filename,用 chunkhash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
}
}
2
3
4
5
6
7
8
9
10
# CSS的文件指纹设置
设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢?
代码分割的本质其实就是在源代码直接上线
和打包成唯一脚本main.bundle.js
这两种极端;方案之间的一种更适合实际场景的中间状态。
「用可接受的服务器性能压力增加来换取更好的用户体验。」
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。
# 分析index.html【要点】
从上面我们可以看到,先用link定义Home.js、app.js、chunk-vendors.js这些资源和web客户端的关系。
ref=preload
:告诉浏览器这个资源要给我提前加载。rel=prefetch
:告诉浏览器这个资源空闲的时候给我加载一下。as=script
:告诉浏览器这个资源是script,提升加载的优先级。
然后在body里面加载了chunk-vendors.js、app.js这两个js资源。可以看出web客户端初始化时候就加载了这个两个js资源。
# 配置单页应用及多页应用
单页应用可以理解为webpack的标准模式,直接在entry
中指定单页应用的入口即可,这里不再赘述
多页应用的话,可以使用webpack的 AutoWebPlugin
来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:
- 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
- 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置
const pages = {}
function resolve(dir) {
return path.join(__dirname, dir)
}
glob.sync('./src/views/**/main.js').forEach(path => {
const chunk = path.split('./src/views/')[1].split('/main.js')[0]
pages[chunk] = {
entry: path,
template: 'public/index.html',
title: titles[chunk],
chunks: ['chunk-vendors', 'chunk-common', chunk]
}
})
module.exports = {
runtimeCompiler: true,
pages,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 相关原理
# 模块打包原理知道吗?【要点】
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
# babel 原理【要点】
babel的转译过程分为三个阶段:parsing、transforming、generating
;
以ES6代码转译为ES5代码为例,babel转译的具体过程如下:
- ES6代码输入;
- babylon 进行解析得到 AST;
- plugin 用 babel-traverse 对 AST 树进行遍历转译,得到新的AST树;
- 用 babel-generator 通过 AST 树生成 ES5 代码;
详见【webpack的插件实践】;
# 聊一聊Babel原理吧
大多数JavaScript Parser遵循 estree
规范,Babel 最初基于 acorn
项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:
- 解析:将代码转换成 AST;
- 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
- 语法分析:分析token流(上面生成的数组)并生成 AST
- 转换:访问 AST 的节点进行变换操作生产新的 AST;
- Taro (opens new window)就是利用 babel 完成的小程序语法转换
- 生成:以新的 AST 为基础生成代码;
想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目 the-super-tiny-compiler (opens new window)
# 文件监听原理呢?
在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:
- 启动 webpack 命令时,带上 --watch 参数
- 在配置 webpack.config.js 中设置 watch:true
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 说一下 Webpack 的热更新原理吧【要点】
(敲黑板,这道题必考)
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是**客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,**当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
细节请参考Webpack HMR 原理解析 (opens new window)
- Webpack编译期,为需要热更新的 entry 注入热更新代码(EventSource通信);
- 页面首次打开后,服务端与客户端通过 EventSource 建立通信渠道,把下一次的 hash 返回前端;
- 客户端获取到hash,这个hash将作为下一次请求服务端
hot-update.js
和hot-update.json
的hash; - 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端;
- 客户端获取到hash,成功后客户端构造hot-update.js script链接,然后插入主文档;
- hot-update.js 插入成功后,执行hotAPI 的 createRecord 和 reload方法,获取到 Vue 组件的 render方法,重新 render 组件, 继而实现 UI 无刷新更新;
# devServer设置
服务于webpack-dev-server 内部封装了一个express;
devServer: { //服务于webpack-dev-server 内部封装了一个express
port: '8080',
before(app) {
app.get('/api/test.json', (req, res) => {
res.json({
code: 200,
message: 'Hello World'
})
},
//在本地服务器开启gzip,线上服务器都支持gzip不需要设置
before(app) {
app.get(/.*.(js)$/, (req, res, next) => {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
next();
})
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 样式相关
# scoped属性的实现原理
scoped属性的实现原理是给每一个dom元素添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom。
这类似于 Shadow DOM 中的样式封装。它有一些注意事项,但不需要任何 polyfill。它通过使用 PostCSS 来实现以下转换:
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
//转换结果
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 子组件的根元素
**使用 scoped
后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。**这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
# scoped样式穿透
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性。有以下两种方式实现:
- 使用/deep/
//Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
.wrap /deep/ .box{
background: red;
}
</style>
//Child
<template>
<div class="box"></div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 使用两个style标签
//Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
//其他样式
</style>
<style lang="scss">
.wrap .box{
background: red;
}
</style>
//Child
<template>
<div class="box"></div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 优化
# 如何优化 Webpack 的构建速度?【要点】
(这个问题就像能不能说一说**「从URL输入到页面显示发生了什么」**一样)
(我只想说:您希望我讲多长时间呢?)
(面试官:。。。)
- 使用
高版本
的 Webpack 和 Node.js 多进程/多实例构建
:HappyPack(不维护了)、thread-loader压缩代码
- 多进程并行压缩
- webpack-paralle-uglify-plugin
- uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
- terser-webpack-plugin 开启 parallel 参数
- 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
- 多进程并行压缩
图片压缩
- 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
- 配置 image-webpack-loader
缩小打包作用域
:- exclude/include (确定 loader 规则范围)
- resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
- resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
- resolve.extensions 尽可能减少后缀尝试的可能性
- noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
- IgnorePlugin (完全排除模块)
- 合理使用alias
提取页面公共资源
:- 基础包分离:
- 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
- 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
- 基础包分离:
DLL
:- 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
- HashedModuleIdsPlugin 可以解决模块数字id问题
充分利用缓存提升二次构建速度
:- babel-loader 开启缓存
- terser-webpack-plugin 开启缓存
- 使用 cache-loader 或者 hard-source-webpack-plugin
Tree shaking
- 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
- 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
- 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
- purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
Scope hoisting
- 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大**。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突**
- 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
动态Polyfill
- 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
更多优化请参考官网-构建性能 (opens new window)
# 优化总括
# 开发环境
- HRM (热替换)
- webpack-dev-server (本地服务器)
- soure-map (调试)
- webpack-bundle-analyzer(打包生成代码块分析视图)
- size-plugin(监控打包资源的体积变量化)
- speed-measure-webpack-plugin(分析loader和plugin打包的耗时)
# 生产环境
# 体积优化【要点】
- html压缩 (html-webpack-plugin )
- js压缩 (production模式自动开启)
- css压缩 (optimize-css-assets-webpack-plugin)
- css提取(mini-css-extract-plugin)
- externals (排除不需要被打包的第三方)
- tree-shake ( production模式自动开启(webpack4限EsModule;webpack5不限EsModule,CommonJs,优秀得很) )
- import(懒加载,预加载(预加载慎用))
- code-split ( optimization )
# 打包速度优化
- exclude / exclude (排除一些不需要编译的文件)
- module.noParse (排除不需要被loader编译的第三方库)
- babel缓存( 缓存cacheDirectory )
- 多线程打包(thread-loader 、happyPack)
- 动态链 ( DLL )
# 相关链接
https://juejin.cn/post/6844904094281236487