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 相关文章推荐
经典的解除许多网站无法复制文字的绝招
Dec 31 Javascript
用js实现控制内容的向上向下滚动效果
Jun 26 Javascript
JS获取并操作iframe中元素的方法
Mar 21 Javascript
jquery将一个表单序列化为一个对象的方法
Dec 02 Javascript
什么是MEAN?JavaScript编程中的MEAN是什么意思?
Dec 18 Javascript
DWR3 访问WEB元素的两种方法实例详解
Jan 03 Javascript
详解如何使用Vue2做服务端渲染
Mar 29 Javascript
Angular2使用Angular-CLI快速搭建工程(二)
May 21 Javascript
深入理解Vue 单向数据流的原理
Nov 09 Javascript
微信小程序基于slider组件动态修改标签透明度的方法示例
Dec 04 Javascript
vue-router3.0版本中 router.push 不能刷新页面的问题
May 10 Javascript
微信小程序背景音乐开发详解
Dec 12 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中autoload的用法总结
2013/11/08 PHP
CodeIgniter控制器之业务逻辑实例分析
2016/01/20 PHP
javascript静态页面传值的三种方法分享
2013/11/12 Javascript
JS 日期比较大小的简单实例
2014/01/13 Javascript
JavaScript常用验证函数实例汇总
2014/11/25 Javascript
javascript使用call调用微信API
2014/12/15 Javascript
浅谈JavaScript事件的属性列表
2015/03/01 Javascript
js实现键盘上下左右键选择文字并显示在文本框的方法
2015/05/07 Javascript
javaScript实现可缩放的显示区效果代码
2015/10/26 Javascript
解决js函数闭包内存泄露问题的办法
2016/01/25 Javascript
JS实现动态修改table及合并单元格的方法示例
2017/02/20 Javascript
Vue-router 类似Vuex实现组件化开发的示例
2017/09/15 Javascript
vue集成百度UEditor富文本编辑器使用教程
2018/09/21 Javascript
vue中选项卡点击切换且能滑动切换功能的实现代码
2018/11/25 Javascript
详解小程序之简单登录注册表单验证
2019/05/13 Javascript
[54:06]OG vs TNC 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
Java实现的执行python脚本工具类示例【使用jython.jar】
2018/03/29 Python
Python实现SQL注入检测插件实例代码
2019/02/02 Python
python Tkinter的图片刷新实例
2019/06/14 Python
对python中基于tcp协议的通信(数据传输)实例讲解
2019/07/22 Python
python实现网站用户名密码自动登录功能
2019/08/09 Python
使用python无账号无限制获取企查查信息的实例代码
2020/04/17 Python
python 实现Requests发送带cookies的请求
2021/02/08 Python
使用phonegap获取位置信息的实现方法
2017/03/31 HTML / CSS
HTML5高仿微信聊天、微信聊天表情|对话框|编辑器功能
2018/04/23 HTML / CSS
Diamondback自行车:拥有你的冒险
2019/04/22 全球购物
文化宣传方案
2014/03/13 职场文书
群众路线教育党课主持词
2014/04/01 职场文书
观看《周恩来的四个昼夜》思想汇报
2014/09/12 职场文书
爱心捐款感谢信
2015/01/20 职场文书
2015年感恩父亲节演讲稿
2015/03/19 职场文书
借款民事起诉状范文
2015/05/19 职场文书
SQL Server中交叉联接的用法详解
2021/04/22 SQL Server
详解Python自动化之文件自动化处理
2021/06/21 Python
Golang 并发编程 SingleFlight模式
2022/04/26 Golang
python数字图像处理:图像简单滤波
2022/06/28 Python