-
必要的警告信息
合理的警告信息,能让用户更清晰且快速地定位问题。
const mount = function (el) { document.querySelector(el).appendChild(document.createTextNode('Hello world.')) } mount('#not-exists') // Uncaught TypeError: Cannot read properties of null (reading 'appendChild')
根据此信息,我们可能很难去定位问题出在哪里。所以在设计框架时,需要提供更为有用的信息来帮助用户定位问题。
const mount = function (el) { const container = document.querySelector(el) if (!container) { throw new Error( `Target selector "${el}" returned null.` ) } container.appendChild(document.createTextNode('Hello world.')) } mount('#not-exists')
-
直观的输出内容
在 Vue.js 3 中,当我们在控制台打印一个 Ref 数据时:
const count = Vue.ref(0) console.log(count) // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}
这样的数据阅读起来是不友好的。
所以在 Vue.js 3 的源码中提供了
initCustomFormatter
的函数用于在开发环境初始化自定义的 formatter。在 Chrome 浏览器中可以通过
设置 -> 控制台 -> 启用自定义格式设置工具
来开启。const count = Vue.ref(0) console.log(count) // Ref<0>
在 Vue.js 3 的源码中,通过一些环境常量来决定某些代码只会在特定的环境中生效:
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
这里的 __DEV__
常量实际上是通过 rollup.js 的插件配置来预定义的。
在开发环境的版本中 __DEV__
会被设置为 true,上面的代码就相当于:
if (true && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
而在生产版本中 __DEV__
会被设置为 false,上面的代码等价于:
if (false && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
我们可以发现,这段代码它的判断条件假,所以它永远不会被执行,这种代码称为 dead code,在构建资源的时候会被移除。
仅仅通过环境常量的形式来排除 dead code 是不够的。
// utils.js
export function foo (obj) {
obj && obj.foo
}
export function bar () {
obj && obj.bar
}
// input.js
import { foo } from 'utils'
foo()
像上面的这种情况,bar() 并未被使用到,那么它就不应该出现在打包后的代码中。要做到这一点,我们就需要 Tree-Shaking。
简单地说,Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是 dead code。
想要实现 Tree-Shaking,必须要满足一个条件:模块必须是 ESM(ES Module)。因为 Tree-Shaking 依赖 ESM 的静态结构。我们以 rollup.js 为例看看 Tree-Shaking 如何工作:
还是上面的代码,我们通过 rollup.js 进行构建
npx rollup input.js -f esm -o bundle.js
我们可以看到 bundle.js 中并未包含 bar()
函数的内容:
// bundle.js
function foo (obj) {
obj && obj.foo;
}
foo();
这就说明 Tree-Shaking 起了作用。由于我们并未使用到 bar()
函数的内容,因此它被作为 dead code 删除了。但是我们通过代码可以发现,foo()
函数似乎没什么作用:它仅仅是读取了对象中的值。把这段代码删了也不会对我们的程序产生影响,那么 rollup.js 为什么不把这段代码也作为 dead code 删除掉呢?
我们把 input.js 中的代码改造一下:
import { foo } from './utils'
const log = {
count: 0
}
const obj = Proxy({}, {
get (target, prop) {
log.count++
return target[prop]
}
})
foo(obj)
在我们读取 obj 中的某个属性时,会记录它的读取次数。
这也就是 Tree-Shaking 中的第二个关键点——副作用。foo()
函数的调用,会产生副作用,那么就不能将其移除。
会不会产生副作用,我们只有在代码运行的时候才会知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 是非常困难的。
因此,像 rollup.js 这类工具提供了一个机制,让我们开发明确地告诉构建工具某些代码是不会产生副作用,你可以放心地移除它。我们修改一下 input.js:
import { foo } from './utils'
/*#__PURE__*/ foo()
通过 /*#__PURE__*/
注释告诉构建工具,我这段代码是纯的,不会产生副作用。此时我们再执行构建命令,会发现 bundle.js
里面是空的,这说明 Tree-Shaking 生效了。
我们需要针对不同的运行环境提供不同的构建产物。通过在 rollup.config.js 中配置
iife
:script 标签直接引用esm
:<script type="module">
cjs
:CommonJS
export default {
input: 'input.js',
output: {
file: 'output.js',
format: 'iife' // 指定模块形式
}
}
一个特性对应一个开关,通过开关的形式来决定是否需要某些代码,从而减小资源的体积。
提供统一的错误处理接口,并且让用户可以自行的注册错误处理函数来处理错误:
// utils.js
let handleError = null
export default {
foo (fn) {
callWithErrorHandling(fn)
},
registerErrorHandler (fn) {
handleError = fn
}
}
function callWithErrorHandling (fn) {
try {
fn && fn()
} catch (e) {
handleError(e)
}
}
- 略
- 下一章: Vue.js 3 的设计思路