Webpack之tree-starking 解析


Posted in Javascript onSeptember 11, 2018

tree-sharking 简介

tree-sharking 是 Webpack 2 后续版本的优化功能,顾名思义,就是将多余的代码给 “摇晃” 掉,在开发中我们经常使用一些第三方库,而这些第三方库只使用了这个库的一部门功能或代码,未使用的代码也要被打包进来,这样出口文件会非常大,tree-sharking 帮我们解决了这个问题,它可以将各个模块中没有使用的方法过滤掉,只对有效代码进行打包。

AST 语法树分析

假设我们现在使用了 ElementUI 库的两个组件,通常会使用解构赋值来引入。

优化前

import { Button, Alert } from "element-ui";

这样引用资源, Webpack 在打包的时候会找到 element-ui 并把里面所有的代码全部打包到出口文件,我们只使用了两个组件,全部打包不是我们所希望的,tree-sharking 是通过在 Webpack 中配置 babel-plugin-import 插件来实现的,它可以将解构的代码转换成下面的形式。

优化后

import Button from "element-ui/lib/button";
import Alert from "element-ui/lib/Alert";

转化后会去 node_modules 中的 element-ui 模块找到 Button 和 Alert 两个组件对应的文件,并打包到出口文件中。

通过上面的转换可以看出,其实 tree-sharking 的实现原理是通过改变 AST 语法树的结构来实现的,我们可以通过在线转换网站 http://esprima.org/demo/parse.html 将 JS 代码装换成 AST 语法树。

优化前的 AST 语法树

{
  "type": "Program",
  "body": [
    {
      "type": "ImportDeclaration",
      "specifiers": [
        {
          "type": "ImportSpecifier",
          "local": {
            "type": "Identifier",
            "name": "Button"
          },
          "imported": {
            "type": "Identifier",
            "name": "Button"
          }
        },
        {
          "type": "ImportSpecifier",
          "local": {
            "type": "Identifier",
            "name": "Alert"
          },
          "imported": {
            "type": "Identifier",
            "name": "Alert"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "value": "element-ui",
        "raw": "\"element-ui\""
      }
    }
  ],
  "sourceType": "module"
}

优化后的 AST 语法树

{
  "type": "Program",
  "body": [
    {
      "type": "ImportDeclaration",
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "local": {
            "type": "Identifier",
            "name": "Button"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "value": "element-ui/lib/button",
        "raw": "\"element-ui/lib/button\""
      }
    },
    {
      "type": "ImportDeclaration",
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "local": {
            "type": "Identifier",
            "name": "Alert"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "value": "element-ui/lib/Alert",
        "raw": "\"element-ui/lib/Alert\""
      }
    }
  ],
  "sourceType": "module"
}

从上面的语法树对比,可以看出在优化前 body 里面只有一个对象,使用的组件信息存在 specifiers 里,source 指向了 element-ui,而在优化后,将两个组件分别拆成了两个对象存在 body 中,每个对象的的 specifiers 只存储一个组件,并在 source 里面指向了当前组件对应的路径。

模拟 tree-starking

既然我们已经清楚要修改语法树的位置,下面就使用 AST 来模拟 tree-sharking 功能,对语法树的操作是依赖于 babel-core 和 babel-types 两个核心模块的,下面先安装依赖。

npm install babel-core babel-types

文件:babel-plugin-my-import.js

const babel = require("babel-core");
const types = require("babel-types");

let code = `import { Button, Alert } from "element-ui"`;

let importPlugin = {
  visitor: {
    ImportDeclaration(path) {
      let node = path.node;
      let source = node.source.value;
      let specifiers = node.specifiers;

      // 判断是否是默认导出,其中一个不是默认导出,则都不是默认导出
      if (!types.isImportDefaultSpecifier(specifiers[0])) {
        // 如果不是默认导出,则需要转换
        specifiers = specifiers.map(specifier => {
          // 数组内容:当前默认导出的标识、从哪里导入
          return types.importDeclaration(
            [types.importDefaultSpecifier(specifier.local)],
            types.stringLiteral(`${source}/lib/${specifier.local.name.toLowerCase()}`)
          );
        });

        // 替换树结构
        path.replaceWithMultiple(specifiers);
      }
    }
  }
};

let result = babel.transform(code, {
  plugins: [importPlugin]
});

console.log(result.code);

// import Button from "element-ui/lib/button";
// import Alert from "element-ui/lib/alert";

通过上面的代码可以发现我们使用 babel-core 和 babel-types 两个模块的核心方法对语法书进行了遍历、修改和替换,更详细的 API 可以查看 https://github.com/babel/babel/tree/6.x/packages/babel-types。

结合 Webpack 使用插件

前面只是验证了 tree-sharking 中 JS 语法的转换过程,接下来将上面的代码转换成插件配合 Webpack 使用,来彻底感受 tree-sharking 的工作过程。

文件:~node_modules/babel-plugin-my-import.js

const babel = require("babel-core");
const types = require("babel-types");

let importPlugin = {
  visitor: {
    ImportDeclaration(path) {
      let node = path.node;
      let source = node.source.value;
      let specifiers = node.specifiers;

      // 判断是否是默认导出,其中一个不是默认导出,则都不是默认导出
      if (!types.isImportDefaultSpecifier(specifiers[0])) {
        // 如果不是默认导出,则需要转换
        specifiers = specifiers.map(specifier => {
          // 数组内容:当前默认导出的标识、从哪里导入
          return types.importDeclaration(
            [types.importDefaultSpecifier(specifier.local)],
            types.stringLiteral(`${source}/lib/${specifier.local.name.toLowerCase()}`)
          );
        });

        // 替换树解构
        path.replaceWithMultiple(specifiers);
      }
    }
  }
};

module.exports = importPlugin;

上面删掉了多余的测试代码,将模块中的 importPlugin 插件导出,并把 babel-plugin-my-import.js 移入了 node_modules 当中。

接下来安装需要的依赖:

npm install webpack webpack-cli babel-loader babel-presets-env
npm install vue element-ui --save

安装完依赖,写一个要编译的文件,使用 Webpack 进行打包,查看使用插件前和使用插件后出口文件的大小。

文件:import.js

import Vue from "vue";
import { Button, Alert } from "element-ui";

下面来写一个简单的 Webpack 配置文件。

文件:webpcak.config.js

module.exports = {
  mode: "development",
  entry: "import.js",
  output: {
    filename: "bundle.js",
    path: __dirname
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader: "babel-loader",
        options: {
          presets: [
            "env",
          ],
          plugins: [
            // 插件:不使用插件打包注释掉该行即可
            ["my-import", { libararyName: "element-ui" }]
          ]
        }
      },
      exclude: /node_modules/
    }]
  }
};

