文章首发地址你可以在这里找到,快人一步掌握最新知识!
写在前边
Webpack在前端前端构建工具中可以堪称中流砥柱般的存在,日常业务开发、前端基建工具、高级前端面试...任何场景都会出现它的身影。
也许对于它的内部实现机制你也许会感到疑惑,日常工作中基于Webpack Plugin/Loader之类查阅API仍然不明白各个参数的含义和应用方式。
其实这一切原因本质上都是基于Webpack工作流没有一个清晰的认知导致了所谓的“面对API无从下手”开发。
文章中我们会从如何实现模块分析项目打包的角度出发,使用最通俗,最简洁,最明了的代码带你揭开Webpack背后的神秘面纱,带你实现一个简易版Webpack,从此对于任何webpack相关底层开发了然于胸。
这里我们只讲「干货」,用最通俗易懂的代码带你走进webpack的工作流。
我希望你能掌握的前置知识
- Tapable
Tapable包本质上是为我们更方面创建自定义事件和触发自定义事件的库,类似于Nodejs中的EventEmitter Api。
Webpack中的插件机制就是基于Tapable实现与打包流程解耦,插件的所有形式都是基于Tapable实现。
- Webpack Node Api
基于学习目的我们会着重于Webpack Node Api流程去讲解,实际上我们在前端日常使用的npm run build命令也是通过环境变量调用bin脚本去调用Node Api去执行编译打包。
- Babel
Webpack内部的AST分析同样依赖于Babel进行处理,如果你对Babel不是很熟悉。我建议你可以先去阅读下这两篇文章「前端基建」带你在Babel的世界中畅游、# 从Tree Shaking来走进Babel插件开发者的世界。
当然后续我也会去详解这些内容在
Webpack中的应用,但是我更加希望在阅读文章之前你可以去点一点上方的文档稍微了解一下前置知识。
流程梳理
在开始之前我们先对于整个打包流程进行一次梳理。
这里仅仅是一个全流程的梳理,现在你没有必要非常详细的去思考每一个步骤发生了什么,我们会在接下来的步骤中去一步一步带你串联它们。
image.png整体我们将会从上边5个方面来分析Webpack打包流程:
- 初始化参数阶段。
这一步会从我们配置的`webpack.config.js`中读取到对应的配置参数和`shell`命令中传入的参数进行合并得到最终打包配置参数。- 开始编译准备阶段
这一步我们会通过调用`webpack()`方法返回一个`compiler`方法,创建我们的`compiler`对象,并且注册各个`Webpack Plugin`。找到配置入口中的`entry`代码,调用`compiler.run()`方法进行编译。- 模块编译阶段
从入口模块进行分析,调用匹配文件的`loaders`对文件进行处理。同时分析模块依赖的模块,递归进行模块编译工作。- 完成编译阶段
在递归完成后,每个引用模块通过`loaders`处理完成同时得到模块之间的相互依赖关系。- 输出文件阶段
整理模块依赖关系,同时将处理后的文件输出到`ouput`的磁盘目录中。接下来让我们详细的去探索每一步究竟发生了什么。
创建目录
工欲善其事,必先利其器。首先让我们创建一个良好的目录来管理我们需要实现的Packing tool吧!
让我们来创建这样一个目录:
image.pngwebpack/core存放我们自己将要实现的webpack核心代码。webpack/example存放我们将用来打包的实例项目。
`webpack/example/webpak.config.js`配置文件.代码语言:txt复制 `webpack/example/src/entry1`第一个入口文件代码语言:txt复制 `webpack/example/src/entry1`第二个入口文件代码语言:txt复制 `webpack/example/src/index.js`模块文件webpack/loaders存放我们的自定义loader。webpack/plugins存放我们的自定义plugin。
初始化参数阶段
往往,我们在日常使用阶段有两种方式去给webpack传递打包参数,让我们先来看看如何传递参数:
Cli命令行传递参数
通常,我们在使用调用webpack命令时,有时会传入一定命令行参数,比如:
webpack --mode=production
# 调用webpack命令执行打包 同时传入mode为productionwebpack.config.js传递参数
另一种方式,我相信就更加老生常谈了。
我们在项目根目录下使用webpack.config.js导出一个对象进行webpack配置:
const path = require('path')
// 引入loader和plugin ...
module.exports = {
mode: 'development',
entry: {
main: path.resolve(__dirname, './src/entry1.js'),
second: path.resolve(__dirname, './src/entry2.js'),
},
devtool: false,
// 基础目录,绝对路径,用于从配置中解析入口点(entry point)和 加载器(loader)。
// 换而言之entry和loader的所有相对路径都是相对于这个路径而言的
context: process.cwd(),
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js',
},
plugins: [new PluginA(), new PluginB()],
resolve: {
extensions: ['.js', '.ts'],
},
module: {
rules: [
{
test: /.js/,
use: [
// 使用自己loader有三种方式 这里仅仅是一种
path.resolve(__dirname, '../loaders/loader-1.js'),
path.resolve(__dirname, '../loaders/loader-2.js'),
],
},
],
},
};同时这份配置文件也是我们需要作为实例项目example下的实例配置,接下来让我们修改example/webpack.config.js中的内容为上述配置吧。
当然这里的
loader和plugin目前你可以不用理解,接下来我们会逐步实现这些东西并且添加到我们的打包流程中去。
实现合并参数阶段
这一步,让我们真正开始动手实现我们的webpack吧!
首先让我们在webpack/core下新建一个index.js文件作为核心入口文件。
同时建立一个webpack/core下新建一个webpack.js文件作为webpack()方法的实现文件。
首先,我们清楚在NodeJs Api中是通过webpack()方法去得到compiler对象的。
image.png此时让我们按照原本的webpack接口格式来补充一下index.js中的逻辑:
- 我们需要一个
webpack方法去执行调用命令。 - 同时我们引入
webpack.config.js配置文件传入webpack方法。
// index.js
const webpack = require('./webpack');
const config = require('../example/webpack.config');
// 步骤1: 初始化参数 根据配置文件和shell参数合成参数
const compiler = webpack(config);嗯,看起来还不错。接下来让我们去实现一下webpack.js:
function webpack(options) {
// 合并参数 得到合并后的参数 mergeOptions
const mergeOptions = _mergeOptions(options);
}
// 合并参数
function _mergeOptions(options) {
const shellOptions = process.argv.slice(2).reduce((option, argv) => {
// argv -> --mode=production
const [key, value] = argv.split('=');
if (key && value) {
const parseKey = key.slice(2);
option[parseKey] = value;
}
return option;
}, {});
return { ...options, ...shellOptions };
}
module.export = webpack;这里我们需要额外说明的是
webpack文件中需要导出一个名为webpack的方法,同时接受外部传入的配置对象。这个是我们在上述讲述过的。
当然关于我们合并参数的逻辑,是将外部传入的对象和执行**shell**时的传入参数进行最终合并。
在Node Js中我们可以通过process.argv.slice(2)来获得shell命令中传入的参数,比如:
image.png当然_mergeOptions方法就是一个简单的合并配置参数的方法,相信对于大家来说就是小菜一碟。
恭喜大家


