Принцип AST, принцип, позволяющий перевоплотиться в старшего фронтенд-инженера

JavaScript
Принцип AST, принцип, позволяющий перевоплотиться в старшего фронтенд-инженера

1. Абстрактное синтаксическое дерево

Абстрактное синтаксическое дерево

Ядро многих инструментов и библиотек, таких как webpack и Link, основано на концепции абстрактного синтаксического дерева для реализации проверки кода, анализа и других операций.

Вы также можете написать подобные инструменты, понимая концепцию абстрактного грамматического дерева.

2. Использование абстрактного синтаксического дерева

  • Проверка синтаксиса кода, проверка стиля кода, форматирование кода, подсветка кода, подсказки об ошибках кода, автодополнение кода и т. д.
    • Например, JSLint, JSHint для проверки ошибок кода или стиля, поиска возможных ошибок.
    • Подсказки об ошибках IDE, форматирование, выделение, автозаполнение и т. д.
  • обфускация кода
    • UglifyJS2 и т. д.
  • Оптимизируйте код изменений, измените структуру кода для достижения желаемой структуры
    • Инструменты упаковки кода webpack, rollup и т. д.
    • Преобразование между спецификациями кода, такими как CommonJS, AMD, CMD, UMD и т. д.
    • Конвертируйте CoffeeScript, TypeScript, JSX и т. д. в собственный Javascript

3. Определение абстрактного синтаксического дерева

Принцип этих инструментов заключается в преобразовании кода в абстрактное синтаксическое дерево (AST) с помощью синтаксического анализатора Javascript. Это дерево определяет структуру кода. Манипулируя этим деревом, мы можем точно определить местонахождение оператора объявления, оператора присваивания и оператора операции. , И так далее, чтобы добиться анализа кода, оптимизации, изменений и других операций.

В информатике абстрактное синтаксическое дерево (сокращенно абстрактное синтаксическое дерево или AST) или синтаксическое дерево (синтаксическое дерево) представляет собой древовидное представление абстрактной синтаксической структуры исходного кода, в частности исходного кода языка программирования. .

Синтаксис Javascript предназначен для лучшего программирования для разработчиков, но он не подходит для понимания программы. Поэтому его необходимо преобразовать в AST, чтобы сделать его более подходящим для анализа программы.Браузерные компиляторы обычно конвертируют исходный код в AST для дальнейшего анализа и других операций.

var AST = "is Tree";
{
    "type": "Program",
    "body": [{
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [{
            "type": "VariableDeclarator",
            "id": {
                "type": "Identifier",
                "name": "AST"
            },
            "init": {
                "type": "Literal",  //文本格式
                "value": "is Tree",
                "raw": "\"is Tree\""
            }
        }]
    }]
}

https://astexplorer.net/

4. Парсер JavaScript

  • JavaScript Parser, парсер, который преобразует исходный код js в абстрактное синтаксическое дерево.
  • Браузер преобразует исходный код js в абстрактное синтаксическое дерево через парсер, а затем дополнительно преобразует его в байт-код или напрямую генерирует машинный код.
  • Вообще говоря, каждый движок js будет иметь свой собственный формат абстрактного синтаксического дерева, движок Chrome v8, движок FireFox SpiderMonkey и т. д. MDN предоставляет подробное описание формата SpiderMonkey AST, который является отраслевым стандартом.

4.1 Обычно используемые инструменты парсера JavaScript:

  • esprima
  • traceur
  • acorn
  • shift

4.2 esprima

  • Преобразование исходного кода в AST через esprima
  • Пройдите и обновите AST через estraverse
  • Регенерация исходного кода из AST через escodegen
  • инструмент визуализации astexplorer AST
npm install esprima estraverse escodegen
let esprima = require('esprima');           //源代码转成AST语法树
let estraverse = require('estraverse');     //遍历语法树
let escodegen = require('escodegen');       //把AST语法树重新生成代码的工具

let sourceCode = 'function ast(){}'
let ast = esprima.parse(sourceCode);

let indent = 0;
function pad(){
    return "  ".repeat(indent)
}
estraverse.traverse(ast,{
    enter(node){
        console.log(pad() + node.type);
        indent += 2;
    },
    leave(node){
        indent -= 2;
        console.log(pad() + node.type)
    }
})

5. Функция стрелки преобразования

  • Режим посетителя Посетитель Для объекта или группы объектов разные посетители дают разные результаты и выполняют разные операции.
  • @babel/core Компилятор Babel, в нем есть все основные API, такие как общее преобразование, синтаксический анализ
  • Парсер для вавилона Babel
  • babel-typesБиблиотека инструментов, подобная Lodash, для узлов AST, которая содержит методы построения, проверки и преобразования узлов AST, полезные для написания логики для обработки AST.
  • babel-traverseИспользуется для обхода AST, поддержания состояния всего дерева и отвечает за замену, удаление и добавление узлов.
  • babel-types-api
  • Руководство по плагину Babel
  • babeljs.ioвизуальный редактор babel
  • babel-plugin-transform-es2015-arrow-functionsФункция стрелки преобразования Бабеля