为了防止 babel 相关的依赖升级 7.0 后出现一些问题导致 Webpack 无法启动,再此贴出 package.json 文件,按照对应版本下载依赖保证上面 Webpack 配置生效。

文件:package.json

{
 "name": "ast-lesson",
 "version": "1.0.0",
 "description": "tree-starking",
 "main": "index.js",
 "scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
 },
 "keywords": [],
 "author": "",
 "license": "ISC",
 "dependencies": {
  "babel-core": "^6.26.3",
  "babel-loader": "^7.1.5",
  "babel-preset-env": "^1.7.0",
  "babel-types": "^6.26.0",
  "escodegen": "^1.10.0",
  "esprima": "^4.0.0",
  "estraverse": "^4.2.0",
  "webpack": "^4.16.0",
  "webpack-cli": "^3.0.8"
 },
 "devDependencies": {
  "vue": "^2.5.17",
  "element-ui": "^2.4.6"
 }
}

对比使用插件前后的出口文件

接下来分别在使用插件和不使用插件时执行打包命令,查看出口文件 bondle.js 的大小。

npx webpack

使用 babel-plugin-my-import 前:

Webpack之tree-starking 解析

使用 babel-plugin-my-import 后:

Webpack之tree-starking 解析

通过对比,可以看到使用 tree-sharking 即我们自己实现的 babel-plugin-my-import 插件后,打包的出口文件大大减小,其原因是将引入第三方库没有使用的代码全都过滤掉了,只打包了有效代码。

总结

