AST 抽象语法树

  • webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的

为什么吗要转换成AST语法树?
不转无法遍历,定位和转换节点

抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
    • IDE 的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2 等
  • 优化变更代码,改变代码结构使达到想要的结构

    • 代码打包工具 webpack、rollup 等等
    • CommonJS、AMD、CMD、UMD 等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX 等转化为原生 Javascript

      抽象语法树定义

  • 这些工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

webpack 插件 - 图2

JavaScript Parser

  • JavaScript Parser是把JavaScript源码转化为抽象语法树的解析器。
  • 浏览器会把JavaScript源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。
  • 一般来说每个JavaScript引擎都会有自己的抽象语法树格式,Chrome 的 v8 引擎,firefox 的 SpiderMonkey 引擎等等,MDN 提供了详细SpiderMonkey AST format 的详细说明,算是业界的标准。

    常用的 JavaScript Parser

  • esprima

  • traceur
  • acorn
  • shift

    遍历

    1. cnpm i esprima estraverse- S
    ``javascript let esprima = require('esprima');//把JS源代码转成AST语法树 let estraverse = require('estraverse');///遍历语法树,修改树上的节点 let escodegen = require('escodegen');//把AST语法树重新转换成代码 // 老代码-》老语法书-》-》遍历语法树-》对语法树进行转换-》根据新的语法树重新生成新的代码 // 例如:es6-》es6语法树-》遍历箭头函数节点-》把箭头函数转换成普通函数-》生成es5代码 let code =function ast(){}`; // 源代码 let ast = esprima.parse(code); let indent = 0; const padding = ()=>” “.repeat(indent); estraverse.traverse(ast,{ // 深度优先 enter(node){
    1. console.log(padding()+node.type+'进入');
    2. if(node.type === 'FunctionDeclaration'){
    3. node.id.name = 'newAst';
    4. }
    5. indent+=2;
    }, leave(node){
      indent-=2;
      console.log(padding()+node.type+'离开');
    
    } });

let result = escodegen.generate(ast) console.log(result)

