视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Webpack之tree-starking 解析
2020-11-27 22:07:55 责编:小采
文档


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 关键字。

下载本文
显示全文
专题