フロントエンド エンジニアリングの実践 - クイック スタート ツリーシェーキング

木が揺れる       

ツリーシェーキングの本質は、冗長なコード アルゴリズムを削除することです。フロントエンド パフォーマンスの最適化では、es6 ではツリー シェイク メカニズムが導入されています。ツリー シェイクとは、プロジェクトに他のモジュールを導入するときに、使用しないコードや決して実行されないコードを自動的にシェイク アウトすることを意味します。 Uglify Stage で検出されましたが、バンドルにパッケージ化されていませんでした。

        ツリーシェイクを学習する目的は、後でロールアップ パッケージングの原理を学習するための基礎を築くことでもあります。ロールアップでは、ファイルはモジュールです。各モジュールはファイルのコードに従って AST 構文抽象化ツリーを生成し、ロールアップでは各 AST ノードを分析する必要があります。AST ノードを分析するとは、ノードが関数またはメソッドを呼び出しているかどうかを確認することです。存在する場合は、呼び出された関数またはメソッドが現在のスコープ内にあるかどうかを確認し、そうでない場合は、モジュールの最上位スコープが見つかるまで調べます。このモジュールが見つからない場合は、この関数とメソッドが他のモジュールに依存しているため、他のモジュールからインポートする必要があることを意味します。rollup は関数と最上位のインポート/エクスポート変数のみを処理します。

0 事前知識 - インストール関連の依存関係

Webpackをインストールする

npm install webpack webpack-cli --save-dev

プロジェクトを初期化する

npm init -y

Webpack のパッケージ化されたコンテンツを表示する

npx webpack ./test1.js

ノードモンをインストールする

nodemon は、node.js の開発を支援するツールで、ディレクトリ内のファイルが変更されると、自動的にノード アプリケーションを再起動します。

npm i ノードモン -g

テストコードコマンド:nodemon ./index.js

パラメータ: 時計

ドングリをインストールする

npm i acorn -d -s

const acorn = require('acorn');

acorn のデフォルトの使用法は非常に簡単で、コード文字列を解析して AST 構造を取得するだけです。

let acorn = require("acorn");

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

解析されたAST構文ツリー

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'
}

このASTのtypeはprogramとなっており、これがプログラムであることがわかります。body には、プログラムの後続のすべてのステートメントに対応する AST 子ノードが含まれます。各ノードには、このノードが識別子であることを示す Identifier などのタイプがあります。

マジックストリングをインストールする

magic-string は、文字列を操作し、ソースマップを生成するためのツールです。magic-string は、rollup の作者によって書かれた文字列操作に関するライブラリです。

インストールコマンド:

npm i マジック文字列 -D -S

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;

 その方法を紹介するもの

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

magic-string の利点は、ソースマップが生成されることです。

1. AST 構文解析の最初の理解

        抽象構文ツリー (AST)、または略して構文ツリーは、ソース コードの文法構造を抽象的に表現したものです。これはプログラミング言語の文法構造をツリー形式で表し、ツリー上の各ノードはソース コード内の構造を表します。このツリーを操作することで、宣言文、代入文、演算文などを正確に見つけ出し、コードの解析や最適化、変更などの操作を実装することができます。

        これは、作成したコードをツリー構造で表現したものであると簡単に理解できます。webpack、UglifyJs、lint などのツールのコアはすべて、コードの検査と分析を可能にする ast 抽象構文ブックを通じて実装されています。最下層は js パーサーと呼ばれ、抽象構文ツリーを生成します。

AST ワークフロー

  • Parse(解析)はソースコードを抽象構文ツリーに変換し、ツリー上には多数の estree ノードが存在します
  • Transform (変換) は、抽象構文ツリーを変換します。
  • Generate (コード生成) 前のステップで変換された抽象構文ツリーから新しいコードを生成します。

新しいファイルsource.js

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

AST スパニング ツリー Web サイトで結果をプレビューします。

ASTエクスプローラー

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

上記の逆アセンブリでは、各ブロックに type、start、end、body などの複数のフィールドがあることがわかります。type は現在のブロックのタイプを表します。たとえば、FunctionDeclaration は関数定義、Identifier は識別子、BlockStatement はブロック ステートメント、ReturnStatement は return ステートメントなどを意味します。start はブロックの開始位置を示し、end はブロックの終了位置を示し、body はサブブロックを示します。他のフィールドはブロックの性質によって異なります。

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())
})

ステートメントnodemon ./index.jsを実行します。

出力

-------------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()

2、treeShakingノードトラバーサル方法

TDD テスト駆動開発の使用

walk.js機能を追加

まず、入口関数と出口関数をテストします。

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

walk.spec.js 関数を作成する

ast がオブジェクトであるケースをテストする

//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})
    })
})

テスト

テスト結果から、walk 関数は {a:1} をテストできることがわかります。

--watchAll を使用して jest をリアルタイムで監視します

jest --watchAll

すべての変数の変数名の出力を実現

実装プロセス: walk ノードの変数メソッドを呼び出し、入力パラメーターのカスタマイズ walk 内の関数 enter : ノード内の ast 構文ツリーの変数名の type 属性に従ってすべての変数を VariableDeclaration として取り出す

 

 新しい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 がノードで、出力された結果は次のとおりです。

 boost: 任意のレベルで変数名を検索

例:

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

source2.jsを追加

元のプログラムを保存する

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

test.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)

試験結果

ノードモンテスト.js

3. スコープシミュレーション

 新しいスコープを作成すると、次のような結果が得られます。

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

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

 contains(name) メソッドのアップグレード

findDefiningScope メソッドは呼び出し元のオブジェクトを取得できます。オブジェクトが見つかった場合、そのオブジェクトには name 属性が含まれます。

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

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)
    })
})

第 3 レベルのスコープを増やしてテストします

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. ノードトラバーサル機能とスコープ機能を統合する

新しいanalyze.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()
}

追加されたanalyze.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)

    })
})

おすすめ

転載: blog.csdn.net/qq_36384657/article/details/128264329