前言

每個編程語言都有自己的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,可玩的東西還有很多

相關文章