「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」。
写在前边
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
中的应用,但是我更加希望在阅读文章之前你可以去点一点上方的文档稍微了解一下前置知识。
流程梳理
在开始之前我们先对于整个打包流程进行一次梳理。
这里仅仅是一个全流程的梳理,现在你没有必要非常详细的去思考每一个步骤发生了什么,我们会在接下来的步骤中去一步一步带你串联它们。
整体我们将会从上边5个方面来分析Webpack
打包流程:
- 初始化参数阶段。
这一步会从我们配置的
webpack.config.js
中读取到对应的配置参数和shell
命令中传入的参数进行合并得到最终打包配置参数。 - 开始编译准备阶段
这一步我们会通过调用
webpack()
方法返回一个compiler
方法,创建我们的compiler
对象,并且注册各个Webpack Plugin
。找到配置入口中的entry
代码,调用compiler.run()
方法进行编译。 - 模块编译阶段
从入口模块进行分析,调用匹配文件的
loaders
对文件进行处理。同时分析模块依赖的模块,递归进行模块编译工作。 - 完成编译阶段
在递归完成后,每个引用模块通过
loaders
处理完成同时得到模块之间的相互依赖关系。 - 输出文件阶段
整理模块依赖关系,同时将处理后的文件输出到
ouput
的磁盘目录中。
接下来让我们详细的去探索每一步究竟发生了什么。
创建目录
工欲善其事,必先利其器。首先让我们创建一个良好的目录来管理我们需要实现的Packing tool
吧!
让我们来创建这样一个目录:
webpack/core
存放我们自己将要实现的webpack
核心代码。webpack/example
存放我们将用来打包的实例项目。webpack/example/webpak.config.js
配置文件.webpack/example/src/entry1
第一个入口文件webpack/example/src/entry1
第二个入口文件webpack/example/src/index.js
模块文件
webpack/loaders
存放我们的自定义loader
。webpack/plugins
存放我们的自定义plugin
。
初始化参数阶段
往往,我们在日常使用阶段有两种方式去给webpack
传递打包参数,让我们先来看看如何传递参数:
Cli
命令行传递参数
通常,我们在使用调用webpack
命令时,有时会传入一定命令行参数,比如:
webpack --mode=production
# 调用webpack命令执行打包 同时传入mode为production
webpack.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
对象的。
此时让我们按照原本的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.exports = webpack;
这里我们需要额外说明的是
webpack
文件中需要导出一个名为webpack
的方法,同时接受外部传入的配置对象。这个是我们在上述讲述过的。
当然关于我们合并参数的逻辑,是将外部传入的对象和执行**shell
**时的传入参数进行最终合并。
在Node Js
中我们可以通过process.argv.slice(2)
来获得shell
命令中传入的参数,比如:
当然_mergeOptions
方法就是一个简单的合并配置参数的方法,相信对于大家来说就是小菜一碟。
恭喜大家