Webpack之tree-starking 解析

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 前:

使用 babel-plugin-my-import 后:

通过对比,可以看到使用 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 关键字。

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

(0)

相关推荐

  • 浅析webpack 如何优雅的使用tree-shaking(摇树优化)

    1.什么是tree-shaking webpack 2 的到来带来的最棒的新特性之一就是tree-shaking .tree-shaking源自于rollup.js,先如今,webpack 2也有类似的做法. webpack 里的tree-shaking的到来不得不归功于es6规范的模块.为什么这么说,如今的前端模块规范很多,比较出流行的比如commonJS , AMD , es6 ,我简单的说一下commonJS和es6模块的区别. commonJS 模块 commonJS的模块规范在Node

  • 浅谈webpack 自动刷新与解析

    前端需要频繁的修改js和样式,且需要根据浏览器的页面效果不断的做调整:而且往往我们的开发目录和本地发布目录不是同一个,修改之后需要发布一下:另外一点就是并不是所有的效果都可以直接双击页面就能看到,我们常常需要在本地用nginx建一个站点来观察(自己电脑上ok了才放到测试环境去).所以如果要用手工刷新浏览器和手动(或点击)发布,还要启动站点,确实是个不小的体力活.而这三点webpack可以帮我们做到. webpack-dev-serverwebpack 是通过webpack-dev-server(

  • webpack的tree shaking的实现方法

    webpack的tree shaking util.js export const a = () => { console.log("a123456方法"); }; export const b = () => { console.log("b123456方法"); }; main.js import {a} from './utils'; a(); sideEffects 一般而言,上述代码,在 webpack 进行 tree shaking 能够不打

  • 浅谈Webpack核心模块tapable解析

    本文介绍了Webpack核心模块tapable,分享给大家,具体如下: 前言 Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,是对前端项目实现自动化和优化必不可少的工具,Webpack 的 loader (加载器)和 plugin (插件)是由 Webpack 开发者和社区开发者共同贡献的,而目前又没有比较系统的开发文档,想写加载器和插件必须要懂 Webpack 的原理,即看懂 Webpack 的源码, tapable 则是 Webpack 依赖的核心库,可以说不懂

  • 关于webpack代码拆分的解析

    最近在学习VUE的时候接触到了webpack,今天就留个小笔记,介绍一下webpack代码拆分 如果利用 webpack 将项目中的所有代码打包在一起,很多时候是不适用的,因为代码中有些东西我们总是希望将其拆分出来.比如: 样式表,希望利用 link 标签引入 使用概率较低的模块,希望后期需要的时候异步加载 框架代码,希望能利用浏览器缓存下部分不易变动的代码 下面是我在阅读 webpack 的官方文档时候,记录的一些笔记,部分地方使用了自己的话来讲,力图让它显得更易懂. 按需加载拆分 webpa

  • 详解webpack介绍&安装&常用命令

    webpack系列目录 webpack 系列 二:webpack 介绍&安装 webpack 系列 三:webpack 如何集成第三方js库 webpack 系列 四:webpack 多页面支持 & 公共组件单独打包 webpack 系列 五:webpack Loaders 模块加载器 webpack 系列 六:前端项目模板-webpack+gulp实现自动构建部署 基于webpack搭建纯静态页面型前端工程解决方案模板, 最终形态源码见github: https://github.com

  • webpack学习--webpack经典7分钟入门教程

    一.什么是 Webpack Webpack 是一个模块打包器.它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源. webpack模式图 二.为什么使用webpack 市面上已经存在的模块管理和打包工具并不适合大型的项目,尤其单页面 Web 应用程序.最紧迫的原因是如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源.而webpack有以下几个优点: 1.将依赖树拆分成按需加

  • webpack与SPA实践之管理CSS等资源的方法

    上一篇介绍了如何使用webpack搭建一个稳定的支持本地服务.自动刷新.模块热替换.使用ES6编写JavaScript的开发环境,本篇主要介绍webpack如何处理HTML应用三大元素的另一元素 – CSS及其他诸如图片.字体文件或者数据配置文件等资源. 前言 在学习使用webpack时,我们需要明白无论它怎么设计,它的工作原理.流程是什么,最根本的它处理的还是HTML文档中的HTML标签.JavaScript.CSS.图片等资源,而且最终的处理结果依然必须是一个HTML文档,包括DOM.Jav

  • webpack 模块热替换原理

    全称是Hot Module ReplaceMent(HMR),理解成热模块替换或者模块热替换都可以吧,和.net中的热插拔一个意思,就是在运行中对程序的模块进行更新.这个功能主要是用于开发过程中,对生产环境没有任何帮助(这一点区别.net热插拔).效果上就是界面的无刷新更新. HMR基于WDS,style-loader可以通过它来实现无刷新更新样式.但是对于JavaScript模块就需要做一点额外的处理,怎么处理继续往下看.因为HMR是用于开发环境的,所以我们修改下配置,做两份准备.一个用于生产

  • 详解用Webpack与Babel配置ES6开发环境

    安装 Webpack 安装: # 本地安装 $ npm install --save-dev webpack webpack-cli # 全局安装 $ npm install -g webpack webpack-cli 在项目根目录下新建一个配置文件-- webpack.config.js 文件: const path = require('path'); module.exports = { mode: 'none', entry: './src/index.js', output: { f

  • webpack 样式加载的实现原理

    加载css需要用到css-loader和style-loader css-loader将@import 和 url 处理成正规的ES6 import ,如果@import指向的是一个外部资源,css-loader会跳过,而只会对内部资源做处理.css-loader处理之后,style-loader会将输出的css注入到打包文件中.css默认是inline模式,且实现了HMR接口.但inline不太适用于生产环境(全部输出在页面上).还需要用extracttextplugin生成一个单独的css文

随机推荐