npm install @babel/core babel-types -D
let babel = require('@babel/core');     //用来生成语法树,并且遍历转化语法树
let types = require('babel-types');     //用来生成新节点,或者判断某个节点是否是某个类型
const { generate } = require('escodegen');

const sourceCode = `const sum = (a,b) => a+b`;

//插件的结构
let transformArrayFunction = {
    visitor: {  //访问者模式;可以访问源代码生成的语法树所有节点,捕获特定节点
        //捕获箭头函数表达式,转成普通函数
        ArrowFunctionExpression: (path,state) => {
            let id = path.parent.id;        //path.node代表当前节点,path.parent代表父节点
            let arrowNode = path.node;
            let params = arrowNode.params;
            let body = arrowNode.body;      //BinaryExpression
            let generator = arrowNode.generator;
            let async = arrowNode.async;
            //types.blockStatement 生成一个函数体
            let functionExpression = types.functionExpression(id,params,types.blockStatement([types.returnStatement(body)]),generator,async)
            path.replaceWith(functionExpression);       //将转化的新节点替换老节点
        }
    }
}
let result = babel.transform(sourceCode,{
    plugins: [transformArrayFunction],

});

console.log(result);
{
  metadata: {},
  options: {
    babelrc: false,
    configFile: false,
    passPerPreset: false,
    envName: 'development',
    cwd: 'd:\\111前端优选\\TODO\\ast',
    root: 'd:\\111前端优选\\TODO\\ast',
    plugins: [ [Plugin] ],
    presets: [],
    parserOpts: { sourceType: 'module', sourceFileName: undefined, plugins: [] },
    generatorOpts: {
      filename: undefined,
      auxiliaryCommentBefore: undefined,
      auxiliaryCommentAfter: undefined,
      retainLines: undefined,
      comments: true,
      shouldPrintComment: undefined,
      compact: 'auto',
      minified: undefined,
      sourceMaps: false,
      sourceRoot: undefined,
      sourceFileName: 'unknown'
    }
  },
  ast: null,
  code: 'const sum = function sum(a, b) {\n  return a + b;\n};',  //结果
  map: null,
  sourceType: 'module'
}

Работа с синтаксическим деревом состоит из трех шагов:

  • Генерация синтаксического дерева из исходного кода
  • преобразовать синтаксическое дерево
  • Генерация преобразованного кода на основе синтаксического дерева

6. АСТ

1. Процесс анализа

Весь процесс парсинга AST делится на два этапа:

  • Сегментация слов, которая разбивает всю строку кода на массив синтаксических единиц.
  • Синтаксический анализ, установление связи между анализируемыми грамматическими единицами

2. Синтаксический блок

Синтаксические единицы в коде Javascript в основном включают следующее:

  • Ключевые слова:const, пусть, вар и т. д.
  • Идентификатор: может быть переменной, ключевым словом if/else или константой true/false.
  • оператор
  • количество
  • пространство
  • Примечания

3. Лексический анализ

let sourceCode = `let   element   = <h1>hello</h1>`;

/**
 * 1.分词,把token拆开 词法分析,就是把代码转成一个token数组
 */

function lexical(code){
    const tokens = [];
    for(let i=0;i<code.length;i++){
        let ch = code.charAt(i);    //l     i=3 ch=空格
        if(/[a-zA-Z_]/.test(ch)){   //判断是否为合理变量名、标识符
            const token = {type: 'Indentifier',value: ch};
            tokens.push(token);
            for(i++;i<code.length;i++){     //再向后移,判断是不是英文字母
                ch = code.charAt(i);    //i=1 ch=e
                if(/[a-zA-Z_]/.test(ch)){ 
                    token.value += ch;      //value=l value=le value=let
                }else{      //i=3 ch=空格
                    if(token.value == 'let'){
                        token.type = 'KeyWord';
                    }
                    i--;        //将空格回减
                    break;
                }
            }
            continue;
        }else if(/\s/.test(ch)){    //如果ch是空格的话
            const token = {
                type: "WhiteSpace",
                value: " "
            }
            tokens.push(token);
            for(i++;i<code.length;i++){
                ch = code.charAt(i);
                if(/\s/.test(ch)){
                    token.value += ch;
                }else{  //关键字和变量名之间的空格(多个)结束
                    i--;
                    break;
                }
            }
            continue;
        }else if(ch == '='){
            const token = {
                type: "Equal",
                value: "="
            };
            tokens.push(token);
        }else if(ch == '<'){
            const token = {
                type: 'JSXElement',     //遇到小于号,则为JSX元素
                value: ch
            }
            tokens.push(token);         //<h1>hello</h1>
            let isClose = true;         //判断是否遇到闭合标签  <hr/> <h1></h1>
            for(i++;i<code.length;i++){
                ch = code.charAt(i);    //ch = h
                token.value += ch;
                if(ch == "/"){
                    isClose = true;     //遇到斜杠时则下一个大于号则为闭合标签
                }
                if(ch == ">"){          //说明标签结束
                    if(isClose){
                        break;
                    }
                }
            }
            continue;
        }
    }
    return tokens;
}
let tokens = lexical(sourceCode);
console.log(tokens);
/**
[
  { type: 'KeyWord', value: 'let' },
  { type: 'WhiteSpace', value: '   ' },
  { type: 'Indentifier', value: 'element' },
  { type: 'WhiteSpace', value: '   ' },
  { type: 'Equal', value: '=' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'JSXElement', value: '<h1>' },
  { type: 'Indentifier', value: 'hello' },
  { type: 'JSXElement', value: '</h1>' }
]
 */

4. Синтаксический анализ

  • Семантический анализ заключается в составлении объемных комбинаций полученных слов для определения связи между словами.
  • Проще говоря, синтаксический анализ — это идентификация операторов и выражений, что является рекурсивным процессом.
function parse(tokens){
    let ast = {
        type: 'Program',
        body: [],
        sourceType: 'module'
    };
    let i = 0;      //当前的索引
    let currentToken;   //当前的token
    while(currentToken = tokens[i]){
        //第一次的时候 currentToken = { type: 'KeyWord', value: 'let' }
        if(currentToken.type == 'KeyWord' && currentToken.value == 'let'){  //或者var/const
            let VariableDeclaration = {
                type: 'VariableDeclaration',declarations: []
            };
            ast.body.push(VariableDeclaration);
            i+=2;   //{ type: 'Indentifier', value: 'element' },
            currentToken = tokens[i];
            let variableDeclarator = {
                type: 'VariableDeclarator',
                id: {
                    type: 'Indentifier',name: currentToken.value
                },
            }
            VariableDeclaration.declarations.push(variableDeclarator);
            i+=2;  //i=4 //
            currentToken = tokens[i];   //{ type: 'JSXElement', value: '<h1>hello</h1>' },
            if(currentToken.type == "String"){
                variableDeclarator.init = {type: 'StringLiteral',value: currentToken.value}
            }else if(currentToken.type == "JSXElement"){
                let value = currentToken.value;
                let [,type,children] = value.match(/<([^>]+?)>([^>]+)<\/\1>/);   //<h1></h1>  type=h1 children=hello
                variableDeclarator.init = {
                    type: 'JSXElement', //JSX元素
                    openingElement: {
                        type: 'openingElement',
                        name: {
                            type: 'JSXIndetifier',
                            name: type
                        }
                    },
                    closingElement: {
                        type: 'closingElement',
                        name: {
                            type: 'JSXIndentifier',
                            name: type
                        }
                    },
                    children: [
                        {type: 'JSXElement',value: children}
                    ]
                }
            }
        }
        i++;
    }
    return ast;
}

let tokens = [
    { type: 'KeyWord', value: 'let' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'Indentifier', value: 'element' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'Equal', value: '=' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'JSXElement', value: '<h1>hello</h1>' }
]

let ast = parse(tokens);
ast.body[0].declarations[0].init = {
    "type": "ExpressionStatement",
    "expression": {
        "type": "CallExpression",
        "callee": {
            "type": "MemberExpression",
            "computed": false,
            "object": {
                "type": "Indentifier",
                "name": "React"
            },
            "property": {
                "type": "Indentifier",
                "name": "createElement"
            }
        },
        "arguments": [
            {
                "type": "Literal",
                "value": "h1",
                "raw": "\"h1\""
            },
            {
                "type": "Literal",
                "value": null,
                "raw": "null"
            },
            {
                "type": "Literal",
                "value": "hello",
                "raw": "\"hello\""
            }
        ]
    }
}
console.log(JSON.stringify(ast));

/**
 * {"type":"Program","body":[{"type":"VariableDeclaration","declarations":[{"type":"VariableDeclarator","id":{"type":"Indentifier","name":"element"},"init":{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","computed":false,"object":{"type":"Indentifier","name":"React"},"property":{"type":"Indentifier","name":"createElement"}},"arguments":[{"type":"Literal","value":"h1","raw":"\"h1\""},{"type":"Literal","value":null,"raw":"null"},{"type":"Literal","value":"hello","raw":"\"hello\""}]}}}]}],"sourceType":"module"}
 */

До сих пор преобразование синтаксического дерева было реализовано просто, в основном в трех шагах разбора исходного кода в синтаксическое дерево с точки зрения мышления. Благодаря анализу принципа синтаксического дерева нам будет полезно проанализировать принцип Babel, Webpack и других подключаемых модулей компиляции и применять его в повседневной разработке.


Для получения дополнительной информации о внешнем интерфейсе добро пожаловать в поиск и обратите внимание на общедоступную учетную запись [Оптимизация внешнего интерфейса]

前端优选

В этой статье используетсяmdniceнабор текста