上面对 Webpack 的 tree-sharking 进行了分析,并模拟 babel-plugin-import 简易的实现了一版 tree-sharking 的优化插件,这个过程中相信大家已经了解了 tree-sharking 的原理以及实现类似插件的思路,并已经具备了开发类似插件的基本条件,最后还有一点需要补充,tree-sharking 优化的方式是根据 ES6 语法 import “静态” 引入的特性实现的,如果要说 tree-sharking 很强大,还不如说 ES6 模块化规范 “静态” 引入的特性强大,正由于是基于 “静态” 引入,所以目前 tree-sharking 只支持遍历一层 import 关键字。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
有效的捕获JavaScript焦点的方法小结
Oct 08 Javascript
getElementsByTagName vs selectNodes效率 及兼容的selectNodes实现
Feb 26 Javascript
jquery实现网站超链接和图片提示效果
Mar 21 Javascript
jq实现左侧显示图片右侧文字滑动切换效果
Aug 04 Javascript
javascript中undefined与null的区别
Aug 16 Javascript
jQuery实现的调整表格行tr上下顺序
Jan 10 Javascript
easyui datagrid 表格中操作栏 按钮图标不显示的解决方法
Jul 27 Javascript
JavaScript控制浏览器全屏显示简单示例
Jul 05 Javascript
vue加载完成后的回调函数方法
Sep 07 Javascript
javascript实现手动点赞效果
Apr 09 Javascript
详解vue.js移动端配置flexible.js及注意事项
Apr 10 Javascript
JS如何生成随机验证码
Mar 02 Javascript
node.js之基础加密算法模块crypto详解
Sep 11 #Javascript
Vue在页面数据渲染完成之后的调用方法
Sep 11 #Javascript
浅谈Webpack核心模块tapable解析
Sep 11 #Javascript
原生js检测页面加载完毕的实例
Sep 11 #Javascript
vue props传值失败 输出undefined的解决方法
Sep 11 #Javascript
解决vue props 拿不到值的问题
Sep 11 #Javascript
vue首次赋值不触发watch的解决方法
Sep 11 #Javascript
You might like
用PHP实现小型站点广告管理(修正版)
2006/10/09 PHP
PHP实现的MongoDB数据库操作类分享
2014/05/12 PHP
php获取系统变量方法小结
2015/05/29 PHP
Zend Framework框架教程之Zend_Db_Table_Rowset用法实例分析
2016/03/21 PHP
PHP 扩展Memcached命令用法实例总结
2020/06/04 PHP
Ext.get() 和 Ext.query()组合使用实现最灵活的取元素方式
2011/09/26 Javascript
Js中使用hasOwnProperty方法检索ajax响应对象的例子
2014/12/08 Javascript
JS中JSON对象和String之间的互转及处理技巧
2016/04/06 Javascript
基于JavaScript代码实现自动生成表格
2016/06/15 Javascript
JS触发服务器控件的单击事件(详解)
2016/08/06 Javascript
js document.getElementsByClassName的使用介绍与自定义函数
2016/11/25 Javascript
js的三种继承方式详解
2017/01/21 Javascript
详解nodejs中exports和module.exports的区别
2017/02/17 NodeJs
JS实现身份证输入框的输入效果
2017/08/21 Javascript
vue轮播图插件vue-awesome-swiper
2017/11/27 Javascript
JS中offset和匀速动画详解
2018/02/06 Javascript
详解Vue结合后台的列表增删改案例
2018/08/21 Javascript
node-red File读取好保存实例讲解
2019/09/11 Javascript
Node.js API详解之 assert模块用法实例分析
2020/05/26 Javascript
json.stringify()与json.parse()的区别以及用处
2021/01/25 Javascript
[05:41]2014DOTA2西雅图国际邀请赛 小组赛7月10日TOPPLAY
2014/07/10 DOTA
python实现从一组颜色中找出与给定颜色最接近颜色的方法
2015/03/19 Python
python字符串中的单双引
2017/02/16 Python
python中kmeans聚类实现代码
2018/02/23 Python
Python基于聚类算法实现密度聚类(DBSCAN)计算【测试可用】
2018/12/26 Python
如何使用Python 打印各种三角形
2019/06/28 Python
Python3 venv搭建轻量级虚拟环境的步骤(图文)
2019/08/09 Python
python多线程使用方法实例详解
2019/12/30 Python
Python3实现飞机大战游戏
2020/04/24 Python
Linux文件系统类型
2012/02/15 面试题
毕业生求职简历的自我评价
2013/10/23 职场文书
后勤部长岗位职责
2013/12/14 职场文书
学生请假条格式
2014/04/11 职场文书
授权委托书
2014/07/31 职场文书
社区端午节活动总结
2015/02/11 职场文书
读《方与圆》有感:交友方圆有度
2020/01/14 职场文书