```javascript
Program进入
  FunctionDeclaration进入
    Identifier进入
    Identifier离开
    BlockStatement进入
    BlockStatement离开
  FunctionDeclaration离开
Program离开

image.png
ast分为两部:

  1. 词法分析
  2. 语法分析

    babel

  • Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行
  • 工作过程分为三个部人
    • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
    • Transform(转换) 对抽象语法树进行转换
    • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

image.png

AST遍历

  • AST是深度优先遍历
  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • Visitor 的对象定义了用于 AST 中获取具体节点的方法
  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

    babel 插件

  • @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform:老源代码转换成新的源代码、parse:把源代码转换成语法树

  • babylon Babel 的解析器
  • babel-types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
  • babel-traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
  • babel-types-api
  • Babel 插件手册
  • babeljs.io babel 可视化编译器

    转换箭头函数

  • astexplorer

  • babel-plugin-transform-es2015-arrow-functions
  • babeljs.io babel 可视化编译器
  • babel-handbook
  • babel-types-api

转换前

const sum = (a,b)=>{
    console.log(this);
    return a+b;
}

转换后

var _this = this;

const sum = function (a, b) {
  console.log(_this);
  return a + b;
};

实现

npm i @babel/core babel-types -D
let core = require('@babel/core');// babel核心包,把源代码生成语法树-》遍历语法树
// 本身并不知道如何转换成代码,那怎么转换呢,插件:它其实是一个钩子函数(一种转换规则),在遍历语法树的过程中可以捕获特别类型的节点进行转换
let types = require('babel-types'); // babel的工具包,判断某个节点是不是某个类型,动态创建某个了类型
let  BabelPluginTransformEs2015ArrowFunctions = require('babel-plugin-transform-es2015-arrow-functions');
// 此插件只处理箭头函数,
// 每个es6的语法,都会对应一个这样的插件
// 每个插件都会捕获自己的语法节点,转换对应的es5语法
//所有的插件会打成一个包,就是preset @babel/preset-envc插件包,基本上包括所有的es6转换插件
const sourceCode = `
const sum = (a,b)=>{
    console.log(this);
    return a+b;
}
`;
//插件的格式是定死的:babel插件其实是一个对象,它会有一个visitor访问器
let BabelPluginTransformEs2015ArrowFunctions2 = {
    //每个插件都会有自己的访问器
    visitor:{
        //属性就是节点的类型,babel在遍历到对应类型的节点的时候会调用此函数
        ArrowFunctionExpression(nodePath){//参数是节点的数据
            let node = nodePath.node;//获取 当前路径上的节点
            //处理this指针的问题
            hoistFunctionEnvironment(nodePath);
            node.type = 'FunctionExpression';
        }
    }
}
function hoistFunctionEnvironment(fnPath){
   const thisEnvFn = fnPath.findParent(p=>{
       //是一个函数,不能是箭头函数 或者 是根节点也可以
       return (p.isFunction() && !p.isArrowFunctionExpression())||p.isProgram()
   });
   //找一找当前作用域哪些地方用到了this的路径
   let thisPaths = getScopeInfoInformation(fnPath);
   //声明了一个this的别名变量,默认是_this __this
   let thisBinding = '_this';
   if(thisPaths.length>0){
       //在thisEnvFn的作用域内添加一个变量,变量名_this,初始化的值为this
    thisEnvFn.scope.push({
        id:types.identifier(thisBinding),
        init:types.thisExpression()    
    });
    thisPaths.forEach(item=>{
        //创建一个_this的标识符  
        let thisBindingRef = types.identifier(thisBinding);
        //把老的路径 上的节点替换成新节点
        item.replaceWith(thisBindingRef);
    });
   }
}
function getScopeInfoInformation(fnPath){
  let thisPaths = [];
  //遍历当前path所有的子节点路径,
  //告诉 babel我请帮我遍历fnPath的子节点,遇到ThisExpression节点就执行函数,并且把对应的路径传进去 
  fnPath.traverse({
    ThisExpression(thisPath){
        thisPaths.push(thisPath);
    }
  });
  return thisPaths;
}

let targetCode = core.transform(sourceCode,{
    plugins:[BabelPluginTransformEs2015ArrowFunctions2]
});
console.log(targetCode.code);

webpack TreeShaking插件

var babel = require("@babel/core");
let { transform } = require("@babel/core");

实现按需加载

  • lodashjs
  • babel-core
  • babel-plugin-import
    import { flatten, concat } from "lodash";
    

    image.png

    转换为
    import flatten from "lodash/flatten";
    import concat from "lodash/flatten";
    
    image.png

    webpack 配置

    cnpm i webpack webpack-cli -D
    
    const path = require("path");
    module.exports = {
    mode: "development",
    entry: "./src/index.js",
    output: {
      path: path.resolve("dist"),
      filename: "bundle.js",
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: "babel-loader",
            options:{
                     plugins:[
                       [
                         path.resolve(__dirname,'plugins/babel-plugin-import.js'),
                         {
                           libraryName:'lodash' // 只有libraryName为lodash的时候才会用到这个插件
                         }
                       ]
                     ]
                  }
          },
        },
      ],
    },
    };
    

    编译顺序为首先plugins从左往右,然后presets从右往左

babel 插件

plugins\babel-plugin-import.js 支持antd antd-mobile lodash

let babel = require("@babel/core");
let types = require("babel-types");
const visitor = {
  ImportDeclaration: {
    enter(path, state = { opts }) {
      const specifiers = path.node.specifiers;
      const source = path.node.source;
      if (
        state.opts.libraryName == source.value &&
        !types.isImportDefaultSpecifier(specifiers[0])
      ) {
        const declarations = specifiers.map((specifier, index) => {
          return types.ImportDeclaration(
            [types.importDefaultSpecifier(specifier.local)],
            types.stringLiteral(`${source.value}/${specifier.local.name}`)
          );
        });
        path.replaceWithMultiple(declarations);
      }
    },
  },
};
module.exports = function (babel) {
  return {
    visitor,
  };
};

面试题:说一下关于tree-shaking的原理

当前端项目到达一定的规模后,我们一般会采用按模块方式组织代码,这样可以方便代码的组织及维护。但会存在一个问题,比如我们有一个utils工具类,在另一个模块中导入它。这会在打包的时候将utils中不必要的代码也打包,从而使得打包体积变大,这时候就需要用到Tree shaking技术了
tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术

原理

  • 利用ES6模块的特点
    • 只能作为模块顶层的语句出现
    • import的模块名只能是字符串常量,不能动态引入模块
    • import 引入的模块不能再进行修改的 虽然tree-shaking的概念在1990年就提出来了,但是直到ES6的ES6-style模块出现后才真正被利用起来。这是因为tree-shaking只能在静态模块下工作。ES6模块加载是静态的,因此在ES6种使用tree-shaking是非常容易地。而且,tree-shaking不仅支持import/export级别,而且也支持声明级别

在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态地,也意味着我们可以基于条件来导入需要的代码:

let mainModule;
//动态导入
if(condition){
    mainModule=require('dog')
}else{
    mainModule=require('cat')
}

CommonJS的动态特性意味着tree-shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。

//不可行
if(condition){
    mainModule=require('dog')
}else{
    mainModule=require('cat')
}

只能通过导入所有的包后再进行条件获取

import dog from 'dog';
import cat from 'cat';
if(condition){
//dog.xxx
}else{
//cat.xxx
}

ES6的import语法可以使用tree-shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。

如何使用?

从webpack2开始支持实现了tree-shaking特性,webpack2正式版本内置支持ES6的模块(也叫harmony模块)和未引用模块检测能力。webpack4正式版本扩展了这个检测能力,通过package.json的sideEffects属性作为标记,向complier提供提示,表明项目中哪些文件是ES6模块,由此可以安全地删除文件中未使用地部分 如果使用的是webpack4,只需要将mode设置为production,就可以开启tree-shaking

entry:'./src/index.js',
mode:'production',
output:{
    path:path.resolve(__dirname,'dist'),
    filename:'bundle.js'
},

如果使用webpack2,可能你会发现tree-shaking不起作用。因为babel会将代码编译成CommonJS模块,而tree-shaking不支持CommonJS,所以需要配置不转义

options:{presets:[['es2015',{modules:false}]]}

关于副作用

副作用是指那些当import的时候会执行一些动作,但是不一定会有任何export。比如ployfill,ployfills不对外暴露方法给主程序使用
tree-shaking不能自动识别哪些代码属于副作用,因此手动指定这些代码显得非常重要,如果不指定可能会出现一些意想不到的问题
在webpack中,是通过package.json的sideEffects属性来实现的

"name":"tree-shaking",
"sideEffects":false

如果所有的代码都不包含副作用,我们就可以简单地将该属性标记为false来告知webpack,它可以安全地删除未用到的export导出。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:

"name":"tree-shaking",
"sideEffects":[
    "./src/public/polyfill.js"
]

总结

  • tree-shaking不会支持动态导入(如CommonJS的require()语法),只纯静态的导入(ES6的import/export)
  • webpack中可以在项目package.json文件中,添加一个”sideEffects”属性,手动指定副作用的脚本

    webpack 里面的插件是如何实现的

  • webpack本质是一个事件流机制,核心模块:tapable(Sync + Async)Hooks 构造出 === Compiler(编译) + Compilation(创建bundles)

  • compiler对象代表了完整的webpack环境配置。这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options、loader和plugin。当在webpack环境中应用一个插件时,插件将收到此compiler对象的引用。可以使用它来访问webpack的主环境
  • compilation对象代表了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一个新的编译资源。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态的信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
  • 创建一个插件函数,在其prototype上定义apply方法,指定一个webpack自身的事件钩子
  • 函数内部处理webpack内部实例的特定数据
  • 处理完成后,调用webpack提供的回调函数 ```javascript function MyWebpackPlugin(){

}; //prototype上定义apply方法 MyWebpackPlugin.prototype.apply=function(){ //指定一个事件函数挂载到webpack compiler.plugin(“webpacksEventHook”,funcion (compiler){ console.log(“这是一个插件”); //功能完成调用后webpack提供的回调函数 callback() }) }

