webpack简介
webpack是一个现代JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块,并生成一个或多个bundle。
webpack核心概念
1. Entry(入口)
入口起点指示webpack应该使用哪个模块作为构建其内部依赖图的开始。进入入口起点后,webpack会找出哪些模块和库是入口起点依赖的。
1 2 3 4 5 6 7 8
| module.exports = { entry: './src/index.js' };
|
2. Output(输出)
output属性告诉webpack在哪里输出它所创建的bundle,以及如何命名这些文件。
1 2 3 4 5 6 7 8 9 10 11
| const path = require('path');
module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' } };
|
3. Loader(加载器)
webpack只能理解JavaScript和JSON文件。loader让webpack能够去处理其他类型的文件,并将它们转换为有效模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, { test: /\.(png|jpg|gif)$/, type: 'asset/resource' } ] } };
|
4. Plugin(插件)
插件用于执行范围更广的任务,包括打包优化、资源管理、注入环境变量等。
1 2 3 4 5 6 7 8 9 10 11
| const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = { plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template: './src/index.html' }) ] };
|
5. Mode(模式)
通过设置mode参数为development、production或none,可以启用webpack内置的优化。
1 2 3
| module.exports = { mode: 'production' };
|
webpack构建流程(核心原理)
webpack的构建流程可以分为以下几个阶段:
1. 初始化阶段
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数
- 创建Compiler对象:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始编译
- 确定入口:根据配置中的entry找出所有的入口文件
2. 编译阶段
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:经过Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系图(Dependency Graph)
3. 输出阶段
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表
- 写入文件系统:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
构建流程详细说明
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 40 41 42 43 44 45 46 47
|
class Compiler { constructor(options) { this.options = options; this.hooks = { run: new SyncHook(), compile: new SyncHook(), emit: new AsyncSeriesHook(), done: new AsyncSeriesHook() }; }
run() { this.hooks.run.call(); this.compile(); }
compile() { this.hooks.compile.call(); const compilation = new Compilation(this); compilation.buildModule(this.options.entry); this.emitAssets(compilation); }
emitAssets(compilation) { this.hooks.emit.callAsync(compilation, err => { compilation.assets.forEach(asset => { this.outputFileSystem.writeFile(asset.path, asset.content); }); this.hooks.done.callAsync(compilation); }); } }
|
webpack模块化原理
webpack支持多种模块化规范(ES Module、CommonJS、AMD等),它通过将所有模块转换成统一的格式来实现。
打包后的代码结构
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 40 41 42
| (function(modules) { var installedModules = {};
function __webpack_require__(moduleId) { if(installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} };
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports; }
return __webpack_require__(__webpack_require__.s = './src/index.js'); })({ './src/index.js': function(module, exports, __webpack_require__) { const util = __webpack_require__('./src/util.js'); console.log(util.add(1, 2)); }, './src/util.js': function(module, exports) { exports.add = function(a, b) { return a + b; }; } });
|
HMR(Hot Module Replacement)原理
HMR热模块替换允许在运行时更新所有类型的模块,而无需完全刷新。
HMR工作流程
- 文件系统监听:webpack-dev-server监听文件变化
- 编译打包:文件变化后webpack重新编译,生成新的manifest文件和更新后的chunk
- 通知客户端:webpack-dev-server通过WebSocket向浏览器发送更新通知
- 下载更新:浏览器端的HMR runtime接收到更新通知后,通过Ajax请求获取更新的模块
- 模块替换:HMR runtime替换掉旧的模块,并执行模块更新逻辑
1 2 3 4 5 6 7
| if (module.hot) { module.hot.accept('./module.js', function() { console.log('模块已更新'); }); }
|
Tree Shaking原理
Tree Shaking是一种通过清除多余代码来优化项目打包体积的技术。
实现原理
- 基于ES6模块:ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析
- 标记未使用代码:webpack在打包时会标记出未使用的代码
- 压缩工具清除:使用压缩工具(如Terser)清除未使用的代码
1 2 3 4 5 6 7 8 9 10 11 12
| export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
import { add } from './util.js'; console.log(add(1, 2));
|
Tree Shaking注意事项
1 2 3 4 5 6
| { "sideEffects": false, }
|
Code Splitting(代码分割)
代码分割是webpack最引人注目的特性之一,可以把代码分割到不同的bundle中,然后按需加载或并行加载。
三种代码分割方式
1. 入口起点分割
1 2 3 4 5 6 7 8 9
| module.exports = { entry: { index: './src/index.js', another: './src/another.js' }, output: { filename: '[name].bundle.js' } };
|
2. 防止重复(SplitChunksPlugin)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 }, common: { minChunks: 2, priority: 5, reuseExistingChunk: true } } } } };
|
3. 动态导入
1 2 3 4 5 6 7
| button.addEventListener('click', () => { import( 'lodash') .then(({ default: _ }) => { console.log(_.join(['Hello', 'webpack'], ' ')); }); });
|
面试常见考察点
1. webpack与其他打包工具的区别
webpack vs Rollup:
- webpack适合应用程序开发,Rollup更适合库开发
- webpack有更丰富的插件生态,Rollup打包产物更简洁
- webpack支持代码拆分和动态导入,Rollup的Tree Shaking更彻底
webpack vs Vite:
- Vite开发环境使用ESM原生支持,启动速度更快
- webpack生态更成熟,生产环境打包更可靠
- Vite适合现代浏览器开发,webpack兼容性更好
2. 如何优化webpack构建速度
开发环境优化
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
| module.exports = { devtool: 'eval-cheap-module-source-map', resolve: { extensions: ['.js', '.json'], modules: [path.resolve(__dirname, 'node_modules')], alias: { '@': path.resolve(__dirname, 'src') } }, cache: { type: 'filesystem' }, module: { rules: [ { test: /\.js$/, include: path.resolve(__dirname, 'src'), exclude: /node_modules/, use: ['babel-loader'] } ] } };
|
生产环境优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const TerserPlugin = require('terser-webpack-plugin');
module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: true }) ], splitChunks: { chunks: 'all' } }, externals: { 'react': 'React', 'react-dom': 'ReactDOM' } };
|
其他优化手段
- 使用thread-loader:多进程打包
- 使用DllPlugin:预编译资源模块
- 使用HardSourceWebpackPlugin:提供中间缓存(webpack5已内置)
- 使用noParse:忽略不需要解析的库
3. 如何优化webpack打包体积
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
| const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const CompressionPlugin = require('compression-webpack-plugin');
module.exports = { mode: 'production', optimization: { usedExports: true, sideEffects: true }, optimization: { minimize: true }, optimization: { splitChunks: { chunks: 'all' } }, plugins: [ new BundleAnalyzerPlugin(), new CompressionPlugin({ algorithm: 'gzip', test: /\.(js|css)$/, threshold: 10240, minRatio: 0.8 }) ] };
|
其他优化策略:
- 使用CDN加载第三方库
- 按需加载(动态import)
- 图片压缩和使用WebP格式
- 使用PurgeCSSPlugin删除未使用的CSS
- 使用scope hoisting(作用域提升)
4. Loader和Plugin的区别
Loader(加载器):
- 用于转换某些类型的模块
- 本质是一个函数,接收源文件内容,返回转换后的结果
- 在module.rules中配置
- 执行顺序:从右到左,从下到上
1 2 3 4 5 6
| module.exports = function(source) { const result = transform(source); return result; };
|
Plugin(插件):
- 用于执行更广泛的任务,如打包优化、资源管理等
- 本质是一个包含apply方法的类
- 在plugins数组中配置
- 通过webpack的钩子系统工作
1 2 3 4 5 6 7 8 9 10
| class MyPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { console.log('正在生成资源...'); callback(); }); } }
|
5. webpack的生命周期钩子(Tapable)
webpack基于Tapable实现了事件流机制,类似Node.js的EventEmitter。
常用钩子:
- beforeRun:清除缓存
- run:开始编译
- compile:真正开始编译,在创建compilation对象之前
- compilation:生成好了compilation对象
- make:从entry开始递归分析依赖,准备对每个模块进行build
- afterCompile:编译build过程结束
- emit:在将内存中assets内容写到磁盘文件夹之前
- afterEmit:在将内存中assets内容写到磁盘文件夹之后
- done:完成所有编译过程
6. source-map原理和配置
source-map是从已转换的代码映射到原始源代码的文件,便于调试。
1 2 3 4 5 6 7
| module.exports = { devtool: 'eval-cheap-module-source-map', };
|
常见配置对比:
- eval:使用eval包裹模块代码,快但不生成map文件
- source-map:生成完整的source-map文件,构建慢但调试友好
- cheap-source-map:不包含列信息,构建较快
- module:包含loader的source-map
- inline:将map以DataURL形式嵌入代码
- hidden:生成map但不引用,用于错误报告工具
- nosources:创建map但不包含源代码内容
7. webpack5的新特性
- 持久化缓存:通过配置cache提升构建性能
- 模块联邦(Module Federation):多个独立构建可以组成一个应用
- 资源模块:内置了资源处理能力,不再需要file-loader等
- 更好的Tree Shaking:支持对嵌套的exports进行优化
- 移除了Node.js polyfills:减少了bundle体积
- 更好的持久化缓存:确定性的chunk和module ID
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'app1', filename: 'remoteEntry.js', exposes: { './Button': './src/Button' }, shared: ['react', 'react-dom'] }) ] };
|
8. 如何编写一个webpack plugin
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 40
| class MyWebpackPlugin { constructor(options) { this.options = options; }
apply(compiler) { compiler.hooks.run.tap('MyWebpackPlugin', (compilation) => { console.log('webpack构建开始!'); });
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => { Object.keys(compilation.assets).forEach(filename => { const content = compilation.assets[filename].source(); console.log(`文件:${filename},大小:${content.length}`); }); compilation.assets['custom-file.txt'] = { source: () => 'hello webpack plugin', size: () => 19 }; callback(); });
compiler.hooks.done.tap('MyWebpackPlugin', (stats) => { console.log('webpack构建完成!'); console.log('构建耗时:', stats.endTime - stats.startTime, 'ms'); }); } }
module.exports = MyWebpackPlugin;
|
9. 如何编写一个webpack loader
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 40 41 42
| module.exports = function(source) { const options = this.getOptions(); const result = source.replace(/console\.log/g, 'console.info'); return result; };
module.exports = function(source) { const callback = this.async(); someAsyncOperation(source, (err, result) => { if (err) return callback(err); callback(null, result); }); };
module.exports.raw = true; module.exports = function(source) { return source; };
|
使用自定义loader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { module: { rules: [ { test: /\.js$/, use: [ { loader: path.resolve(__dirname, 'loaders/my-loader.js'), options: { } } ] } ] } };
|
10. 长期缓存优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| module.exports = { output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].chunk.js' }, optimization: { runtimeChunk: 'single', moduleIds: 'deterministic', splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } } };
|
总结
webpack作为前端工程化的重要工具,掌握其核心原理对前端开发至关重要。主要需要理解:
- 核心概念:Entry、Output、Loader、Plugin、Mode
- 构建流程:初始化 → 编译 → 输出
- 优化策略:构建速度优化和打包体积优化
- 高级特性:HMR、Tree Shaking、Code Splitting
- 扩展能力:如何编写Loader和Plugin
在实际项目中,需要根据具体场景选择合适的配置,平衡构建速度、打包体积和开发体验。