JavaScrpit AST實戰
前言
每個編程語言都有自己的AST,瞭解AST並能進行一些開發,會給我們的項目開發提供很大的便利。下面就帶大家一探究竟
通過本文能瞭解到什麼
1. JS AST結構和屬性 2. babel插件開發
JS AST簡介
AST也就是抽象語法樹。簡單來說就是把程序用樹狀形式展現。每種語言(HTML,CSS,JS等)都有自己的AST,而且還有多種AST解析器。
迴歸JS本身,常見的AST解析器有:
• acorn • @babel/parser • Typescript • Uglify-js • 等等
爲什麼會有多種解析器?
JS AST並沒有一個統一的規範。各家AST解析器都是爲內部提供服務。所以有各自的解析器也就不難理解了。雖然不同解析器解析出來的AST在結構上有些許差異,但本質上類似。 本文將基於@babel/parser來進行示例和講解
下面來看一句常見的代碼
import ajax from 'axios'
轉換後的AST結構如下:
{
"type": "ImportDeclaration",
"start": 0,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 21
}
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 7,
"end": 10,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 10
}
},
"local": {
"type": "Identifier",
"start": 7,
"end": 10,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 10
},
"identifierName": "Vue"
},
"name": "Vue"
}
}
],
"importKind": "value",
"source": {
"type": "StringLiteral",
"start": 16,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 21
}
},
"extra": {
"rawValue": "vue",
"raw": "'vue'"
},
"value": "vue"
}
}
內容是不是比想象的多?莫慌,我們一點一點看。來一張簡略圖:
ImportDeclaration
語句的類型,表明是一個import的聲明。常見的有:
-
VariableDeclaration:var x = 'init'
-
FunctionDeclaration:function func(){}
-
ExportNamedDeclaration:export function exp(){}
-
IfStatement:if(1>0){}
-
WhileStatement:while(true){}
-
ForStatement:for(;;){}
-
不一一列舉
既然是一個引入表達式,自然分左右兩部分,左邊的是specifiers,右邊的是source
specifiers
specifiers節點會有一個列表來保存specifier
-
如果左邊只聲明瞭一個變量,那麼會給一個ImportDefaultSpecifier
-
如果左邊是多個聲明,就會是一個ImportSpecifier列表。
什麼叫左邊有多個聲明?看下面的示例
import {a,b,c} from 'X'
變量的聲明要保持唯一性 而Identifier就是鼓搗這個事情的
source
source包含一個字符串節點StringLiteral,對應了引用資源所在位置。示例中就是axios
AST是如何轉換出來的呢?
以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裏執行一下,就能打印出AST 通過這個小示例,大家應該對AST有個初步的瞭解,下面我們談談了解它有什麼意義
應用場景以及實戰
實際上,我們在項目中,AST技術隨處可見
-
Babel對es6語法的轉換
讓我們可以提前使用瀏覽器不支持的語法。babel通過AST將語法進行轉換
-
Webpack對依賴的收集
webpack在編譯時,會從入口開始收集所有的依賴。而對依賴的判定也是基於AST的
-
組件庫的按需加載babel-plugin
典型的就是各個組件庫的按需加載,比如element-ui的按需加載babel-plugin
-
等等
爲了更好的理解AST,我們定義一個場景,然後實戰一下。
場景:把import轉換成require,類似於babel的轉換
目標:通過AST轉換,把語句
import ajax from 'axios'
轉爲
var ajax = require('axios')
要達到這個效果,首先我們要寫一個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)
//創建新節點
var newNode = t.variableDeclaration("var", [declarator])
//節點替換
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)
輸出結果:
'var ajax = require("axios");'
目標達成!
babel-plugin
在babel的官網有開發文檔,這裏只是簡單的描述一下注意要點:
• 插件要求返回一個visitor對象。 • 可以攔截所有的節點,函數名稱就是節點類型,入參是path,可以通過path.node來獲取當前節點 • @babel/types提供了大量節點操作的API,同樣可以在官網看的詳細的說明 transform 這裏的代碼大家是不是看着很熟悉。 沒錯,就是.babelrc裏的配置。我們開發的插件,配置到.babelrc的plugins裏,就可以全局運行了。
寫在最後
JS的AST,給我們提供了實現各種可能的機會。我們可以自定義一個語法,可以將組件的按需引入過程簡化等等。同時不僅僅是JS,CSS,HTML,SQL等語言都可以在ast語法級別去進行一些有趣的操作。該篇文章只是帶大家簡單入門。
前端不僅僅是UI,可玩的東西還有很多