写在最前
看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。
关于webapck
webpack是现在前端开发当中打交道最多的角色,初次接触时,难免被它的配置项、概念所困扰。
webpack对于很多人来说是一个黑盒工具(你并不知道里面的实现),掌握基本的配置,和一些优化相关的配置就可以处理大部分日常工作的内容
而本文(上篇)将从webpack构建产物分析、源码分析再到代码框架的搭建讲述如何实现一个丐版的webpack。
webpack为啥能成为前端工具链的核心角色
我认为主要原因是解决了如下痛点:
- 模块化,支持CommonJs、ESModule等模块化规范的同时,将各种资源,如字体,图片,css等也视为模块
- 生态系统,众多的Plugins/Loaders使得前端开发更加规范、方便、高效
- 打包构建,前端CI/CD过程的重要工具
可以说wbpack是前端工程化的利器,我们编写的代码,经过webpack的处理,能成功在浏览器、node等运行时环境上执行
回顾webpack的使用
前面了解webpack 的作用,现在回顾一下webpack的使用
本着极简实现的原则,这里只针对js
,以及使用commonJs
规范
- 首先webpack安装
bash
npm init
npm install webpack webpack-cli --save-dev
- 根目录下创建
webpack.config.js
文件
javascript
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js', // 输出文件名
path: path.resolve(__dirname, 'dist') // 输出目录
}
};
这里涉及到一些概念,可以自行回顾
- entry
- output
- loader
- plugin
- 在
package.json
当中配置scripts,用来启动webpack
json
{
"scripts": {
"build:dev": "webpack --mode",
"build": "webpack --mode production"
},
}
- 编写一些js代码
javascript
// src/index.js
const bar = require('./bar');
const foo = require('./foo');
console.log(bar.add(1,1))
console.log(foo.name)
// src/bar.js
function add(a, b) {
return a + b;
}
module.exports = {
add,
};
// src/foo.js
const name = "JetTsang"
exports.name = name
此时的依赖关系为:
- 打包,在终端中执行
webpack
命令 这里使用development
模式,默认使用devtool:"eval"
,方便我们看打包产物
bash
npm run build:dev
此时的目录结构大概是这样子
lua
my-project/
├── src/
│ ├── index.js
│ ├── foo.js
│ └── bar.js
├── dist/
├── node_modules/
├── package.json
├── package-lock.json
└── webpack.config.js
再来看看产出的结果:
bundle.js分析
分析一下产出结果:
-
首先是一个自执行函数,即
(function() { ... })()
,这是为了创建一个独立的作用域,避免变量污染。 这里写成箭头函数 -
在函数内部,首先定义了一个
__webpack_modules__
对象,它包含了所有被打包的模块。
这里对换行做了处理,方便阅读 可以看到这个对象是以路径
作为key
,value
则是一个箭头函数:(module,exports,webpack_require)=>{eval(/*你写的代码*/)}
eval执行的是我们打包好的代码,代码里用到的
commonJs
规范里的require
、module
、exports
正是通过这个函数的入参传入
ps: 为了避免混淆,require
函数被webpack处理成它自己的__webpack_require__
-
函数内部,接下来是一个
__webpack_module_cache__
对象,用于缓存已加载的模块,避免重复加载 以及解决模块之间循环引用造成的问题。 -
紧接着定义了一个
__webpack_require__
函数,它是Webpack的模块加载器。入参moduleId,即为路径 ,同时通过闭包引用了前面的__webpack_module_cache__
-
最后一行,通过调用
__webpack_require__("./src/index.js")
来加载入口模块
以上这部分可以称作是webpack的运行时
webpack的工作流程
对webpack的产物有了一定了解之后,再梳理一下webpack的工作流程,就可以动手开始实现了。
那么webpack的工作流程大致可以分为如下
- 获取配置(入口/loaders/plugins等等)生成compiler
- 解析入口,调用对应的loader,构建AST语法树,同时整理出一份依赖图,在必要的时机调用loader/plugin
- 将产物放到配置好的位置
参考webpack的设计框架
要实现它,我们首先需要参考一下它的源码。 找到node_modules/webpack/package.json
,这里的main字段描述了它的入口
在入口lib/index.js
当中可以看到这里做了合并导出
其核心是在lib/webpack.js
,webpack是一个函数,返回的是一个compiler
实例
这个compiler
主要就是基于事件流来管理整个打包流程的,也就是webpack的核心,
它里面也有生命周期钩子hook
,在打包的过程当中,可以在适当的生命周期去call对应的hook,这种解耦的关系就是webpack的plugins系统,这里的生命周期可以理解为跟Vue/React类似。
事件的发布和订阅使用了tapable这个库,它更专注于nodeJs环境
再来看看里面的核心方法:run,它就是用来启动编译的
在run方法当中,核心是这个run箭头函数,里面调用了this.compile去编译代码
再来看看 this.compile 当中,真正干活的是Compilation
这个Compilation
是核心模块,这里面的实现比较复杂,主要是负责每一个具体的构建过程,它主要的功能是:
-
资源管理(根据配置从入口模块开始逐步解析处理所有的资源)、
-
依赖解析(构建依赖图 moduleGraph)、
-
打包输出(根据chunkGraph产出),
-
还有错误处理以及插件系统(通过调用对应时机的hook)
可以说compiler
是整个webpack的编译器实例,负责读取配置、管理插件,而compilation
则是每次构建的具体过程,负责管理资源、解析依赖、生成输出文件。
关系示意图大概如此:
lua
+-------------------+
| Compiler |
| |
| - Compilation 1 |
| - Compilation 2 |
| - ... |
+-------------------+
搭建代码框架
让我们在项目中新建lib
目录,里面是我们实现的webpack源码
里面的代码组织如下
lib/webpack.js
javascript
const { Compiler } = require('./Compiler.js')
function webpack(webpackOptions) {
const compiler = new Compiler()
return compiler;
}
module.exports = webpack
lib/Compiler.js
javascript
class Compiler {
constructor(config) {
this.entry = config.entry;
this.output = config.output;
this.module = config.module;
this.plugins = config.plugins;
// 利用tapable定义一些hook
this.hooks = {
run: new SyncHook(), //会在编译刚开始的时候触发此run钩子
done: new SyncHook(), //会在编译结束的时候触发此done钩子 };
}
run(){
// 这里创建Compilation实例
const compilation = new Compilation();
}
}
module.exports = {
Compiler
}
lib/Compilation.js
javascript
class Compilation {
constructor({ module, output }) {
this.loaders = module.rules;
this.output = output;
this.graph = [];
}
// 这里开始构建
build(){
}
接着在根目录下,新建packing.js
,这个是用来启动启动手写的webpack
javascript
const webpack = require("./lib/webpack"); //手写webpack
const webpackOptions = require("./webpack.config.js"); //这里一般会放配置信息
const compiler = webpack(webpackOptions);
compiler.run((err, stats) => {
console.log(err);
});
此时项目目录结构如下:
最后是依赖介绍
虽然说是手写,但有些轮子不需要我们去造,着重了解webpack的实现即可
-
tapable:之前提到的,用来做事件流的管理,是插件系统的核心
-
@babel/parser:babel 提供的 Javascript 代码解析器。它可以将 Javascript 代码转换为 ast(抽象语法树),方便后续的代码处理和转换
-
@babel/traverse:用于对输入的抽象语法树(ast)进行遍历。在这里主要是定位到require,从而构建依赖图
-
@babel/core:babel 的核心库,它提供了 babel 的编译器和 API 接口,用于将源代码转换为目标代码
-
@babel/preset-env:可根据配置的目标浏览器或者运行环境来自动将 ES6 + 的代码转换为 ES5
-
ejs: 用于生成模板代码或者读取模板文件,上面的webpack运行时就需要这个依赖去读取
zsh
npm install -D @babel/parser @babel/traverse @babel/core @babel/preset-env ejs tapable
结尾
在本文当中,为了实现webpack,介绍了webpack的产物和源码思路,并且在最后准备了我们需要的代码框架
在下篇当中,会去逐步的实现一个迷你版本的webpack。