webpack面试题
- webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件
- 不同的Loader来处理不同的文件,用Plugin来扩展webpack功能
构建流程是什么?从读取配置到输出文件这个过程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
1.初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
3.确定入口:根据配置中的 entry 找出所有的入口文件;
4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
5.完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
编写loader或plugin的思路
Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。
编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。
每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。
还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。
此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。相对于Loader而言,Plugin的编写就灵活了许多。
webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
webpack的热更新是如何做到的

首先要知道server端和client端都做了处理工作
1.第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,
根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
2.第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和
webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
3.第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。
注意,这儿是浏览器刷新,和 HMR 是两个概念。
4.第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket
长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket
消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
5.webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server
的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,
也就没有后面那些步骤了。
6.HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime
向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,
获取到最新的模块代码。这就是上图中 7、8、9 步骤。
7.而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,
检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
8.最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
提高webpack的构建速度
- 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的
UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css - 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于
output参数和各loader的publicPath参数来修改资源路径 - 删除死代码(
Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现 - 提取公共代码。
提高webpack的构建速度
- 多入口情况下,使用
CommonsChunkPlugin来提取公共代码 - 通过
externals配置来提取常用库 - 利用
DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。 - 使用
Happypack实现多线程加速编译 - 使用
webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度 - 使用
Tree-shaking和Scope Hoisting来剔除多余代码
Loader 和 Plugin 的区别
Loader:
- 本质是一个函数,用于转换模块的源代码
- 在模块加载时执行,将文件从不同语言转换为 JavaScript
- 执行顺序:从右到左(或从下到上)
- 常见:
babel-loader、css-loader、file-loader
Plugin:
- 本质是一个类,可以监听 webpack 生命周期钩子
- 在 webpack 运行到某个时刻时执行,可以访问整个编译过程
- 功能更强大,可以修改输出、优化资源等
- 常见:
HtmlWebpackPlugin、MiniCssExtractPlugin、CleanWebpackPlugin
如何编写一个自定义 Loader
// my-loader.js
module.exports = function(source) {
// source 是源文件内容
// this 是 loader 的上下文对象
// 同步 loader
return source.replace('foo', 'bar');
// 异步 loader
const callback = this.async();
setTimeout(() => {
callback(null, source.replace('foo', 'bar'));
}, 100);
// 使用 this.callback
this.callback(null, transformedSource, sourceMap);
};
如何编写一个自定义 Plugin
// my-plugin.js
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
// compilation.assets 包含所有输出资源
for (const filename in compilation.assets) {
if (filename.endsWith('.js')) {
const source = compilation.assets[filename].source();
compilation.assets[filename] = {
source: () => source.replace(/console\.log/g, ''),
size: () => source.length,
};
}
}
});
}
}
module.exports = MyPlugin;
webpack 的代码分割原理
代码分割方式:
- 入口起点:配置多个 entry
- 动态导入:使用
import()语法 - SplitChunksPlugin:自动提取公共代码
原理:
- webpack 会分析模块依赖关系
- 将公共模块提取到单独的 chunk
- 通过
__webpack_require__.e()实现按需加载 - 使用 JSONP 方式加载异步 chunk
Tree Shaking 原理
原理:
- ES6 模块是静态的,可以在编译时分析
- webpack 标记未使用的导出
- 使用 UglifyJS 或 Terser 删除未使用的代码
条件:
- 使用 ES6 模块语法(
import/export) - 配置
optimization.usedExports: true - 配置
optimization.sideEffects: false或在 package.json 中声明 - 生产模式自动启用
webpack 的模块解析机制
解析顺序:
- 绝对路径:直接使用
- 相对路径:相对于当前文件
- 模块路径:从
node_modules查找 - 别名:使用
resolve.alias配置
解析过程:
- 根据
resolve.extensions尝试添加扩展名 - 根据
resolve.mainFields查找 package.json 中的入口 - 根据
resolve.modules查找模块目录
webpack 的 hash、chunkhash、contenthash 区别
hash:
- 与整个项目构建相关
- 项目任何文件改变,hash 都会改变
- 所有文件共享同一个 hash
chunkhash:
- 与 chunk 相关
- 同一个 chunk 的文件 hash 相同
- 不同 chunk 的 hash 不同
- 适合用于 JS 文件
contenthash:
- 与文件内容相关
- 只有文件内容改变,hash 才改变
- 适合用于 CSS 文件(与 JS 分离)
webpack 5 相比 webpack 4 的改进
- 持久化缓存:显著提升构建速度
- Tree Shaking 增强:支持嵌套模块的 Tree Shaking
- 模块联邦(Module Federation):微前端解决方案
- 更好的 Tree Shaking:支持 CommonJS 的 Tree Shaking
- 资源模块:内置资源模块,无需 loader
- Top Level Await:支持顶层 await
- 更好的长期缓存:改进的 chunk ID 算法
webpack 的优化策略
开发环境:
- 使用
cache启用缓存 - 使用
devtool: 'eval-cheap-module-source-map' - 减少 loader 处理范围(exclude)
- 使用
DllPlugin预编译第三方库
生产环境:
- 代码压缩(TerserPlugin)
- Tree Shaking
- 代码分割(SplitChunksPlugin)
- 使用 CDN(externals)
- 启用 Gzip 压缩
- 图片优化(压缩、懒加载)
webpack 如何处理 CSS
处理流程:
css-loader:解析 CSS 文件,处理@import和url()postcss-loader:使用 PostCSS 处理(autoprefixer 等)sass-loader/less-loader:编译预处理器style-loader:将 CSS 注入到 DOM(开发环境)MiniCssExtractPlugin.loader:提取 CSS 到文件(生产环境)
webpack 的 Source Map 原理
Source Map 作用:
- 将编译后的代码映射回原始源代码
- 方便调试和错误定位
常见类型:
eval:最快,但只能看到行号source-map:最完整,但最慢cheap-module-source-map:折中方案hidden-source-map:生成但不引用
webpack 如何处理图片资源
处理方式:
- file-loader:将图片复制到输出目录,返回 URL
- url-loader:小图片转为 base64,大图片使用 file-loader
- asset/resource(webpack 5):内置资源模块
- image-webpack-loader:压缩图片
webpack 的 Scope Hoisting 是什么
作用:
- 将所有模块的代码提升到一个作用域内
- 减少函数声明和闭包
- 减小代码体积,提升执行效率
原理:
- 分析模块依赖关系
- 将模块代码合并到同一个作用域
- 需要 ES6 模块语法支持
webpack 的模块联邦(Module Federation)
概念:
- webpack 5 新特性
- 允许在运行时动态加载远程模块
- 实现微前端架构
配置:
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});