前言
每個編程語言都有自己的AST增炭,了解AST并能進(jìn)行一些開發(fā)款筑,會給我們的項目開發(fā)提供很大的便利轰绵。下面就帶大家一探究竟
通過本文能了解到什么
- JS AST結(jié)構(gòu)和屬性
- babel插件開發(fā)
JS AST簡介
AST也就是抽象語法樹聪铺。簡單來說就是把程序用樹狀形式展現(xiàn)。
每種語言(HTML摄杂,CSS坝咐,JS等)都有自己的AST,而且還有多種AST解析器析恢。
回歸JS本身墨坚,常見的AST解析器有:
- acorn
- @babel/parser
- Typescript
- Uglify-js
- 等等
不同解析器解析出來的AST有些許差異,但本質(zhì)上是一樣的映挂。
本文將基于@babel/parser來進(jìn)行示例和講解
下面來看一句常見的代碼
import ajax from 'axios'
轉(zhuǎn)換后的AST結(jié)構(gòu)如下:
{
"type": "ImportDeclaration",
"start": 0,
"end": 24,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 24
}
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 7,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 11
}
},
"local": {
"type": "Identifier",
"start": 7,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 11
},
"identifierName": "ajax"
},
"name": "ajax"
}
}
],
"importKind": "value",
"source": {
"type": "StringLiteral",
"start": 17,
"end": 24,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 24
}
},
"extra": {
"rawValue": "axios",
"raw": "'axios'"
},
"value": "axios"
}
}
內(nèi)容是不是比想象的多泽篮?莫慌,我們一點一點看柑船。
來一張簡略圖:
ImportDeclaration
語句的類型咪辱,表明是一個import的聲明。
常見的有:
- VariableDeclaration:var x = 'init'
- FunctionDeclaration:function func(){}
- ExportNamedDeclaration:export function exp(){}
- IfStatement:if(1>0){}
- WhileStatement:while(true){}
- ForStatement:for(;;){}
- 不一一列舉
既然是一個引入表達(dá)式椎组,自然分左右兩部分油狂,左邊的是specifiers,右邊的是source
specifiers
specifiers節(jié)點會有一個列表來保存specifier
如果左邊只聲明了一個變量寸癌,那么會給一個ImportDefaultSpecifier
如果左邊是多個聲明专筷,就會是一個ImportSpecifier列表
什么叫左邊有多個聲明?看下面的示例
import {a,b,c} from 'x'
變量的聲明要保持唯一性
而Identifier就是鼓搗這個事情的
source
source包含一個字符串節(jié)點StringLiteral蒸苇,對應(yīng)了引用資源所在位置磷蛹。示例中就是axios
AST是如何轉(zhuǎn)換出來的呢?
以babel為例子:
const parser = require('@babel/parser')
let codeString = `
import ajax from 'axios'
`;
let file = parser.parse(codeString,{
sourceType: "module"
})
console.dir(file.program.body)
在node里執(zhí)行一下溪烤,就能打印出AST
通過這個小示例味咳,大家應(yīng)該對AST有個初步的了解,下面我們談?wù)劻私馑惺裁匆饬x
應(yīng)用場景以及實戰(zhàn)
實際上檬嘀,我們在項目中槽驶,AST技術(shù)隨處可見
- Babel對es6語法的轉(zhuǎn)換
- Webpack對依賴的收集
- Uglify-js對代碼的壓縮
- 組件庫的按需加載babel-plugin
- 等等
為了更好的理解AST,我們定義一個場景鸳兽,然后實戰(zhàn)一下掂铐。
場景:把import轉(zhuǎn)換成require,類似于babel的轉(zhuǎn)換
目標(biāo):通過AST轉(zhuǎn)換,把語句
import ajax from 'axios'
轉(zhuǎn)為
var ajax = require('axios')
要達(dá)到這個效果揍异,首先我們要寫一個babel-plugin全陨。先上代碼
babelPlugin.js代碼如下:
const t = require('@babel/types');
module.exports = function babelPlugin(babel) {
function RequireTranslator(path){
var node = path.node
var specifiers = node.specifiers
//獲取變量名稱
var varName = specifiers[0].local.name;
//獲取資源地址
var source = t.StringLiteral(path.node.source.value)
var local = t.identifier(varName)
var callee = t.identifier('require')
var varExpression = t.callExpression(callee,[source])
var declarator = t.variableDeclarator(local, varExpression)
//創(chuàng)建新節(jié)點
var newNode = t.variableDeclaration("var", [declarator])
//節(jié)點替換
path.replaceWith(newNode)
}
return {
visitor: {
ImportDeclaration(path) {
RequireTranslator.call(this,path)
}
}
};
};
測試代碼:
const babel = require('@babel/core');
const babelPlugin = require('./babelPlugin')
let codeString = `
import ajax from 'axios'
`;
const plugins = [babelPlugin]
const {code} = babel.transform(codeString,{plugins:plugins});
console.dir(code)
輸出結(jié)果:
'var ajax = require("axios");'
目標(biāo)達(dá)成!
babel-plugin
在babel的官網(wǎng)有開發(fā)文檔衷掷,這里只是簡單的描述一下注意要點:
- 插件要求返回一個visitor對象辱姨。
- 可以攔截所有的節(jié)點,函數(shù)名稱就是節(jié)點類型戚嗅,入?yún)⑹莗ath雨涛,可以通過path.node來獲取當(dāng)前節(jié)點
- @babel/types提供了大量節(jié)點操作的API枢舶,同樣可以在官網(wǎng)看的詳細(xì)的說明
transform
這里的代碼大家是不是看著很熟悉。沒錯镜悉,就是.babelrc里的配置祟辟。我們開發(fā)的插件,配置到.babelrc的plugins里侣肄,就可以全局運行了旧困。
寫在最后
JS的AST,給我們提供了實現(xiàn)各種可能得機會稼锅。我們可以自定義一個語法吼具,可以將組件的按需引入過程簡化等等。同時不僅僅是JS矩距,CSS拗盒,HTML,SQL都可以在ast語法級別去進(jìn)行一些有趣的操作锥债。該篇文章只是帶大家簡單入門陡蝇。寫在最后:前端不僅僅是UI,可玩的東西還有很多