前言
webpack
在前端工程领域起到了中流砥柱的作用,理解它的内部实现机制会对你的工程建设提供很大的帮助(不论是定制功能还是优化打包)。
下面我们基于 webpack5 源码结构,对整个打包流程进行简单梳理并进行实现,便与思考和理解每个阶段所做的事情,为今后扩展和定制工程化能力打下基础。
一、准备工作
在流程分析过程中我们会简单实现 webpack
的一些功能,部分功能的实现会借助第三方工具:
tapable
提供 Hooks 机制来接入插件进行工作;babel
相关依赖可用于将源代码解析为 AST,进行模块依赖收集和代码改写。
// 创建仓库
mkdir webpack-demo && cd webpack-demo && npm init -y
// 安装 babel 相关依赖
npm install @babel/parser @babel/traverse @babel/types @babel/generator -D
// 安装 tapable(注册/触发事件流)和 fs-extra 文件操作依赖
npm install tapable fs-extra -D
接下来我们在 src
目录下新建两个入口文件和一个公共模块文件:
mkdir src && cd src && touch entry1.js && touch entry2.js && touch module.js
并分别为文件添加一些内容:
// src/entry1.js
const module = require('./module');
const start = () => 'start';
start();
console.log('entry1 module: ', module);
// src/entry2.js
const module = require('./module');
const end = () => 'end';
end();
console.log('entry2 module: ', module);
// src/module.js
const name = 'cegz';
module.exports = {name,
};
有了打包入口,我们再来创建一个 webpack.config.js
配置文件做一些基础配置:
// ./webpack.config.js
const path = require('path');
const CustomWebpackPlugin = require('./plugins/custom-webpack-plugin.js');
module.exports = {entry: {entry1: path.resolve(__dirname, './src/entry1.js'),entry2: path.resolve(__dirname, './src/entry2.js'),},context: process.cwd(),output: {path: path.resolve(__dirname, './build'),filename: '[name].js',},plugins: [new CustomWebpackPlugin()],resolve: {extensions: ['.js', '.ts'],},module: {rules: [{test: /\.js/,use: [path.resolve(__dirname, './loaders/transformArrowFnLoader.js'), // 转换箭头函数],},],},
};
以上配置,指定了两个入口文件,以及一个 output.build
输出目录,同时还指定了一个 plugin
和一个 loader
。
接下来我们编写 webpack
的核心入口文件,来实现打包逻辑。这里我们创建 webpack 核心实现所需的文件:
// cd webpack-demo
mkdir lib && cd lib
touch webpack.js // webpack 入口文件
touch compiler.js // webpack 核心编译器
touch compilation.js // webpack 核心编译对象
touch utils.js // 工具函数
这里我们创建了两个比较相似的文件:compiler
和 compilation
,在这里做下简要说明:
compiler
:webpack 的编译器,它提供的run
方法可用于创建compilation
编译对象来处理代码构建工作;compilation
:由compiler.run
创建生成,打包编译的工作都由它来完成,并将打包产物移交给compiler
做输出写入操作。
对于入口文件 lib/webpack.js
,你会看到大致如下结构:
// lib/webpack.js
function webpack(options) {...
}
module.exports = webpack;
对于执行入口文件的测试用例,代码如下:
// 测试用例 webpack-demo/build.js
const webpack = require('./lib/webpack');
const config = require('./webpack.config');
const compiler = webpack(config);
// 调用run方法进行打包
compiler.run((err, stats) => {if (err) {console.log(err, 'err');}// console.log('构建完成!', stats.toJSON());
});
接下来,我们从 lib/webpack.js
入口文件,按照以下步骤开始分析打包流程。
1、初始化阶段 - webpack
- 合并配置项
- 创建 compiler
- 注册插件
2、编译阶段 - build
- 读取入口文件
- 从入口文件开始进行编译
- 调用 loader 对源代码进行转换
- 借助 babel 解析为 AST 收集依赖模块
- 递归对依赖模块进行编译操作
3、生成阶段 - seal
- 创建 chunk 对象
- 生成 assets 对象
4、写入阶段 - emit
二、初始化阶段
初始化阶段的逻辑集中在调用 webpack(config)
时候,下面我们来看看 webpack()
函数体内做了哪些事项。
2.1、读取与合并配置信息
通常,在我们的工程的根目录下,会有一个 webpack.config.js
作为 webpack
的配置来源;
除此之外,还有一种是通过 webpak bin cli 命令进行打包时,命令行上携带的参数也会作为 webpack 的配置。
在配置文件中包含了我们要让 webpack 打包处理的入口模块、输出位置、以及各种 loader、plugin 等;
在命令行上也同样可以指定相关的配置,且权重高于
配置文件。(下面将模拟 webpack cli 参数合并处理)
所以,我们在 webpack 入口文件这里将先做一件事情:合并配置文件与命令行的配置。
// lib/webpack.js
function we