Closed axuebin closed 3 years ago
最近在尝试玩一玩已经被大家玩腻的 Babel,今天给大家分享如何用 Babel 为代码自动引入依赖,通过一个简单的例子入门 Babel 插件开发。
Babel
const a = require('a'); import b from 'b'; console.log(axuebin.say('hello babel'));
同学们都知道,如果运行上面的代码,一定是会报错的:
VM105:2 Uncaught ReferenceError: axuebin is not defined
我们得首先通过 import axuebin from 'axuebin' 引入 axuebin 之后才能使用。。
import axuebin from 'axuebin'
axuebin
为了防止这种情况发生(一般来说我们都会手动引入),或者为你省去引入这个包的麻烦(其实有些编译器也会帮我们做了),我们可以在打包阶段分析每个代码文件,把这个事情做了。
在这里,我们就基于最简单的场景做最简单的处理,在代码文件顶部加一句引用语句:
import axuebin from 'axuebin'; console.log(axuebin.say('hello babel'));
简单地说,Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。我们日常开发中,都会通过 webpack 使用 babel-loader 对 JavaScript 进行编译。
ECMAScript 2015+
webpack
babel-loader
JavaScript
首先得要先了解一个概念:抽象语法树(Abstract Syntax Tree, AST),Babel 本质上就是在操作 AST 来完成代码的转译。
AST
了解了 AST 是什么样的,就可以开始研究 Babel 的工作过程了。
Babel 的功能其实很纯粹,它只是一个编译器。
大多数编译器的工作过程可以分为三部分,如图所示:
所以我们如果想要修改 Code,就可以在 Transform 阶段做一些事情,也就是操作 AST。
Code
Transform
我们可以看到 AST 中有很多相似的元素,它们都有一个 type 属性,这样的元素被称作节点。一个节点通常含有若干属性,可以用于描述 AST 的部分信息。
type
比如这是一个最常见的 Identifier 节点:
Identifier
{ type: 'Identifier', name: 'add' }
所以,操作 AST 也就是操作其中的节点,可以增删改这些节点,从而转换成实际需要的 AST。
更多的节点规范可以查阅 https://github.com/estree/estree
AST 是深度优先遍历的,遍历规则不用我们自己写,我们可以通过特定的语法找到的指定的节点。
Babel 会维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法。
Visitor
一个 Visitor 一般是这样:
const visitor = { ArrowFunction(path) { console.log('我是箭头函数'); }, IfStatement(path) { console.log('我是一个if语句'); }, CallExpression(path) {} };
visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法。
visitor
通过上面简单的介绍,我们就可以开始任意造作了,肆意修改 AST 了。先来个简单的例子热热身。
箭头函数是 ES5 不支持的语法,所以 Babel 得把它转换成普通函数,一层层遍历下去,找到了 ArrowFunctionExpression 节点,这时候就需要把它替换成 FunctionDeclaration 节点。所以,箭头函数可能是这样处理的:
ES5
ArrowFunctionExpression
FunctionDeclaration
import * as t from "@babel/types"; const visitor = { ArrowFunction(path) { path.replaceWith(t.FunctionDeclaration(id, params, body)); } };
在开始写代码之前,我们还有一些事情要做一下:
将原代码和目标代码都解析成 AST,观察它们的特点,找找看如何增删改 AST 节点,从而达到自己的目的。
我们可以在 https://astexplorer.net 上完成这个工作,比如文章最初提到的代码:
转换成 AST 之后是这样的:
可以看出,这个 body 数组对应的就是根节点的三条语句,分别是:
body
const a = require('a')
import b from 'b'
console.log(axuebin.say('hello babel'))
我们可以打开 VariableDeclaration 节点看看:
VariableDeclaration
它包含了一个 declarations 数组,里面有一个 VariableDeclarator 节点,这个节点有 type、id、init 等信息,其中 id 指的是表达式声明的变量名,init 指的是声明内容。
declarations
VariableDeclarator
id
init
通过这样查看/对比 AST 结构,就能分析出原代码和目标代码的特点,然后可以开始动手写程序了。
节点规范:https://github.com/estree/estree
我们要增删改节点,当然要知道节点的一些规范,比如新建一个 ImportDeclaration 需要传递哪些参数。
ImportDeclaration
准备工作都做好了,那就开始吧。
我们的 index.js 代码为:
index.js
// index.js const path = require('path'); const fs = require('fs'); const babel = require('@babel/core'); const TARGET_PKG_NAME = 'axuebin'; function transform(file) { const content = fs.readFileSync(file, { encoding: 'utf8', }); const { code } = babel.transformSync(content, { sourceMaps: false, plugins: [ babel.createConfigItem(({ types: t }) => ({ visitor: { } })) ] }); return code; }
然后我们准备一个测试文件 test.js,代码为:
test.js
// test.js const a = require('a'); import b from 'b'; require('c'); import 'd'; console.log(axuebin.say('hello babel'));
我们这次需要做的事情很简单,做两件事:
我们来分析一下 test.js 的 AST,看一下这几个节点有什么特征:
ImportDeclaration 节点的 AST 如图所示,我们需要关心的特征是 value 是否等于 axuebin, 代码这样写:
value
if (path.isImportDeclaration()) { return path.get('source').isStringLiteral() && path.get('source').node.value === TARGET_PKG_NAME; }
其中,可以通过 path.get 来获取对应节点的 path,嗯,比较规范。如果想获取对应的真实节点,还需要 .node。
path.get
path
.node
满足上述条件则可以认为当前代码已经引入了 axuebin 包,不用再做处理了。
对于 VariableDeclaration 而言,我们需要关心的特征是,它是否是一个 require 语句,并且 require 的是 axuebin,代码如下:
require
/** * 判断是否 require 了正确的包 * @param {*} node 节点 */ const isTrueRequire = node => { const { callee, arguments } = node; return callee.name === 'require' && arguments.some(item => item.value === TARGET_PKG_NAME); }; if (path.isVariableDeclaration()) { const declaration = path.get('declarations')[0]; return declaration.get('init').isCallExpression && isTrueRequire(declaration.get('init').node); }
require('c'),语句我们一般不会用到,我们也来看一下吧,它对应的是 ExpressionStatement 节点,我们需要关心的特征和 VariableDeclaration 一致,这也是我把 isTrueRequire 抽出来的原因,所以代码如下:
require('c')
ExpressionStatement
isTrueRequire
if (path.isExpressionStatement()) { return isTrueRequire(path.get('expression').node); }
如果上述分析都没找到代码里引用了 axuebin,我们就需要手动插入一个引用:
import axuebin from 'axuebin';
通过 AST 分析,我们发现它是一个 ImportDeclaration:
简化一下就是这样:
{ "type": "ImportDeclaration", "specifiers": [ "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "axuebin" } ], "source": { "type": "StringLiteral", "value": "axuebin" } }
当然,不是直接构建这个对象放进去就好了,需要通过 babel 的语法来构建这个节点(遵循规范):
babel
const importDefaultSpecifier = [t.ImportDefaultSpecifier(t.Identifier(TARGET_PKG_NAME))]; const importDeclaration = t.ImportDeclaration(importDefaultSpecifier, t.StringLiteral(TARGET_PKG_NAME)); path.get('body')[0].insertBefore(importDeclaration);
这样就插入了一个 import 语句。
import
Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。
Babel Types
Lodash
我们 node index.js 一下,test.js 就变成:
node index.js
import axuebin from "axuebin"; // 已经自动加在代码最上边 const a = require('a'); import b from 'b'; require('c'); import 'd'; console.log(axuebin.say('hello babel'));
如果我们还想帮他再多做一点事,还能做什么呢?
既然都自动引用了,那当然也要自动安装一下这个包呀!
/** * 判断是否安装了某个包 * @param {string} pkg 包名 */ const hasPkg = pkg => { const pkgPath = path.join(process.cwd(), `package.json`); const pkgJson = fs.existsSync(pkgPath) ? fse.readJsonSync(pkgPath) : {}; const { dependencies = {}, devDependencies = {} } = pkgJson; return dependencies[pkg] || devDependencies[pkg]; } /** * 通过 npm 安装包 * @param {string} pkg 包名 */ const installPkg = pkg => { console.log(`开始安装 ${pkg}`); const npm = shell.which('npm'); if (!npm) { console.log('请先安装 npm'); return; } const { code } = shell.exec(`${npm.stdout} install ${pkg} -S`); if (code) { console.log(`安装 ${pkg} 失败,请手动安装`); } }; // biu~ if (!hasPkg(TARGET_PKG_NAME)) { installPkg(TARGET_PKG_NAME); }
判断一个应用是否安装了某个依赖,有没有更好的办法呢?
我也是刚开始学 Babel,希望通过这个 Babel 插件的入门例子,可以让大家了解 Babel 其实并没有那么陌生,大家都可以玩起来 ~
完整代码见:https://github.com/axuebin/babel-inject-dep-demo
前言
最近在尝试玩一玩已经被大家玩腻的
Babel
,今天给大家分享如何用Babel
为代码自动引入依赖,通过一个简单的例子入门Babel
插件开发。需求
同学们都知道,如果运行上面的代码,一定是会报错的:
我们得首先通过
import axuebin from 'axuebin'
引入axuebin
之后才能使用。。为了防止这种情况发生(一般来说我们都会手动引入),或者为你省去引入这个包的麻烦(其实有些编译器也会帮我们做了),我们可以在打包阶段分析每个代码文件,把这个事情做了。
在这里,我们就基于最简单的场景做最简单的处理,在代码文件顶部加一句引用语句:
前置知识
什么是 Babel
简单地说,
Babel
能够转译ECMAScript 2015+
的代码,使它在旧的浏览器或者环境中也能够运行。我们日常开发中,都会通过webpack
使用babel-loader
对JavaScript
进行编译。Babel 是如何工作的
首先得要先了解一个概念:抽象语法树(Abstract Syntax Tree, AST),
Babel
本质上就是在操作AST
来完成代码的转译。了解了
AST
是什么样的,就可以开始研究Babel
的工作过程了。Babel
的功能其实很纯粹,它只是一个编译器。大多数编译器的工作过程可以分为三部分,如图所示:
所以我们如果想要修改
Code
,就可以在Transform
阶段做一些事情,也就是操作AST
。AST 节点
我们可以看到
AST
中有很多相似的元素,它们都有一个type
属性,这样的元素被称作节点。一个节点通常含有若干属性,可以用于描述AST
的部分信息。比如这是一个最常见的
Identifier
节点:所以,操作
AST
也就是操作其中的节点,可以增删改这些节点,从而转换成实际需要的AST
。更多的节点规范可以查阅 https://github.com/estree/estree
AST 遍历
AST
是深度优先遍历的,遍历规则不用我们自己写,我们可以通过特定的语法找到的指定的节点。Babel
会维护一个称作Visitor
的对象,这个对象定义了用于AST
中获取具体节点的方法。一个
Visitor
一般是这样:visitor
上挂载以节点type
命名的方法,当遍历AST
的时候,如果匹配上type
,就会执行对应的方法。操作 AST 的例子
通过上面简单的介绍,我们就可以开始任意造作了,肆意修改
AST
了。先来个简单的例子热热身。箭头函数是
ES5
不支持的语法,所以Babel
得把它转换成普通函数,一层层遍历下去,找到了ArrowFunctionExpression
节点,这时候就需要把它替换成FunctionDeclaration
节点。所以,箭头函数可能是这样处理的:开发 Babel 插件的前置工作
在开始写代码之前,我们还有一些事情要做一下:
分析 AST
将原代码和目标代码都解析成
AST
,观察它们的特点,找找看如何增删改AST
节点,从而达到自己的目的。我们可以在 https://astexplorer.net 上完成这个工作,比如文章最初提到的代码:
转换成
AST
之后是这样的:可以看出,这个
body
数组对应的就是根节点的三条语句,分别是:const a = require('a')
import b from 'b'
console.log(axuebin.say('hello babel'))
我们可以打开
VariableDeclaration
节点看看:它包含了一个
declarations
数组,里面有一个VariableDeclarator
节点,这个节点有type
、id
、init
等信息,其中id
指的是表达式声明的变量名,init
指的是声明内容。通过这样查看/对比
AST
结构,就能分析出原代码和目标代码的特点,然后可以开始动手写程序了。查看节点规范
节点规范:https://github.com/estree/estree
我们要增删改节点,当然要知道节点的一些规范,比如新建一个
ImportDeclaration
需要传递哪些参数。写代码
准备工作都做好了,那就开始吧。
初始化代码
我们的
index.js
代码为:然后我们准备一个测试文件
test.js
,代码为:分析 AST / 编写对应 type 代码
我们这次需要做的事情很简单,做两件事:
AST
中是否含有引用axuebin
包的节点AST
,插入一个ImportDeclaration
节点我们来分析一下
test.js
的AST
,看一下这几个节点有什么特征:ImportDeclaration 节点
ImportDeclaration
节点的AST
如图所示,我们需要关心的特征是value
是否等于axuebin
, 代码这样写:其中,可以通过
path.get
来获取对应节点的path
,嗯,比较规范。如果想获取对应的真实节点,还需要.node
。满足上述条件则可以认为当前代码已经引入了
axuebin
包,不用再做处理了。VariableDeclaration 节点
对于
VariableDeclaration
而言,我们需要关心的特征是,它是否是一个require
语句,并且require
的是axuebin
,代码如下:ExpressionStatement 节点
require('c')
,语句我们一般不会用到,我们也来看一下吧,它对应的是ExpressionStatement
节点,我们需要关心的特征和VariableDeclaration
一致,这也是我把isTrueRequire
抽出来的原因,所以代码如下:插入引用语句
如果上述分析都没找到代码里引用了
axuebin
,我们就需要手动插入一个引用:通过
AST
分析,我们发现它是一个ImportDeclaration
:简化一下就是这样:
当然,不是直接构建这个对象放进去就好了,需要通过
babel
的语法来构建这个节点(遵循规范):这样就插入了一个
import
语句。结果
我们
node index.js
一下,test.js
就变成:彩蛋
如果我们还想帮他再多做一点事,还能做什么呢?
既然都自动引用了,那当然也要自动安装一下这个包呀!
判断一个应用是否安装了某个依赖,有没有更好的办法呢?
总结
我也是刚开始学
Babel
,希望通过这个Babel
插件的入门例子,可以让大家了解Babel
其实并没有那么陌生,大家都可以玩起来 ~完整代码见:https://github.com/axuebin/babel-inject-dep-demo