美文网首页
JavaScrpit AST实战

JavaScrpit AST实战

作者: cd2001cjm | 来源:发表于2020-07-03 18:08 被阅读0次

    前言

    每个编程语言都有自己的AST,了解AST并能进行一些开发,会给我们的项目开发提供很大的便利。下面就带大家一探究竟

    通过本文能了解到什么
    1. JS AST结构和属性
    2. babel插件开发

    JS AST简介

    AST也就是抽象语法树。简单来说就是把程序用树状形式展现。
    每种语言(HTML,CSS,JS等)都有自己的AST,而且还有多种AST解析器。

    回归JS本身,常见的AST解析器有:

    • acorn
    • @babel/parser
    • Typescript
    • Uglify-js
    • 等等

    不同解析器解析出来的AST有些许差异,但本质上是一样的。
    本文将基于@babel/parser来进行示例和讲解

    下面来看一句常见的代码

    import ajax from 'axios'
    

    转换后的AST结构如下:

    {
            "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"
            }
          }
    

    内容是不是比想象的多?莫慌,我们一点一点看。
    来一张简略图:


    image.png
    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语法的转换
    • Webpack对依赖的收集
    • Uglify-js对代码的压缩
    • 组件库的按需加载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,可玩的东西还有很多

    相关文章

      网友评论

          本文标题:JavaScrpit AST实战

          本文链接:https://www.haomeiwen.com/subject/izpsqktx.html