先来了解一下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规范导出和引入总结:
- module.exports 初始值为一个空对象 {},所以 exports 初始值也是 {}; module.exports === exports 为true
- exports 的改变影响不了module.exports。
- 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';
总结:
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过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?");
可以看到mins和obj都被打上了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 ''