构建工具-webpack插件开发2-进阶
前情提要
前文【构建工具-webpack插件开发1-入门】(点击访问)实现了一个简单插件:在构建完成后生成一个markdown文件,文件里会记录构建时的日期。
前文的插件太初级了,它甚至不配被起个名字。但对新手叩开webpack插件开发之门至关重要,因为它摒弃掉了所有妨碍理解的部分。
本文会在理解最基础的webpack插件开发之上,进阶一下,开发个上的了台面的Babel插件。
前置知识(可略过)
本文视图尽可能多的引入常用的工程化工具、概念,试图通过“五脏六腑”充盈本插件,包括:Babel、AST、TSC
概念回顾
AST(Abstract Syntax Tree)抽象语法树
**具象形式:**以 JSON 形式将数据保存在的一棵树状结构里。
**作用:**是 Babel
、tsc
等编译器 进行代码转换的核心数据结构,表示了源代码的抽象语法结构,以树状的形式表现编程语言的语法结构,每个节点都表示源代码中的一种结构。使得 Babel
能够理解和操作代码。
编译器compiler
是一种重要的系统软件,负责将一种高级语言编写的程序转换成一种等价的、低级语言(目标语言)编写的程序。
原理图:
Babel 【JS编译器】
名称来源:巴别塔,或意译为通天塔,本是犹太教《塔纳赫·创世纪篇》中的一个故事,说的是人类产生不同语言的起源。在这个故事中,一群只说一种语言的人在“大洪水”之后从东方来到了示拿地区,并决定在这修建一座城市和一座“能够通天的”高塔;上帝见此情形就把他们的语言打乱,让他们再也不能明白对方的意思,并把他们分散到了世界各地。
**作用:**将 es6+的 JavaScript 代码编译成 目标环境支持的语法环境,并且对目标环境不支持的 api 自动 polyfill。编译过程主要分为三个阶段 (解析,转换,生成)
TSC(TypeScript Compiler)【TS编译器】
作用: typescript 官方的编译器,可以将 typescript 代码转换为 JavaScript 代码,也可以像 Babel 那样对代码进行编译,同样使用了 AST 作为代码转换和优化的核心数据结构。
Babel与TSC区别
大体概括为:
- tsc 可以对代码进行类型检查,babel 不能
- tsc 可以输出类型声明文件(.d.ts),babel 不能
- tsc 可以导出非 const 的值,babel 不能
- 与 tsc 相比,babel支持更多的语言特性,兼容性更强
- babel 的编译速度要比 tsc 更快
整体对比:
功能点 | tsc | babel |
---|---|---|
源码转换成 AST 的 Parser | ✅ | ✅ |
语义分析(作用域分析) | ✅ | ✅ |
类型检查 | ✅ | ❌ |
AST 的 transform | ✅ | ✅ |
使用 Generator(或者 Emitter)生成目标代码和sourcemap | ✅ | ✅ |
过时的 export = import = 的模块语法 | ❌ | ❌ |
const enum | ✅ | ❌ (会作为enum处理) |
namespace 的跨文件合并 | ❌ | ✅ |
导出非 const 的值 | ✅ | ❌ |
支持更多的语言特性 | ❌ (支持最新的es标准特性和部分草案特性) | ✅ |
没有做 polyfill 的处理 | ❌ 需要全量引入polyfill | ✅ |
功能介绍
Babel的核心内容
Babel的编译过程
parse(解析) 阶段: 通过**@babel/parser**将代码转化为 AST
transform(转换)阶段: 通过 @babel/traverse对 AST 进行操作
generate(生成)阶段: 通过 @babel/generator将 AST 转化为源代码,并生成 source-map
插件实现
创建一个简单的 Babel 插件
一个 Babel 插件其实就是一个函数,它接收一个包含 types
工具箱的对象作为参数,并返回一个对象,该对象包含一个 visitor
属性,visitor
属性是一个对象,其方法会在遍历 AST 时被调用。
这个插件将会把 ES6 的 **
运算符转换为 Math.pow
函数调用。以下是插件的代码示例:
// transform-to-mathpow.js
module.exports = function(babel) {
const { types: t } = babel;
return {
name: "transform-to-mathpow",
visitor: {
BinaryExpression(path) {
if (path.node.operator === "**") {
const mathpowAstNode = t.callExpression(
t.memberExpression(t.identifier("Math"), t.identifier("pow")),
[path.node.left, path.node.right]
);
path.replaceWith(mathpowAstNode);
}
},
},
};
};
在这个插件中,我们首先检查当前节点是否为二元表达式,并且操作符是否为 **
。如果是,我们就创建一个新的 CallExpression
节点,表示 Math.pow
函数的调用,并将原来的二元表达式的左右两边作为参数传递给 Math.pow
。最后,我们用新的 CallExpression
节点替换原来的二元表达式节点。
使用 babel 插件
要使用上面创建的插件,我们需要在项目的配置文件(babel.config.json 或者 .babelrc)中添加它:
{
"plugins": ["./transform-to-mathpow.js"]
}
Babel 预设和插件的执行顺序
Babel 插件和预设的执行顺序是其在转换 JavaScript 代码过程中的一个重要方面
预设(Presets)的执行顺序
- 逆序执行:与插件不同,预设是按照配置文件中声明的顺序逆序执行的。这意味着最后一个声明的预设会首先执行,然后是倒数第二个,依此类推。
- 内部插件顺序:每个预设内部通常包含了一系列插件。这些插件的执行顺序由预设本身定义,并且遵循预设内部的规则。
插件(Plugins)的执行顺序
- 在所有预设执行完之后,才轮到插件执行
- 正序执行:Babel 会按照配置文件中插件声明的顺序,依次执行这些插件。这意味着第一个声明的插件会首先执行,然后是第二个,依此类推。
- 交替调用:在遍历抽象语法树(AST)的过程中,Babel 会交替调用不同插件的处理函数。当遇到某个节点类型时,Babel 会依次执行所有插件中针对该节点类型的处理函数。
- enter 和 exit 阶段:对于每个节点,Babel 提供了两个处理时机:enter 和 exit。enter 阶段表示进入节点时执行的处理,而 exit 阶段表示离开节点时执行的处理。插件可以选择在 enter 或 exit 阶段,或者两个阶段都进行处理。
示例说明
假设有以下 Babel 配置文件:
{
"presets": ["preset-a", "preset-b"],
"plugins": ["plugin-1", "plugin-2"]
}
在这个配置中:
- 预设的执行顺序将是
preset-b
(先执行),然后是preset-a
(后执行)。 - 插件的执行顺序将是
plugin-1
(先执行),然后是plugin-2
(后执行)。 preset-a
和preset-b
内部包含的插件将分别按照它们各自的内部规则执行。
流程图: