框架提供了哪些构建产物?产物的模块格式?没有按照预期使用框架的警告信息,如何快速定位问题?开发版本的构建和生成版本的构建有何区别?热更新 HMR 框架层面的支持?自主选择需要的功能,能否选择关闭其他功能从而减少打包体积?

一、提升开发体验

在 vue.js 3 中,我们打印一个 ref 数据时:

1
2
const count = ref(0);
console.log(count);

控制台打印结果为:

1
RefImpl { _rawValue:0,_shallow: false,__v_isRef:true, _value: 0 }

控制台打印的输出结果很不直观(当然我们可以选择打印 count.vlaue,输出的结果为 0),其实 Chrome 浏览器已经提供了 RefImpl 类的打印设置,打开 devtool 的设置,勾选“console” => enable custom formatters 选项,浏览器的打印结果就会变成

1
Ref<0>

当然还包括错误提示。

二、框架代码的体积

在实现同样功能的情况
下,当然是用的代码越少越好,这样体积就会越小,最后浏览器加载
资源的时间也就越少。这时我们不禁会想,提供越完善的警告信息就
意味着我们要编写更多的代码,这不是与控制代码体积相悖吗?

vue.js3 的源码,我们会发现每一个 warn 函数的调用都会配合DEV常量的检查:

1
2
3
if (__DEV__ && !res) {
warn(`Fail to mount app:mount target selector "${container}" returned null`);
}

打印警告信息的前提是:DEV常量为 true,DEV常量就是达到目的的关键。vue.js 是使用 rollup.js 对项目进行构建的,这里的 DEV 常量实际
上是通过 rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的
DefinePlugin 插件。vue.js 在输出资源时,会存在开发环境(vue.global.js)和生产环境(vue.global.prod.js)。开发环境时,DEV 常量值为 true,生产环境时,DEV 常量值为 false。可以看到,DEV常量替换为字面量 false 时,判断条件为 false,就不会打印警告信息了。不会执行的代码(dead code),不会被打包到生产环境。
这样的目的就是,在开发环境中为用户提供友好的警告信息的 同时,不会增加生产环境代码的体积。

三、Tree-Shaking

DEV 常量只是控制了警告信息的打印,框架的代码量不会随警告信息的增加而增加。这样做还不够,vue.js 3 内建了很多组件,例如 Transition 组件,如果我们在项目中未使用该组件,那么打包时,该组件的代码不需要被打包到生产环境。这时候就需要 Tree-Shaking 了。

Tree-Shaking 是指在打包时,只保留用到的代码,未用到的代码不会被打包到生产环境。vue.js 3 使用了 rollup.js 进行打包,rollup.js 提供了 Tree-Shaking 功能。

实现 Tree-Sharking 必须满足的条件:模块必须为 ES Module,因为 Tree-Sharking 依赖 ESM 的静态结构。

举例分析:

1
2
3
4
-demo;
--package.json;
--input.js;
--utils.js;

首先安装 rollup.js

1
2
3
yarn add rollup -D
// 或者
npm i rollup -D

input.js 和 utils.js

1
2
3
4
5
6
7
8
9
10
// input.js
import { foo } from "./utils.js";
foo();
// utils.js
export function foo(obj) {
obj && obj.foo;
}
export function bar(obj) {
obj && obj.bar;
}

代码很简单,定义并导出 utils.js 中的 2 个函数,分别是 foo 函数和 bar 函数,然后在 input.js 中导入并调用 foo 函数(我们没有导入 bar 函数)。

当我们执行构建命令:

1
npx rollup input.js -f esm -o bundle.js

bundle.js 中并未包含 bar 函数的代码,这就是 Tree-Shaking 的效果。这就是因为 bar 函数被作为 dead code 被删除了。

但是,我们可以发现 foo 函数也没有什么意义(仅仅是读取对象的值),为什么 rollup.js 不把 foo 函数也删除呢?

这里涉及 Tree-Sharking 的第二个关键点:副作用

所谓副作用,是指在调用函数时,除了返回函数的返回值之外,还会对函数外的其他变量产生影响。

上面的代码明显是读取对象的值,怎么会产生副作用呢?其实是有可能的,试想一下,如果 obj 对象是一个通过 Proxy 创建的代理对象,那么当我们读取对象属性时,就会触发代理对象的 get 夹子(trap),在 get 夹子中是可能产生副作用的,例如我们在 get 夹子中修改了某个全局变量。而到底会不会产生副作用,只有代码真正运行的时候才能知 JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度。

rollup.js 是如何知道代码不会产生副作用,可以放心移除呢?

1
2
import { foo } from "./utils.js";
/*#__PURE__*/ foo();

这里需要使用的是 /*#__PURE__*/,它的作用就是告诉 rollup.js,这段代码不会产生副作用,可以放心使用 Tree-Sharking。

这里需要理解的是,通常产生副作用的代码都是模块内函数的顶级调用。

1
2
3
4
foo(); // 顶级调用
function bar() {
foo(); // 函数内部调用
}

对于顶级调用来说,是可能产生副作用的;但对于函
数内调用来说,只要函数 bar 没有被调用,那么 foo 函数的调用自然
不会产生副作用。

四、小结

预定义 DEV 常量,从而实现
仅在开发环境中打印警告信息,而生产环境中则不包含这些用于提升
开发体验的代码,从而实现线上代码体积的可控性。
Tree-Shaking 是一种排除 dead code 的机制,Tree-Shaking 本身基于 ESM,并且 JavaScript 是一门动态语言,通过纯静态
分析的手段进行 Tree-Shaking 难度较大,因此大部分工具能够识别
/#PURE/ 注释,在编写框架代码时,我们可以利用
/#PURE/ 来辅助构建工具进行 Tree-Shaking。