前语

最近在尝试玩一玩现已被咱们玩腻的 Babel,今日给咱们共享怎么用 Babel 为代码主动引进依靠,经过一个简略的比方入门 Babel 插件开发。

需求

consta=require('@ + / * = ) ta');
importbfrom'b';

console.log(at 9 ` ? A i 7xuebin.say('hellobabel'));

同学们都知道,假如运行上面的代码,一定是会报错的:

VM105:2UU / T F X SncaughtReferenceError:axuebinisnoB a r m 0 } 5 (tdefineT F w F  k 2d

咱们得首要经过 import az , ~ { H hxuebin from 'axuebin' 引进 axuebin 之后才干运用。。

为了防止c | ^ t [ ` Q /这种状? % p @ – 况发作(一般来说咱们都会手动引进),或者为你省去引进这个包的费事(其实有些编译器S w 3 a 8 . B {也会帮咱们做了),咱们能够在打包阶段剖析每个代码文件,V b & W O E 4 =把这个工作做了。

在这里,咱们# h B x = c , ~ r就基于最简略的场景做最简略的处理,在代码文件顶部加一句引证句子:

importaxuebinf5 2 grom'axuebin';
console.log(axuebin.say('hellobabel'));

前置知识

什么是 Babel

简略地说,Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。咱O o V !们日常开发中,都会经过 webpacg j l * V c z _k 运用 babel-loader7 m % { ! | UJavaScript 进行I 8 V 9 9编译。

Babel 是怎么作业的

首要得要先了V p n 6 | s N $解一个概e W u q U念:笼统语法树(Abstract Syntax Tree, AST),Babel 本质上便是在操作 AST 来完成代码的转译。

了解了 AST 是什么样的,就能够开端研讨 Babel 的作业过程了。

Babel 的功用其实很朴实,它只是一个编译器。

大多数编译器的作业过程能够分为三部D g _ ; &分,如图所示:

【Babel 小玩具】如何用 Babel 为代码自动引入依赖
  • Parse(解析) 将源代码转换成更加笼统的表明方法(例如笼统语法0 Z k ]树)
  • Transform: B 6 )(转换) 对(笼统语法树)做一些特别处理,让它契合编译器的期望
  • 6 e q 7 Q 9 SGenerata } s Y Me(代码生成) 将第二步经过转换过的(笼统语法树)生成新的代码

所以b : : w b h +咱们假如想要修正 Code,就能够在 Transform 阶段做一些工作,也便是操作 AST

AST 节点

咱们能够看到 AST 中有很多相似的元素,它们d 7 O `都有一个 type 特点,这样的元素被称作节点。一个节点通常含有若干特点,能够用于描述 AST 的部9 C O E g r % g K L ( s信息。

比方这是一个最常见的 Identifier 节点:

{
type:'Identifier',
nam} 3 e U & |e:'add'
}

所以,操作 AST 也便是操作其间的节点T , L Z,能够增修改这些节点,然后转换成实际需求的 AST

更多的节点标准能够查阅 github.com/estree/estr…

AST 遍历

AST 是深度优先遍历的,遍历规矩不必咱们自己写,咱d A M d们能够经过特定的语法找到的指定的节点。

Babel 会维护一个称作 Visitor 的方针,这个方针定义了] X I用于 AST 中获取具体节点的方法。

一个 Visitor 一般是这样:

constvisitor={
ArrowFunction(path){
console.log('我是箭头函数| Y P u f x');
},
IfStatement(path){
consh Aole.log('我是一个if句子');
},
CallExpression(path){}
};

visitor 上挂载以节点 type 命名的8 k x方法,当遍历 AST 的时候,假如匹配上 type,就会执行对应的方法。

操作 AST 的比方

经过上面简略的介绍,咱t 2 . A们就能够开端恣意造作了,k B I W M m g L ,肆意修正 AST 了。先来个简略的比方热热身。

箭头函数是 ES5 不支持的语法,所以 Babel 得把它转换成普通函数,一层层遍历下去,找到了 ArrowFunci J 9 z x T 4 e rtionExpression 节点,这时候就需求把它替换成 FunctionDeclaratio K q I #n 节点。所以,箭头函数可能是这样处理的:

impo6 q ^ J F g ; ]rt*astfe k H [rom"@babel/typT F I w 6 Wes";

constH T % mvisitor={
ArrowFunction(path){
path.replaceW + y l , p G G cWith(t.FunctionDeclaration(id,params,br F ; } l X 9 F Fody));
}
};

开发 Babel 插件的前置作业

在开端写代码之前,咱们还有一些b h n Z # 0 I R工作要做一下:

剖析 AST

原代码方针代码都解析成 A} 7 $ h eST,调查它们的特点,找找看怎么增修改 AST 节点,然后达到自己的目的。

咱们能够在 astexplorer.net 上完成这个作业,比方文章开端说到的代码:

consta=require('a');
importbfrom'b';
console.log(aY 8 Wxuebin.say('helloba4 } Obel'));

转换成 AST 之后是这样的:

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

能够看出,这个 body 数组对应的便是根节点的三– U n % .条句[ X g I } Q子,分别是:

  • VariableDeclaration: const a = require('a')
  • ImportDeclaration: import b from 'b'
  • ExpressionStatement: console.lo& ] ? 8 . :g(axuebin.say(S ? v F d A }'hello bW u 3 ; & r X , `abel'))