<a name="RhlgJ"></a>
# 数据分析插件
<a name="gK7g3"></a>
## friendly-errors-webpack-plugin 日志美化插件
[friendly-errors-webpack-plugin ](http://www.febeacon.com/webpack-plugins-docs-cn/routes/friendly-errors-webpack-plugin.html)可以识别webpack中的某些类别的错误,并对它们进行清理、聚合和排序,以提供更好的开发体验
```javascript
yarn add -D node-notifier   
yarn add -D friendly-errors-webpack-plugin
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const notifier = require('node-notifier')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const path = require("path");
const ICON = path.join(__dirname, './src/assets/icon.png');
module.exports = {
  mode: "development",
  entry: "./src/main.tsxx",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  devtool: "source-map",
  devServer: {
    hot: true,
    static: {
      directory: path.join(__dirname, 'dist'),
    }
    // historyApiFallback: {// 如果路径不能正常响应会重定向到index.html里去
    //   index: "./index.html",
    // },
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"], // 扩展名
    alias: {
      "@": path.resolve("src"), // 这样配置后 @ 可以指向 src 目录
    },
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,// ts tsx
        loader: "ts-loader",
      },
      {
        enforce: "pre", // 提前执行,可以让我们调试ts源代码
        test: /\.tsx$/,
        loader: "source-map-loader",
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new FriendlyErrorsWebpackPlugin(
      {
        onErrors: function (severity, errors) {
          const error = errors[0];
          notifier.notify({
            title: "Webpack编译失败",
            message: severity + ': ' + error.name,
            subtitle: error.file || '',
            icon: ICON
          });
        },
      }),
    new webpack.HotModuleReplacementPlugin(), // 热更新
  ],
};

image.png

speed-measure-webpack-plugin速度分析插件

speed-measure-webpack-plugin:可以分析打包速度

  • 分析整个打包总耗时
  • 每个插件和loader的耗时情况
    yarn add -D speed-measure-webpack-plugin
    
    ```javascript const speedMeasureWebpackPlugin = require(‘speed-measure-webpack-plugin’) const smp = new speedMeasureWebpackPlugin();

module.exports = smp.wrap({ mode: “development”, entry: “./src/main.tsx”, …… })

![image.png](https://cdn.nlark.com/yuque/0/2021/png/735369/1637116858517-4951556e-6686-4d18-a147-71d43c8471f6.png#clientId=u69c715cb-186c-4&from=paste&height=900&id=u49563eb2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1800&originWidth=2880&originalType=binary&ratio=1&size=709163&status=done&style=none&taskId=u09fe1621-a433-43ef-b02f-c5487e1099f&width=1440)
<a name="aXvrK"></a>
## 文件体积监控
[webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)是一个webpack插件,需要配合webpack和webpack-cli一起使用,这个插件的功能是生成代码分析报告帮助代码质量和网站性能<br />它可以更直观的分析打包出来的文件包含哪些,大小占比;模块关系,依赖项,文件是否重复,压缩后大小。
```javascript
yarn add -D webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ],
}
// 配置启动命令
"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server",
    "analyzer":"webpack --progress"
  },

执行命令启动yarn analyzer
image.png