前语
最近在尝试玩一玩现已被咱们玩腻的 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 % { ! | U
对 JavaScript
进行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 _ ; &分,如图所示:
-
「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
之后是这样的:
能够看出,这个 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
节点看看:
它包含了一个 declarations
数组,里面有一个 VariableDeclarator
节点,这个节点有 type
、id
、init
等信息,其间 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 代码
咱们这次需求做的工作很简略,做两件事:
-
寻觅当时 AST
中是D C O , v / D s否含有引证axuebin
包的节点 -
假如没引证J 8 L 9,则修正 AST
,刺进一个ImportDeclaration
节点
咱们来剖析一下 te! # u | ` K Wst.js
的 AST
,看一下这几个节点有什么特征:
ImportDeclaration 节点
ImportDeclaration
节点的 AST
如图所示,咱们需求关怀的特征是 value
是Z 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 节点
关于 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 节点
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
:
简化一下便是这样:
{
"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
,期望经过这个 Babel
插s 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 ` ;之美。专心深度和最佳实践,期望打造一个高质量的大众号。偶尔还会共享一些拍摄 ~
大众号后台回复「加群」,拉你进沟通划水聊天群,有看到好文章/代码都会发在群里。
假如你不想加群,只是想加我也是能够。