JavaScrpit AST實(shí)戰(zhàn)

前言

每個(gè)編程語言都有自己的AST,了解AST并能進(jìn)行一些開發(fā),會(huì)給我們的項(xiàng)目開發(fā)提供很大的便利。下面就帶大家一探究竟

通過本文能了解到什么
  1. JS AST結(jié)構(gòu)和屬性
  2. babel插件開發(fā)

JS AST簡(jiǎn)介

AST也就是抽象語法樹。簡(jiǎn)單來說就是把程序用樹狀形式展現(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)容是不是比想象的多?莫慌,我們一點(diǎn)一點(diǎn)看。
來一張簡(jiǎn)略圖:


image.png
ImportDeclaration

語句的類型,表明是一個(gè)import的聲明。
常見的有:
- VariableDeclaration:var x = 'init'
- FunctionDeclaration:function func(){}
- ExportNamedDeclaration:export function exp(){}
- IfStatement:if(1>0){}
- WhileStatement:while(true){}
- ForStatement:for(;;){}
- 不一一列舉
既然是一個(gè)引入表達(dá)式,自然分左右兩部分,左邊的是specifiers,右邊的是source

specifiers

specifiers節(jié)點(diǎn)會(huì)有一個(gè)列表來保存specifier
如果左邊只聲明了一個(gè)變量,那么會(huì)給一個(gè)ImportDefaultSpecifier
如果左邊是多個(gè)聲明,就會(huì)是一個(gè)ImportSpecifier列表
什么叫左邊有多個(gè)聲明?看下面的示例

import {a,b,c} from 'x'

變量的聲明要保持唯一性
而Identifier就是鼓搗這個(gè)事情的

source

source包含一個(gè)字符串節(jié)點(diǎn)StringLiteral,對(duì)應(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
通過這個(gè)小示例,大家應(yīng)該對(duì)AST有個(gè)初步的了解,下面我們談?wù)劻私馑惺裁匆饬x

應(yīng)用場(chǎng)景以及實(shí)戰(zhàn)

實(shí)際上,我們?cè)陧?xiàng)目中,AST技術(shù)隨處可見

  • Babel對(duì)es6語法的轉(zhuǎn)換
  • Webpack對(duì)依賴的收集
  • Uglify-js對(duì)代碼的壓縮
  • 組件庫的按需加載babel-plugin
  • 等等

為了更好的理解AST,我們定義一個(gè)場(chǎng)景,然后實(shí)戰(zhàn)一下。
場(chǎng)景:把import轉(zhuǎn)換成require,類似于babel的轉(zhuǎn)換
目標(biāo):通過AST轉(zhuǎn)換,把語句

import ajax from 'axios'

轉(zhuǎn)為

var ajax = require('axios')

要達(dá)到這個(gè)效果,首先我們要寫一個(gè)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é)點(diǎn)
    var newNode = t.variableDeclaration("var", [declarator])
    //節(jié)點(diǎn)替換
    path.replaceWith(newNode)

  }

  return {
    visitor: {
      ImportDeclaration(path) {
        RequireTranslator.call(this,path)      
      }
    }
  };
};

測(cè)試代碼:

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ā)文檔,這里只是簡(jiǎn)單的描述一下注意要點(diǎn):

  • 插件要求返回一個(gè)visitor對(duì)象。
  • 可以攔截所有的節(jié)點(diǎn),函數(shù)名稱就是節(jié)點(diǎn)類型,入?yún)⑹莗ath,可以通過path.node來獲取當(dāng)前節(jié)點(diǎn)
  • @babel/types提供了大量節(jié)點(diǎn)操作的API,同樣可以在官網(wǎng)看的詳細(xì)的說明
transform

這里的代碼大家是不是看著很熟悉。沒錯(cuò),就是.babelrc里的配置。我們開發(fā)的插件,配置到.babelrc的plugins里,就可以全局運(yùn)行了。

寫在最后

JS的AST,給我們提供了實(shí)現(xiàn)各種可能得機(jī)會(huì)。我們可以自定義一個(gè)語法,可以將組件的按需引入過程簡(jiǎn)化等等。同時(shí)不僅僅是JS,CSS,HTML,SQL都可以在ast語法級(jí)別去進(jìn)行一些有趣的操作。該篇文章只是帶大家簡(jiǎn)單入門。寫在最后:前端不僅僅是UI,可玩的東西還有很多

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容