# 谈谈你对 webpack 的看法
webpack 是一个模块打包工具,可以使用 webpack 管理模块依赖,并编译输出模块们所需的静态文件。它能很好地管理、打包 web 开发中所用到的 html、css、js 以及各种静态文件(图片、字体等),让开发过程更加高效。对于不同类型的资源,webpack 有对应的模块加载器。webpack 模块打包器会分析模块间的依赖关系,最后生成了优化且合并后的静态资源。
# 特点
- 对 CommonJS、AMD、ES6 的语法做了兼容处理
- 对 js、css、图片等资源文件都支持打包
- 串联模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如对 ES6 的支持
- 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化的时间
- 支持 sourcemap,易于调试
- 具有强大的 plugin 接口,大多是内部插件,使用起来比较灵活
# webpack 与 grunt、gulp、rollup 的不同
# 优点
- 专注于模块化项目
- plugins 能做很多事情
- 社区非常活跃
# 缺点
- 上手难度高
- plugins 繁多,需要不断学习才能灵活掌握,经常出现文章中推荐的 plugin,但是实际上已经停止维护
- 对于初学者,调试很难定位问题
- 构建速度较慢,这也是后起之秀主要针对的点
# 什么情况下选择 webpack?什么情况下选择 rollup?
- webpack 适用于构建大型复杂的前端站点
- rollup 适用于基础库的打包
从另一个角度说:如果你需要进行代码分割,或者你有很多静态资源,再或者你的项目深度依赖于 CommonJS,选择 webpack。
如果你的代码基于 ES6 模块编写,并且做出来的是供他人使用,可以考虑 rollup
# webpack 有什么优劣势
grunt 和 gulp 是基于任务和流(Task、Stream)的,找到一个(或一类)文件,对其做一些链式操作,更新流上的数据,整条链式操作构成一个任务,多个任务就构成整个 web 构建流程。
rollup 和 webpack 类似,但更专注于 ES6 的模块打包。它最大亮点是利用 ES6 模块设计,生成更简洁、更简单的代码。
webpack 是模块化管理工具和打包工具,它是基于入口。webpack 会自动递归解析入口所需要加载的所有资源文件,然后用不同的 loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。
虽然目前主流方向是 webpack,但是一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件;另外一些库也会使用 rollup 打包。
# loader 和 plugin 的区别
# 从功能上说
loader 用于加载待打包的资源,Plugin 用于扩展 webpack 功能。
loader 本质上就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果,主要用于加载某些资源文件。因为 webpack 只会加载 js 和 json,这就需要对应的 loader 将资源转化,加载进来。
plugin 用于扩展 webpack 的功能(loader 其实也是扩展功能,但是只专注于转化文件这一领域),在 webpack 运行的生命周期中会广播许多生命周期钩子事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。
# 从运行时机角度区分
loader 运行在打包文件之前(loader 为模块加载时的预处理文件)
plugin 在整个编译周期都起作用
# 从使用角度区分
loader 在 rules 中配置,类型为数组,每一项都是一个 Object,内部包含了 test(类型文件)、loader、options(参数)等属性
plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入
# webpack 构建过程
webpack 的运行流程是一个串行的过程
# 初始化参数
从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
# 开始编译
用上一部得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
# 确定入口
根据配置中的 entry
找出所有文件的入口
# 编译模块
从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤处理
# 完成模块编译
再经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
# 输出资源
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
# 输出完成
在确定输出内容后,根据配置确定输出的路径和文件名,把文件内容写入文件系统
# 其他
以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供 API 改变 webpack 的运行结果
简单来说:
- 初始化:启动构建,读取与合并配置参数,加载 plugin,实例化 Compiler
- 编译:从 entry 出发,针对每个 Module 串行调用对应的 loader 去翻译文件的内容,在找到该 module 依赖的 module,递归的进行编译处理
- 输出:将编译后的 module 组合成 chunk,将 chunk 转换成文件,输出到文件系统中
# webpack 如何解析代码路径
webpack 依赖 enhanced-resolve 来解析代码模块路径,这个模块像 Node.js 那一套模块路径解析的增强版,有很多可以自定义的解析配置。
模块解析规则分三种:
- 解析相对路径
- 查找想对当前模块的路径下是否有对应的文件或文件夹,是 文件 则直接加载
- 如果是文件夹则找到对应文件夹下是否有
package.json
文件 - 有的话就按照文件中的
main
字段的文件名来查找文件 - 没有
package.json
或main
字段,则查找index.js
文件
- 解析绝对路径:直接查找对应路径文件,不建议使用,因为不同的机器会用绝对路径查找不到
- 解析模块名:查找当前文件目录,父级直至根目录下的 node_modules 文件夹,看是否有对应名称的模块
通过配置 resolve.alias
、resolve.extensions
、resolve.modules
等字段优化路径查找速度
# sourcemap 的作用和原理
sourcemap 是将编译、打包、压缩之后的代码映射回源码的过程。打包压缩后,代码不具备良好的可读性,想要调试源码就需要 sourcemap,出错的时候控制台会直接显示原始代码出错的位置。
map 文件只要不打开开发者工具,浏览器是不会加载的。
# 模块打包原理
webpack 根据 webpack.config.js
中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用 CommonJS 写的,还是 ES6 Module 规范写的,webpack 会自动进行分析,并通过转换、编译代码,打包成最终文件。最终文件中的模块实现是基于 webpack 自己实现的 webpack_require(es5 代码),所以打包后的文件可以跑在浏览器上。
同时意味着在 webpack 环境中,你可以使用 es6 语法,也可以使用 commonjs 语法,因为从 webpack2 开始内置了对 ES6、CommonJS、AMD 模块化语句的支持,webpack 会对各种模块进行语法分析,并作转换编译。
另外,针对异步模块:webpack 实现模块的异步加载方式有点像 jsonp 的流程。
遇到异步模块时,使用 __webpack_require__.e
函数去把异步代码加载进来。该函数会在 html 的 head 中动态增加 script 标签,src 指向指定的异步模块存放的文件。
加载的异步模块文件会执行 webpackJsonpCallback
函数,把异步模块加载到主文件中。
所以后续可以像同步模块一样,直接使用 __webpack_require__("**.js")
加载异步模块。
# 文件监听原理
发现源码发生改变时,自动重新构建出新的输出文件
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,初次构建是把文件的修改时间储存起来,下次有修改时会和上次的修改时间做比对,发现不一致时,不会立即告诉监听者,而是缓存起来,等 aggregateTimeout
后,把变更列表一起构建,并生成到 bundle 文件夹
module.export = {
// 默认为 false,也就是不开启
watch: true,
watchOptions: {
// 默认为空,不监听的文件夹或文件,支持正则匹配
ignore: /node_modules/,
// 监听到变化发生后会等 300ms,再去执行,默认 300ms
aggregateTimeout: 300,
// 判断文件是否发生变化的轮询在1s内发生的次数
poll: 1000
}
}
# webpack 热更新原理
Webpack 的热更新也称热替换(Hot Module Replacement),这个机制可以做到不用刷新浏览器而将变更的模块替换掉旧的模块。
相对于手动刷新页面,HMR 的优势在于可以保存应用的状态,提高开发效率。
- webpack 构建的项目,分为 server 端和 client 端,项目启动时,双方会保持一个 socket 连接用于通话
- 当本地资源发生变化时,server 向浏览器发送新资源的 hash 值,浏览器调用 reloadApp 方法,检查是否有变化,有差异会向 server 发起 ajax 获取更改内容(文件列表、hash),这样浏览器继续借助这些信息向 server 端发起请求,通过 jsonp 的方式获取 chunk 的增量更新
# 文件指纹
文件指纹是指打包后输出的文件后缀
- Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
- Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash
- Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
js 文件指纹设置:
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
}
}
css 文件指纹设置:
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
}),
new ExtractTextPlugin('[name][contenthash].css')
]
}
图片文件指纹设置 file-loader 或 url-loader 的 name,使用 hash
# webpack 如何保证各个 loader 的执行顺序
通过配置 module.rules
中的 enforce 字段,将 loader 分为 preLoader
、postLoader
和 loader
三种,执行顺序为 pre -> loader -> inline -> post。
pre 代表所有正常 loader 之前执行,post 是所有 loader 之后执行,inline 官方不推荐使用
# 代码分割的意义
用可接受的服务器性能压力增加来换取更好的用户体验
- 复用的代码抽离到公共模块,解决代码冗余
- 公共模块再按照使用的页面多少(或其他思路)进一步拆分,用来减小文件体积,顺便优化首屏速度
拆分原则:
如何拆分因项目而异,但是普遍使用的原则:
- 业务代码和第三方库代码分离打包,实现代码分割
- 业务代码中的公共业务模块提取打包到一个模块
- 首屏相关模块单独打包