webpack的Tree Shaking原来要这么使用才有效

先来了解一下commonJS和ES6模块导入导出使用方法。

commonJS

utils/index.js

exports.bar = 2
module.exports = {foo: 9}
exports.foo = 1;

index.js中引入:

const a = require('./utils/index')
console.log(a) // {foo: 9}

utils/index.js

exports.bar = 2

exports.foo = 1;

index.js中引入:

const a = require('./utils/index')
console.log(a) // {foo: 1, bar: 2}

也可以这样引入:


const {foo, bar} = require('./utils/index')
console.log(foo, bar) // 1 2

commonJs规范导出和引入总结:

  1. module.exports 初始值为一个空对象 {},所以 exports 初始值也是 {}; module.exports === exports 为true
  2. exports 的改变影响不了module.exports。
  3. require() 返回的是 module.exports 而不是 exports;

ES6

定义

export const bar = 2;
export function foo() {}
const bar1 = 4;
function foo1() {}
export {bar1 , foo1}
const b = {}
export default b;

引入:


import {bar, foo, bar1, foo1} from './utils/index';

或者只引入默认:

import b from './utils/index';

可以同时引入:

import b, {bar ,foo} from './utils/index';

总结:

  1. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  2. 通过export方式导出,在导入时要加{ },export default则不需要

Tree Shaking是什么?

Tree Shaking的意思就是将你项目中的没有用到的代码删除掉,减少打包后的js文件体积大小,达到性能优化的目的。

webpack的Tree Shaking会在production模式下默认开启。

测试

新建一个测试项目,目录结构如下:

webpack.config.js写上最简单的配置如下:


const path = require('path')
const config = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: `main.min.js`,
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    usedExports: true, // 开启
  },
}

module.exports = config;

webpack的Tree Shaking会在production模式下默认开启,但是这种模式下会压缩代码,为了便于观察,这里将mode设置为development,打包后的代码不会被压缩,但是Tree Shaking会失效。

usedExports为true可以标记哪些代码使用了,哪些代码未被使用,压缩的时候标记为未被使用的就会被删除。

optimization: {
    usedExports: true, // 开启
  },

所需要安装的依赖如下:

"devDependencies": {
    "airspeed": "^1.0.4",
     "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }

这里使用的webpack版本是5.x。

 "scripts": {
    "build": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

场景1: 引入文件部分内容使用

定义模块:

utils/index.js

export function add(a, b) {
  return a + b
}

export function mins(a,b){
  return a - b
};

const obj = {}

export default obj;

入口文件index.js

// 这里只用到了add 其它代码会删除 add保留
import { add } from './utils/index'

const d = add(1,3);

export default d;

执行打包:npm run build

eval("/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"add\": () => (/* binding */ add)\n/* harmony export */ });\n/* unused harmony export mins */\nfunction add(a, b) {\r\n  return a + b\r\n}\r\n\r\nfunction mins(a,b){\r\n  return a - b\r\n};\r\n\r\nconst obj = {}\r\n\r\n/* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = (obj);\r\n\r\n\n\n//# sourceURL=webpack://testimport/./src/utils/index.js?");

可以看到minsobj都被打上了unused harmony标记,压缩的时候会被删除。

使用默认引入方式

// 这里只用到了b 其余的都会被删除
import b from './utils/index'
console.log(b) 

场景2: 未使用引入文件

index.js

import utils from './utils/index'

const d = 2;

export default d;

打包后

eval("/* unused harmony exports add, mins */\nfunction add(a, b) {\r\n  return a + b\r\n}\r\n\r\nfunction mins(a,b){\r\n  return a - b\r\n};\r\n\r\nconst obj = {}\r\n\r\n/* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = (obj);\r\n\r\n\n\n//# sourceURL=webpack://testimport/./src/utils/index.js?");

可以看到add, mins和obj都被打上了unused harmony的标记。

未使用文件引入方式还可以是下面这样。同样会被标记为未使用。

import './utils/index'

注意:如果你需要这样引入js文件,比如使用rem布局时,有时需要手动引入flexible.js。可以使用下面的方式,


require('./utils/index') 
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"add\": () => (/* binding */ add),\n/* harmony export */   \"mins\": () => (/* binding */ mins),\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction add(a, b) {\r\n  return a + b\r\n}\r\n\r\nfunction mins(a,b){\r\n  return a - b\r\n};\r\n\r\nconst obj = {}\r\n\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (obj);\r\n\r\n\n\n//# sourceURL=webpack://testimport/./src/utils/index.js?");

add mins obj都没有被打标记。

为什么require('./utils/index')的方式引入就不会被删除掉呢?

其实Webpack 不支持使用 commonjs 模块来完成 tree-shaking。用ES6模块来编写代码可以很好的完成tree-shaking。

Webpack的tree-shaking是代码在运行前完成的,而require('./utils/index')的引入是代码运行的时候才会执行的,所以tree-shaking没有办法对它进行分析删除。

而es6的引入方式是静态的,在运行之前进行了引入,所以打包出来以后就是tree-shaking处理过的。

场景三:引入node_modules中的模块

测试发现,下面的两种引入方式,不管你是否使用,都会把整个lodash打包进来。

// 全部引入
import _ from 'lodash';
// 部分引入
import {get} from 'lodash';

这是因为lodash是通过moduls.exports = _这种方式导出的。

还好lodash提供了按需引入模式:

import get from 'lodash/get';

上面的方式引入,只会引入get.js文件,其它模块都不会加载进来。

场景四:用commonJS定义,es6引入

定义:

exports.foo = 3;
exports.bar = 7;

引入:

import {foo} from './utils/index';

console.log(foo)

打包以后,看下utils/index.js文件的改变。

eval("var __webpack_unused_export__;\nexports.foo = 3;\r\n__webpack_unused_export__ = 7;\r\n\r\n\n\n//# sourceURL=webpack://testimport/./src/utils/index.js?");

7 被赋值给了_webpack_unused_export__ 这个变量,也是标记删除的一种方式。这是CommonJs规范导出文件的一种标记方式。

webpack的mode置为 production 打包后全局搜一下,找不到7。说明7没有被打包进来。

如下module.exports的方式导出,会被全部引入

module.exports = {
  foo: 3,
  bar: 7
}

引入方式1:

import {foo} from './utils/index';

console.log(foo)

引入方式2:

import obj from './utils/index';

console.log(obj.foo)

上面的两种引入方式都会把bar打包进来。

如果引入进来没有使用,同样会被全部打包进来。

import './utils/index'

所以能不能很好的利用webpack的Tree Shaking也跟你定义模块的方式有关。

总结:

  • Tree Shaking只处理函数和顶层的import/export变量,不能把没用到的类的方法消除掉
  • 项目中的工具文件,定义模块尽量使用es6规范 的export
  • 引入和导出使用最小化原则,export 和 import {} from ''

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值