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';
複製代碼