babel做为现代前端项目的标配,工做中常常会用到。可是,不多人会去研究它的底层实现和设计。这篇文章是平常工做中实践总结,将会由浅入深地和你们一块儿学习下babel的一些基础知识,以及编写属于本身的babel插件,并在项目中使用。php
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构前端
分词 / 词法分析: 将一个语句中的关键词进行提取, 例如let a = 3; 分词提取以后获得let, a, =, 3node
解析 / 语法分析: 在对上面已经被拆分提取过的关键词进行分析以后创建一课语法树(AST)git
底层代码生成: 获得语法树以后执行引擎(例如 chrome 的 v8引擎)会对这颗树进行必定的优化分析, 而后生成更底层的代码或者机器指令交由机器执行github
Babel is a compiler for writing next generation JavaScriptchrome
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from '@babel/generator';
//源代码
const code = `function square(n) {
return n * n;
}`;
//解析为ast结构
const ast = parser.parse(code, {
// parse in strict mode and allow module declarations
sourceType: "module",
plugins: [
// enable jsx and flow syntax
"jsx",
"flow"
]
});
//进行遍历,修改节点
//第二个参数是一个访问者对象,定义遍历时的具体转换规则,囊括本文95%的重点
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
//将修改后的ast结构,生成从新的代码
const output = generate(ast, { /* options */ }, code);
复制代码
整个流程最核心的就是traverse部分,接下来咱们回顾下traverse的核心知识npm
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,一般也叫作“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,而后返回给你新生成的代码编程
Babel 或是其余编译器中最复杂的过程 同时也是插件将要介入工做的部分。小程序
首先熟悉下常见的js结构对应的ast节点类型数组
//functionDeclaration
function square(n) {
return n * n;
}
let a = {
test(){}, //ObjectMethod
setOnly: function(){} //ObjectProperty
}
let b = 3; //VariableDeclaration
b = 5; //AssignmentExpression
复制代码
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法
const MyVisitor = {
//完整写法
functionDeclaration: {
enter(path) {
console.log("Entered!");
},
exit(path) {
console.log("Exited!");
}
},
//经常使用写法
functionDeclaration(path){
},
ObjectMethod | ObjectProperty: {
enter(path) {
console.log("Entered!");
},
exit(path) {
console.log("Exited!");
}
}
...
};
复制代码
visitor对象每次访问节点方法时,都会传入一个path参数。Path 是表示两个节点之间链接的对象。这个对象不只包含了当前节点的信息,也有当前节点的父节点的信息,同时也包含了添加、更新、移动和删除节点有关的其余不少方法
+ 属性
- node 当前节点
- parent 父节点
- parentPath 父path
- scope 做用域
- context 上下文
- ...
+ 方法
- findParent 向父节点搜寻节点
- getSibling 获取兄弟节点
- replaceWith 用AST节点替换该节点
- replaceWithSourceString 用代码字符串替换该节点
- replaceWithMultiple 用多个AST节点替换该节点
- insertBefore 在节点前插入节点
- insertAfter 在节点后插入节点
- remove 删除节点
复制代码
注意:js中不一样的数据类型,对应的ast节点信息也不竟相同。以图中为例,externalClasses对象的节点信息中类型(type)是ObjectProperty,包含key ,value等关键属性(其余类型节点可能就没有)
注意选择最新的babel7版本,否则下面例子中的类型会匹配不上,
假设咱们的目标是要把properties属性中key为‘current’的属性改成myCurrent。let's go!
原始代码:
/*eslint-disable*/
/*globals Page, getApp, App, wx,Component,getCurrentPages*/
Component({
externalClasses: ['u-class'],
relations: {
'../tab/index': {
type: 'child',
linked() {
this.changeCurrent();
},
linkChanged() {
this.changeCurrent();
},
unlinked() {
this.changeCurrent();
}
}
},
properties: {
current: {
type: String,
value: '',
observer: 'changeCurrent'
}
},
methods: {
changeCurrent(val = this.data.current) {
let items = this.getRelationNodes('../tab/index');
const len = items.length;
if (len > 0) {
items.forEach(item => {
item.changeScroll(this.data.scroll);
item.changeCurrent(item.data.key === val);
item.changeCurrentColor(this.data.color);
});
}
},
emitEvent(key) {
this.triggerEvent('change', { key });
}
}
});
复制代码
首先在原始代码中选中'current',查看右边ast的节点结构,如图:
这是一个对象属性(ObjectProperty),关键节点信息为key和value,key自己也是一个ast节点,类型为Identifier(准确的应该是StringIdentifer,经常使用的还有NumberIdentifer等),'curent'是里面的name属性。因此咱们的第一步就是找到改节点,而后修改它。
export default function (babel) {
const { types: t } = babel;
return {
name: "ast-transform", // not required
visitor: {
Identifier(path) {
//path.node.name = path.node.name.split('').reverse().join('');
},
ObjectProperty(path) {
if (path.node.key.type === 'StringIdentifier' &&
path.node.key.name === 'current') {
console.log(path,'StringIdentifier')
}
}
}
};
}
复制代码
这里须要用到@babel/types
babeljs.io/docs/en/bab…来辅助咱们进行类型判断,开发中会很是依赖这个字典进行查找
在控制台会看见,path下面的节点信息不少,关键字段为node和parentPath,node记录了该节点下数据信息,例如以前提到过的key和value。parentPath表明父级节点,此例中表示ObjectExpression中properties节点信息,有时咱们须要修改父节点的数据,例如常见的节点移除操做。接下来咱们修改该节点信息。
在@babel/types
中找到该ObjectProperty的节点信息以下,咱们须要须要构造一个新的同类型节点(ObjectProperty)来替换它。
ObjectProperty(path) {
console.log(path,'ObjectProperty--')
if (path.node.key.type === 'Identifier' &&
path.node.key.name === 'current') {
//替换节点
path.replaceWith(t.objectProperty(t.identifier('myCurrent'), path.node.value));
}
}
复制代码
其中咱们用到了replaceWith方法,这个方法表示用一个ast节点来替换当前节点。 还有一个经常使用的replaceWithSourceString方法,表示用一个字符串来代替该ast节点,参数为一串代码字符串,如:'current : {type:String};',感兴趣的,能够本身试试。
最后查看转换后的代码,发现'current'已经被咱们替换成了'myCurrent'。
到这里,一个完整的例子就演示完了。这里补充说明一下,在实际中可能会遇到嵌套结构比较深的ast结构。咱们须要嵌套类型判断,好比:
ObjectProperty(path) {
console.log(path,'ObjectProperty--')
MemberExpression(memberPath) {
console.log(path,'memberPath--')
}
}
复制代码
由于遍历中的path指定的是当前匹配的节点信息。因此能够为不一样的类型遍历指定不一样的path参数,来获取当前遍历的节点信息,避免path覆盖,例如上面的path和memberPath。
到这里,babel的基本用法就差很少介绍完了,想要熟练掌握,还须要你在项目中反复练习和实践。想系统学习babel,并在实际项目中使用的同窗能够先看看这篇babel的介绍文档,边写边查,巩固学习
自定义组件不支持relations
的关系申明
不支持getRelationNodes
的API调用
transition动画数据结构不一样
onLaunch
, onShow
, onLoad
中不支持使用selectComponent
和selectAllComponents
微信的wxs语法
登陆流程,百度系使用passport,非百度系使用Oauth
以
relations
为例,进行演示,完整项目请查看互转工程项目
relations: {
'./custom-li': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期以后
},
linkChanged: function(target) {
// 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期以后
},
unlinked: function(target) {
// 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期以后
}
}
},
复制代码
let linkedBody = '';
if (path.node.type === 'ObjectProperty' && path.node.key.name === 'relations') {
//获取到relations属性中type的value
//获取到relations属性中linked函数
let componentName = '';
let relationsValue = '';
path.traverse({
ObjectMethod(path) {
if (path.node.key.name === 'linked') {
linkedBody = path.node.body;
}
},
ObjectProperty(path) {
if (path.node.key.type === 'StringLiteral' && path.node.key.value) {
relationsValue = path.node.key.value || '';
let index = relationsValue.lastIndexOf('./');
let lastIndex = relationsValue.lastIndexOf('/');
componentName = relationsValue.substring(index + 2, lastIndex);
}
// '../grid/index' ⇒ 'grid'
if (path.node.key.name === 'type') {
if (context.isDesgin) {
//添加组件库前缀
componentName = 'u-' + componentName;
}
let action = path.node.value.value === 'parent' ? 'relationComponentsParent' : 'relationComponentsChild';
contextStore.dispatch({
action,
payload: componentName
});
relationsMap[relationsValue] = path.node.value.value;
}
}
});
if (!linkedBody) {
path.remove();
return;
} else {
path.replaceWith(t.objectMethod('method', t.identifier('attached'), [], linkedBody, false));
}
}
复制代码
使用组件库的时候,不想打包全部组件,只打包项目中引入的组件
visitor: {
ImportDeclaration(path, {opts})
{
const specifiers = path.node.specifiers;
const source = path.node.source;
// 判断传入的配置参数是不是数组形式
if (Array.isArray(opts)) {
opts.forEach(opt => {
assert(opt.libraryName, 'libraryName should be provided');
});
if (!opts.find(opt => opt.libraryName === source.value)) return;
} else {
assert(opts.libraryName, 'libraryName should be provided');
if (opts.libraryName !== source.value) return;
}
const opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts;
opt.camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined'
? false
: opt.camel2UnderlineComponentName;
opt.camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined'
? false
: opt.camel2DashComponentName;
if (!t.isImportDefaultSpecifier(specifiers[0]) && !t.isImportNamespaceSpecifier(specifiers[0])) {
// 遍历specifiers生成转换后的ImportDeclaration节点数组
const declarations = specifiers.map((specifier) => {
// 转换组件名称
const transformedSourceName = opt.camel2UnderlineComponentName
? camel2Underline(specifier.imported.name)
: opt.camel2DashComponentName
? camel2Dash(specifier.imported.name)
: specifier.imported.name;
// 利用自定义的customSourceFunc生成绝对路径,而后建立新的ImportDeclaration节点
return t.ImportDeclaration([t.ImportDefaultSpecifier(specifier.local)],
t.StringLiteral(opt.customSourceFunc(transformedSourceName)));
});
// 将当前节点替换成新建的ImportDeclaration节点组
path.replaceWithMultiple(declarations);
}
}
}
复制代码
而后安装babel-cli工具,将代码打包,发布到npm,就能够在项目中使用了。若是再优化完善下,是否是就能够把现有项目中ant-design的按需加载功能移除了。。。
在项目中设置.babelrc
文件,增长自定义插件配置
//以前
import { button, table } from 'union-design';
//如今
import button from 'union-design/src/components/button/index.js';
import table from 'union-design/src/components/table/index.js';
复制代码