咱们能够翻开 VariableDeclara? T f , b ltion 节点看看:

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

它包含了一个 declarations 数组,里面有一个 VariableDeclarator 节点,这个节点有 typeidinit 等信息,其间 i2 U 2 q ( z i .d 指的是表达式声明的变量名,init 指的是声明内容。

经过这样检查/对比 AST 结构,就能分分出原代码方针代码的特点,然后能够开端动手写程序了。

检查节点标准

节点标准:githuO S [ Y Y ,b.cc B T uom/estree/estr…

咱们要增修改节点,当然要知道节点的一些标准,比方新建一个 ImportDeclaration 需求传递哪些参数。

写代码

准备作业都做好了,那就开端吧。

初始化代码

咱们的 inL / [ P l %dex.js 代码为:

//index.js
constpath=require([ [ s @ b C'path');
constfs=require('fs');
constbabel=require('@babel/core');

con6 y B m f Y tstTARGET_PKG_q S p ~ L R yNAME='axuebin';

functiontransform(file, % N /){
constcontent=fs.readFileSync(file,{
encoding:'utf8',
});
const{code}=babel.tran* f | A `sformSync(^ w [ | u f ncontent,{
sourceMaps:fa= ? + ^lse,
plugins:[
babel.createConfigItem(({types:t})=>({
visitor:{
}
}))
]
});
returncode;
}

然后咱们准备一个测验文件 teI I - = @ l w 5 st.js,代码为:

//test.js
consta=require('a');
importbfrom'b';
require('c');
import'% I 7 4 & !d';
console.log(h / vaxuebin.sZ v o n q y $ay('hellobabeK P 2l'));

剖析 AST / 编写对应 type 代码

咱们这次需求做的工作很简略,做两件事:

  1. 寻觅当时 AST 中是D C O , v / D s否含有引证 axuebin 包的节点
  2. 假如没引证J 8 L 9,则修正 AST,刺进一个 ImportDeclaration 节点

咱们来剖析一下 te! # u | ` K Wst.jsAST,看一下这几个节点有什么特征:

ImportDeclaration 节点

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

ImportDeclaration 节点的 AST 如图所示,咱们需求关怀的特征是 valueZ r + U G _否等于 axuebin
代码这样写:

i[ R y % xf(pat] o &h.isImportDeclaration()){
returnpath.get('source').isStringLiteral()&&path.get('source').node.value===TARGET_PKG_NAME;
}

其间,能够经过 path.get 来获取对应节点的 path,嗯,比较标准。假如想获取对应的真实节点,还需求 .node

满意上述条件则能够认为当时代码现已引进了 axuebin 包,不必再做处理了。

VariableDeclaration 节点

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

关于 VariableDeclaration 而言,l ; g l F J : u咱们需求关怀的特征是,它是否是一个 require 句子,而且 require 的是 axuebin,代码如下:

/**
*判别= J x 6是否require了正确的包
*@param{*}node节点
*/

constisTrueRe? R P u 7 Lquire=node=& 3 Q 9>{
const{callee,arguments= h 1 t D h}=node;
returncallee.name==='require'&&arguments.some(item=>item.A q b w ! ) X zvalue===TARGET_PKG_NAME);
};


if(path.isV. , n 1 { bariableDeclara] l &tion()){
constdeclaration=path.get('declaratiu # D yons')[0];
returnd? V 7 } : }eclaratiq m g ` , Von.get('init').isCallExpression&&isTrueRequire(declaration.get('inM + 5 S { c } W 8it')r z m 4.node);
}

ExpressionStatement 节点

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

require('c'),句子咱们一般不会用到,咱们也来看一下吧,它对应的是 ExpressionStatement 节点,咱们需求关怀的特征和 VariableDeclaration 一致,这也是我把 isTr X I p queRequire 抽出来的原因,所以代码如下:

if(path.is^ c XExpressionStatement()){
returnisTrueRequire(path.get('expression').nodQ ( y C 2 ^ Oe);
}

刺进引证句子

假如上述剖析都没找到代码里引证了 axuebin,咱们就需求r 6 $ q手动刺k V 9 L U进一个引证:

importaxuebinfromN c z'axuebin'I z % u m ` x k 4;

经过 AST 剖析W & ( ~,咱们发现它是一个 ImportDeclaration

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

简化一下便是这样:

{
"type":"ImportDeclaration",
"specifiers":[
"type":"ImportDefaultSpecifier",
"local":{
"type":"Identifier",
"name":"axuebin"
}
],
"source":{
"type":"StringLiteral",
"value":"axuebin"
}
}

当然,不是直接构建这个方针放进去就好了,需求E b R / g经过 babel 的语法来构建这个节点(遵从标准):

constip K L k k + 3mportDefaultSpecifier=[t.Impork c L 9 V KtDefaultSpecifier(t.Identifier(TARGET_PKG_NAME))];
constimportDeclaration=t.ImportDeclaration(importDefaultSpecifier,t.StriX l RngLiteral(TARGET_PKG_NAME));
path.get('body')[0].insertBefore(importDeclaration);

这样就刺进了一个 import 句子。

Babew - Q 2 X . E ~ ,l Type7 5 Y 8 M % N O 7s 模块是一个用于 AST 节点的 Lodash 式东西库,它包含– J z了结构、验证以及变换 AST 节点的方法。

成果

咱们 node index.js 一下,test.js 就变成:

import_ + B n +axuebinfrom"axuebin";//现已主动加在代码最上边
consta=require('a');
importbfrom'b';
require('c');
imps Z W g 2 Lort'd';
console.log(axuebin.say('hellobabel'));

彩蛋

假如咱们还想帮他再多做一点事,还能做什) Q { s ) : 7么呢?

既然都主动引证了,那当然也要主^ o 9 : { . , B B动装置一下这个包呀!

/**
*判别是否装置了某个包
*@param{string}pkg包名
*/

consthasPkg=pkg=>{
constpkgPath=path.join(process.cwd(),`package.json`);. E x l @ @ $
constpkgJson=fs.exisy # a M & &tsSync(pkgPath)?fse.readJsonSync(pkgPath):{};
const{depen| v [ y V 5dencies={},devDependencies={}}=pkgJson;
returndependencies[pkg]||devDependenc2 F g F H I & Nies[pkg];
}

/**
*经过npm装置包
*@param{string}pkg包名
*/

constink C . L R V m IstallPkg=pkg=>{
console.log(`开端装置${pkg}`);
constnpm=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);
}

? 2 b J Z m别一个应用是否装置了某个依靠,有没有更好的方法呢?

总结M = L G W [

我也是刚开端学 Babel,期望经过这个 Babels j – m J 0 } X件的入门比方,能够让咱们了解 Babel 其实并没有那么生疏,咱们都能够玩起来 ~

x V { q ( c整代码见:github.e J U g & g H P Hcom/axuebin/bab…

  • Babel 用户手册
  • Babel 插件手册
  • ast 剖析
  • U k H 点标准

沟通评论

欢迎关注大众号前端试炼,大众号平时会共享一些* J / W T , b实用或者有意思的东西,发现代码k * P ` ;之美。专心深度和最佳实践,期望打造一个高质量的大众号。偶尔还会共享一些拍摄 ~

【Babel 小玩具】如何用 Babel 为代码自动引入依赖

大众号后台回复「加群」,拉你进沟通划水聊天群,有看到好文章/代码都会发在群里。

假如你不想加群,只是想加我也是能够。