前端工程化实践——快速入门treeshaking

treeshaking       

treeshaking本质是去除多余代码算法。在前端的性能优化中,es6 推出了tree shaking机制,tree shaking就是当我们在项目中引入其他模块时,他会自动将我们用不到的代码,或者永远不会执行的代码摇掉,在Uglify阶段查出,不打包到bundle中。

        学习treeshaking的目的也是是为了后面学习rollup打包原理做铺垫。在 rollup 中,一个文件就是一个模块。每一个模块都会根据文件的代码生成一个 AST 语法抽象树,rollup 需要对每一个 AST 节点进行分析。分析 AST 节点,就是看看这个节点有没有调用函数或方法。如果有,就查看所调用的函数或方法是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入。rollup只处理函数和顶层的import/export变量。

0前置知识-安装相关依赖

安装webpack

npm install webpack webpack-cli --save-dev

初始化项目

npm init -y

查看webpack打包后的内容

npx webpack ./test1.js

安装nodemon

nodemon 一个辅助node.js开发的工具,当目录中的文件更改时,会自动重启node应用程序

npm i nodemon -g

测试代码命令:nodemon ./index.js

参数:watch

安装acorn

npm i acorn -d -s

const acorn = require('acorn');

acorn的默认用法非常简单,直接来段代码字符串parse一下就出来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 的类型为 program,表明这是一个程序。body 则包含了这个程序下面所有语句对应的 AST 子节点。每个节点都有一个 type 类型,例如 Identifier,说明这个节点是一个标识符;

安装magic-string

magic-string是一个操作字符串和生成source-map的工具。magic-string 是 rollup 作者写的一个关于字符串操作的库。

安装命令:

npm i magic-string -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的好处是会生成sourcemap

一、初识AST语法解析

        抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。

        你可以简单理解为 它就是你所写代码的的树状结构化表现形式。webpack、UglifyJs、lint等工具的核心都是通过ast抽象语法书实现的,实现对代码的检查、分析。底层是调用的js parser 来生成抽象语法树。

AST工作流

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

前端工程化实践——快速入门treeshaking_第1张图片

新建文件source.js

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

在AST生成树网站预览结果

AST explorer

可以根据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 表示返回语句等。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()

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

测试

前端工程化实践——快速入门treeshaking_第2张图片

由测试结果可以看出walk函数可以对{a:1}进行测试

使用--watchAll实时监听jest

jest --watchAll

实现打印所有变量的变量名

实现过程:调用walk节点变量方法,自定义walk中的入参enter函数:根据node中ast语法树变量名type属性为VariableDeclaration取出所有的变量

前端工程化实践——快速入门treeshaking_第3张图片

 

 新建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才是节点,打印结果如下

前端工程化实践——快速入门treeshaking_第4张图片

 提升:找出任意层级的变量名

示例:

输入
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)

测试结果

nodemon test.js

前端工程化实践——快速入门treeshaking_第5张图片

三、作用域模拟

 新建一个scope作用域实现类似于下面的结果

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

增加三级作用域,测试

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

四、整合节点遍历和作用域函数

新建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)

    })
})

你可能感兴趣的:(前端工程化,前端,javascript,开发语言)