We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
最近在尝试玩一玩已经被大家玩腻的 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
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
最近在尝试玩一玩已经被大家玩腻的
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
The text was updated successfully, but these errors were encountered: