Práctica de ingeniería front-end: inicio rápido de la agitación de árboles

sacudir árboles       

La esencia del treeshaking es eliminar algoritmos de código redundantes. En la optimización del rendimiento del front-end, es6 introduce el mecanismo de vibración del árbol, que significa que cuando introducimos otros módulos en el proyecto, automáticamente eliminará el código que no usamos o el código que nunca se ejecutará. En Uglify Stage detectado, no empaquetado en el paquete.

        El propósito de aprender a sacudir árboles también es sentar las bases para aprender el principio del embalaje enrollado más adelante. En resumen, un archivo es un módulo. Cada módulo generará un árbol de abstracción de sintaxis AST de acuerdo con el código del archivo, y el resumen debe analizar cada nodo AST. Analizar un nodo AST es ver si el nodo llama a una función o método. Si es así, verifique si la función o método llamado está en el alcance actual; de lo contrario, busque hasta encontrar el alcance de nivel superior del módulo. Si no se encuentra este módulo, significa que esta función y método dependen de otros módulos y deben importarse desde otros módulos. El resumen solo procesa funciones y variables de importación/exportación de nivel superior.

0 conocimientos previos: dependencias relacionadas con la instalación

Instalar paquete web

npm instala el paquete web paquete web-cli --save-dev

Inicializar el proyecto

inicio npm -y

Ver el contenido empaquetado del paquete web

paquete web npx ./test1.js

instalar nodemon

nodemon es una herramienta para ayudar al desarrollo de node.js. Cuando los archivos en el directorio cambian, reiniciará automáticamente la aplicación del nodo

npm i nodemon -g

Comando de código de prueba: nodemon ./index.js

Parámetros: reloj

instalar bellota

npm i bellota -d -s

const acorn = require('acorn');

El uso predeterminado de bellota es muy simple: simplemente analice la cadena de código y obtenga la estructura AST:

let acorn = require("acorn");

console.log(acorn.parse("for(let i=0;i<10;i+=1){console.log(i);}", {ecmaVersion: 2020}));

Árbol de sintaxis AST analizado

Node {
  type: 'Program',
  start: 0,
  end: 39,
  body: [
    Node {
      type: 'ForStatement',
      start: 0,
      end: 39,
      init: [Node],
      test: [Node],
      update: [Node],
      body: [Node]
    }
  ],
  sourceType: 'script'
}

Se puede ver que el tipo de este AST es programa, lo que indica que se trata de un programa. El cuerpo contiene los nodos secundarios de AST correspondientes a todas las siguientes declaraciones del programa. Cada nodo tiene un tipo, como Identificador, que indica que este nodo es un identificador;

instalar cuerda mágica

magic-string es una herramienta para manipular cadenas y generar mapas fuente. magic-string es una biblioteca sobre manipulación de cadenas escrita por el autor del resumen.

Comando de instalación:

npm i cuerda mágica -D -S

Aquí hay un ejemplo de github:

var MagicString = require('magic-string');
var magicString = new MagicString('export var name = "beijing"');
//类似于截取字符串
console.log(magicString.snip(0,6).toString()); // export
//从开始到结束删除字符串(索引永远是基于原始的字符串,而非改变后的)
console.log(magicString.remove(0,7).toString()); // var name = "beijing"

//很多模块,把它们打包在一个文件里,需要把很多文件的源代码合并在一起
let bundleString = new MagicString.Bundle();
bundleString.addSource({
    content:'var a = 1;',
    separator:'\n'
});
bundleString.addSource({
    content:'var b = 2;',
    separator:'\n'
});
/* let str = '';
str += 'var a = 1;\n'
str += 'var b = 2;\n'
console.log(str); */
console.log(bundleString.toString());
// var a = 1;
//var b = 2;

 que introduce el método

const MagicString = require('magic-string');

La ventaja de magic-string es que se generará un mapa fuente

1. Primera comprensión del análisis de sintaxis AST

        El árbol de sintaxis abstracta (AST), o árbol de sintaxis para abreviar, es una representación abstracta de la estructura gramatical del código fuente. Representa la estructura gramatical del lenguaje de programación en forma de árbol, y cada nodo del árbol representa una estructura en el código fuente. Al manipular este árbol, podemos ubicar con precisión declaraciones, declaraciones de asignación, declaraciones de operación, etc., e implementar operaciones como análisis, optimización y cambios de código.

        Simplemente puedes entender que es una representación estructurada en árbol del código que escribes. Los núcleos de herramientas como webpack, UglifyJs y lint se implementan a través del libro de sintaxis abstracta ast, que permite la inspección y el análisis del código. La capa inferior se llama analizador js para generar un árbol de sintaxis abstracta.

flujo de trabajo AST

  • Parse (análisis) convierte el código fuente en un árbol de sintaxis abstracta y hay muchos nodos de árbol en el árbol.
  • Transform (transformación) transforma el árbol de sintaxis abstracta
  • Generar (generación de código) Genera nuevo código a partir del árbol de sintaxis abstracta convertido en el paso anterior

Nuevo archivo fuente.js

const a = ()=>'a888888'
const b = ()=>'b'
a()

Obtenga una vista previa de los resultados en el sitio web de AST Spanning Tree

explorador AST

La información variable se puede analizar capa por capa en función de los datos generados por AST.

{
  "type": "Program",
  "start": 0,
  "end": 46,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 23,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 23,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 10,
            "end": 23,
            "id": null,
            "expression": true,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "Literal",
              "start": 14,
              "end": 23,
              "value": "a888888",
              "raw": "'a888888'"
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "VariableDeclaration",
      "start": 24,
      "end": 41,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 30,
          "end": 41,
          "id": {
            "type": "Identifier",
            "start": 30,
            "end": 31,
            "name": "b"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 34,
            "end": 41,
            "id": null,
            "expression": true,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "Literal",
              "start": 38,
              "end": 41,
              "value": "b",
              "raw": "'b'"
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 42,
      "end": 45,
      "expression": {
        "type": "CallExpression",
        "start": 42,
        "end": 45,
        "callee": {
          "type": "Identifier",
          "start": 42,
          "end": 43,
          "name": "a"
        },
        "arguments": [],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

Puede ver que en el desmontaje anterior, cada bloque tiene varios campos, como tipo, inicio, fin y cuerpo. Donde tipo expresa el tipo del bloque actual. Por ejemplo, FunctionDeclaration significa definición de función, Identifier significa identificador, BlockStatement significa declaración de bloque, ReturnStatement significa declaración de devolución, etc. inicio indica dónde comienza el bloque, final indica dónde termina el bloque y cuerpo indica el subbloque. Otros campos varían según la naturaleza del bloque.

Agregar el archivo index.js

//文件形式读取source.js
//fs模块  node.js中文件处理工具
const fs = require('fs');//引入
//读文件 readFileSync方法是同步读取文件,第一个参数表示文件路径,第二个参数表示读文件的编码方式(可省略)
const code = fs.readFileSync('./source.js').toString();
console.log('-------------source code----------------')
console.log(code);

//acorn 一个将代码解析为AST语法树的工具
const acorn = require('acorn');
const ast = acorn.parse(code,{ecmaVersion:'7'});//指定解析的js ECMAScript版本
console.log('--------------parsed as AST-------------')
ast.body.map(node=>{
    console.log(node)
})

//MagicString 一个处理字符串的工具
const MagicString = require('magic-string');
const m = new MagicString(code);
console.log('--------------output node-------------')
console.log('index  info')
ast.body.map((node,index)=>{
    console.log(index+'     ',m.snip(node.start,node.end).toString())//打印每个节点的信息
})


//分离声明和调用类型
const VariableDeclaration = []
const ExpressionStatement =[]
//对象当做map用 key是变量名,value是变量对应的节点
const VariableObj ={}
//statement数组 存放变量的声明和使用
const statementArr = []
ast.body.map(node=>{
    if(node.type == 'VariableDeclaration') {
        VariableDeclaration.push(node);//声明节点数组
        //取声明数组的变量名key和节点value
        const key = node.declarations[0].id.name
        VariableObj[key] = node
    } else if (node.type == 'ExpressionStatement') {
        //对于引用的数组
        ExpressionStatement.push(node);//引用节点数组
    }
})
//取变量名
console.log('---------variableDeclaration name--------------')
VariableDeclaration.map(node=>{
    console.log(node.declarations[0].id.name)
    
})

console.log('----------expressionStatement --------------')
ExpressionStatement.map(node=>{
    // console.log(node.expression.callee.name)
    console.log(node)
})

ExpressionStatement.map(node=>{
    statementArr.push(VariableObj[node.expression.callee.name])//把表达式中使用的变量名的定义语句push到数组中
    statementArr.push(node)//将表达式也push到数组中,未在表达式中调用的变量将不会遍历其VariableObj数组,也即过滤掉
})
console.log('------------treeshaking result----------')
// console.log(statementArr)
statementArr.map((node,index)=>{
    console.log(index,m.snip(node.start,node.end).toString())
})

Ejecute la declaración nodemon ./index.js

producción

-------------source code----------------
const a = ()=>'a888888'
const b = ()=>'b'
a()

--------------parsed as AST-------------
Node {
  type: 'VariableDeclaration',
  start: 0,
  end: 23,
  declarations: [
    Node {
      type: 'VariableDeclarator',
      start: 6,
      end: 23,
      id: [Node],
      init: [Node]
    }
  ],
  kind: 'const'
}
Node {
  type: 'VariableDeclaration',
  start: 25,
  end: 42,
  declarations: [
    Node {
      type: 'VariableDeclarator',
      start: 31,
      end: 42,
      id: [Node],
      init: [Node]
    }
  ],
  kind: 'const'
}
Node {
  type: 'ExpressionStatement',
  start: 44,
  end: 47,
  expression: Node {
    type: 'CallExpression',
    start: 44,
    end: 47,
    callee: Node { type: 'Identifier', start: 44, end: 45, name: 'a' },
    arguments: []
  }
}
--------------output node-------------
index  info
0      const a = ()=>'a888888'
1      const b = ()=>'b'
2      a()
---------variableDeclaration name--------------
a
b
----------expressionStatement --------------
Node {
  type: 'ExpressionStatement',
  start: 44,
  end: 47,
  expression: Node {
    type: 'CallExpression',
    start: 44,
    end: 47,
    callee: Node { type: 'Identifier', start: 44, end: 45, name: 'a' },
    arguments: []
  }
}
------------treeshaking result----------
0 const a = ()=>'a888888'
1 a()

Dos, método de recorrido del nodo treeShaking

Uso del desarrollo basado en pruebas TDD

Función walk.js agregada

Primero, pruebe las funciones de entrada y salida.

const walk = (ast, callObj)=>{
    callObj.enter(ast)
    callObj.leave(ast)
}
module.exports = walk

Escribe la función walk.spec.js

Pruebe el caso donde el ast es un objeto.

//walk.spec.js
//测试walk函数
describe('walk函数',()=>{
    test('单个节点',()=>{
        const ast = {
            a:1,
            // child:[{b:2}]
        }
        const walk = require('../walk')
        const mockEnter = jest.fn()//fn方法是jest工厂方法
        const mockLeave = jest.fn()
        //walk函数遍历ast对象,对于单个节点,进入时调用enter函数,退出时调用leave函数
        walk(ast,{
            enter:mockEnter,
            leave:mockLeave
        })
        //判断mockEnter是否被调用
        let calls = mockEnter.mock.calls //calls是数组,每调用一次增加一项
        expect(calls.length).toBe(1)//断言,ast={a:1}
        expect(calls[0][0]).toEqual({a:1})

        calls = mockLeave.mock.calls //在对leave是否调用进行判断
        expect(calls.length).toBe(1)//断言,ast={a:1}
        expect(calls[0][0]).toEqual({a:1})
    })
})

prueba

Se puede ver en los resultados de la prueba que la función caminar puede probar {a:1}

Utilice --watchAll para monitorear bromas en tiempo real

jest --watchAll

Realizar la impresión de los nombres de las variables de todas las variables.

Proceso de implementación: llame al método de variable del nodo walk, personalice el parámetro de entrada, ingrese la función en walk: elimine todas las variables de acuerdo con el atributo de tipo de nombre de variable del árbol de sintaxis ast en el nodo como Declaración de variable

 

 Nuevo test.js

//文件形式读取source.js
//fs模块  node.js中文件处理工具
const fs = require('fs');//引入
//读文件 readFileSync方法是同步读取文件,第一个参数表示文件路径,第二个参数表示读文件的编码方式(可省略)
const code = fs.readFileSync('./source.js').toString();
console.log('-------------source code----------------')
console.log(code);

//引入walk函数
const walk = require('./src/walk')
//acorn 一个将代码解析为AST语法树的工具
const acorn = require('acorn');
const ast = acorn.parse(code,{ecmaVersion:'7'});//指定解析的js ECMAScript版本
console.log('--------------walk ast-------------')
ast.body.map(node=>{
    walk(node,{
        enter:(node)=>{
            // console.log('enter---------------lalala')
            if(node && typeof node === 'object') {
                if(node.type === 'VariableDeclaration'){
                    console.log(node.declarations[0].id.name)
                    // console.log(JSON.stringify(node.declarations[0].id.name,'\t','\t'))
                }
            }
        },
        leave:(node) =>{
            // console.log('leave----------------lalala')
        }
    })
})

ast.body es el nodo y los resultados impresos son los siguientes

 impulso: busque nombres de variables en cualquier nivel

Ejemplo:

输入
const a,b =1
if(true) {
const c ='123'
}
function fn1() {
const d =1
}
const e =3
-----------------------------------
输出
a
b
c
fn1 =>d
e
-------------------------------------

Agregar fuente2.js

Guardar el programa original

const a=2
const b =1
if(true) {
const c ='123'
}
function fn1() {
const d =1
}

Agregar prueba.js

//test.js
//文件形式读取source2.js
//fs模块  node.js中文件处理工具
const fs = require('fs');//引入
//读文件 readFileSync方法是同步读取文件,第一个参数表示文件路径,第二个参数表示读文件的编码方式(可省略)
const code = fs.readFileSync('./source2.js').toString();
console.log('-------------source code----------------')
console.log(code);

//引入walk函数
const walk = require('./src/walk')
//acorn 一个将代码解析为AST语法树的工具
const acorn = require('acorn');
const ast = acorn.parse(code,{ecmaVersion:'7'});//指定解析的js ECMAScript版本
console.log('--------------walk ast-------------')
const statements = []
const parseAST = (ast)=>{
    ast.body.map(node=>{
        // console.log(node)
        walk(node,{
            enter:(node)=>{
                if(node.type === 'VariableDeclaration'){
                    console.log(node.declarations[0].id.name)
                }          
                //是个函数
                if(node.type === 'FunctionDeclaration'){
                    console.log('=>'+node.id.name)
                }    
            },
            leave:(node) =>{
          
            }
        })
    })
    
}
parseAST(ast)

Resultados de la prueba

prueba de nodemon.js

3. Simulación de alcance

 Cree un nuevo alcance para lograr un resultado similar al siguiente

const a = '1'
function(){
    const b = 2
}

Agregar el archivo src/scope.js

module.exports = class Scope{
    //定义构造方法
    constructor(option){
        //初始化names
        this.names=[]
        if(option){
            this.parent = option.parent
        }
    }
    //新增方法,每次添加一个变量名到作用域中
    add(name){
        this.names.push(name)
    }
    //判断对象中是否包含某个变量名,谁调用,this指向谁
    contains(name){
        return this.names.includes(name) || this.parent && this.parent.contains(name)
    }
    findDefiningScope(name){
        //如果调用方的names中包含name,返回调用方本身,否则沿着调用方的作用域链逐层向上寻找
        if(this.names.includes(name)){
            return this
        } else if(this.parent){//如果存在父作用域,递归寻找父作用域中是否含有该方法
            return this.parent.findDefiningScope(name)
        } else {
            return null
        }
    }
}

 La actualización contiene el método (nombre)

El método findDefiningScope puede obtener el objeto que llama. Si se encuentra un objeto, el objeto contiene el atributo de nombre.

module.exports = class Scope{
    //定义构造方法
    constructor(option){
        //初始化names
        this.names=[]
        if(option){
            this.parent = option.parent
        }
    }
    //新增方法,每次添加一个变量名到作用域中
    add(name){
        this.names.push(name)
    }
    //判断对象中是否包含某个变量名,谁调用,this指向谁
    contains(name){
        // return this.names.includes(name) || this.parent && this.parent.contains(name)
        //等价与下面  其中!!表示取字符串的布尔类型表示
        return !! this.findDefiningScope(name)
    }
    //返回实际的作用域对象
    findDefiningScope(name){
        //如果调用方的names中包含name,返回调用方本身,否则沿着调用方的作用域链逐层向上寻找
        if(this.names.includes(name)){
            return this
        } else if(this.parent){//如果存在父作用域,递归寻找父作用域中是否含有该方法
            return this.parent.findDefiningScope(name)
        } else {
            return null
        }
    }
}

Agregue el archivo src/__test__/scope.spec.js

describe('scope',()=>{
    test('scope',()=>{
        const Scope = require('../scope')
        //实例化Scope,命名为route
        const root = new Scope()
        root.add('a')
        //定义一个child子作用域,嵌套在Parent父作用域
        const child = new Scope({'parent':root})
        child.add('b')

        //编写断言 
        expect(child.findDefiningScope('a')).toBe(root)//'child中是否有a',a在父作用域链上,按作用域规则可以找到
        expect(child.contains('a')).toEqual(true)//toEqual比较每项每个的值

        expect(child.findDefiningScope('b')).toBe(child)//toBe比较的是地址
        expect(child.contains('b')).toBe(true)

        // expect(child.findDefiningScope('c')).toBe(null)
        // expect(child.contains('c')).toEqual(false)
    })
})

Agregue alcance de tres niveles, pruebe

describe('scope',()=>{
    test('scope',()=>{
        const Scope = require('../scope')
        //实例化Scope,命名为route
        const root = new Scope()
        root.add('a')
        //定义一个child子作用域,嵌套在Parent父作用域
        const child = new Scope({'parent':root})
        child.add('b')
        //定义一个三级作用域,孙子节点
        const childChild = new Scope({'parent':child})
        childChild.add('c')

        //编写断言 
        expect(child.findDefiningScope('a')).toBe(root)//'child中是否有a',a在父作用域链上,按作用域规则可以找到
        expect(child.contains('a')).toEqual(true)//toEqual比较每项每个的值

        expect(child.findDefiningScope('b')).toBe(child)//toBe比较的是地址
        expect(child.contains('b')).toBe(true)

        expect(child.findDefiningScope('c')).toBe(null)
        expect(child.contains('c')).toEqual(false)

        expect(childChild.findDefiningScope('b')).toBe(child)
        expect(childChild.contains('b')).toEqual(true)
    })
})

4. Integre funciones de alcance y recorrido de nodos.

Cree un nuevo archivo analyse.js

//analyze.js
//输入是一个ast 输出是已经分析好的scope
const acorn = require('acorn')
const fs = require('fs')
const { node } = require('webpack')
const {walk} = require('../walk')
const code = fs.readFileSync('../source.js').toString()
const ast = acorn.parse(code,{ecmaVersion:7})
const Scope = require('./scope')

module.exports = function analyze(ast) {
    const root = new Scope()
    ast.body.map(node=>{
        walk(node,{
            enter:(node)=>{
                if(node.type === 'Identifier'){
                    root.add(node.name)
                    // console.log(node.Identifier.name)
                }          
                //是个函数 如果存在函数,函数在子作用域里
                if(node.type === 'FunctionDeclaration'){
                    // console.log('=>'+node.id.name)
                }    
            },
            leave:(node) =>{
          
            }
        })
    })
    return new Scope()
}

Se agregó el archivo analyse.spec.js.

describe('analyze',()=>{
    test('analyze 1',()=>{
        const analyze = require('../analyze')
        //测试ast到scope
        const acorn = require('acorn')
        const fs = require('fs')
        const {walk} = require('../walk')
        const code = `
const a =1`
        const ast = acorn.parse(code,{ecmaVersion:7})
        const root = analyze(ast)
        expect(root.findDefiningScope('a')).toBe(root)
        expect(root.contains('a')).toEqual(true)

    })
})

Supongo que te gusta

Origin blog.csdn.net/qq_36384657/article/details/128264329
Recomendado
Clasificación