2.1 提升用户的开发体验
- 友好的警告信息
在vuejs源码中有许多warn函数的调用, 提供尽可能有用的信息帮助用户快速定位问题, 从而提升开发体验.
- 直观地输出结果
打印ref数据时,控制台会输出
RefImpl{ _rawValue: 0, _shallow: false, __v_isRef: true, _value: 0 }很不直观.
可以打开开发工具的设置, 勾选 console enable custom formatters选项. 源码中initCustomFormatter函数是开发环境下初始化自定义formatter的. 现在浏览器会直观地输出Ref<0>.
2.2 控制框架代码的体积
vuejs使用rollupjs对项目进行构建, 通过插件配置预定义常量, 在开发环境中提供友好的警告信息的同时, 不会增加生产环境代码的体积
// 开发环境(如vue.global.js)__DEV__设置为true
// 生产环境(如vue.global.prod.js)设置为false
// 此时代码(dead code)用远不会执行, 也不会出现在最终产物中
if(__DEV__ && !res){
warn(`Failed to mount app! mount target selector "${container}" returned null`)
}
2.3 框架要做到良好的Tree-Shaking
消除永远不会执行的代码(dead code). 要实现Tree-Shaking, 模块必须是ESM(ES Module), 因为Tree-Shaking依赖ESM的静态结构.
// input.js
import { foo } from './utils.js'
foo()
// utils.js
export function foo(obj){
obj && obj.foo
}
export function bar(obj){
obj && obj.bar
}
// 通过打包构建, 消除没有导入的 bar
// npx rollup input.js -f esm -o bundle.js
// bundle.js
function foo(obj){
obj && obj.foo
}
foo()
🌰中没有bar函数. 但foo函数的执行仅仅是读取了对象的值, 好像也可以作为 dead code 删掉. 如果一个函数调用会产生副作用(当函数调用时, 对外部产生影响. 如修改了全局变量), 那么就不能将其移除. js本身是动态语言, 静态地分析很困难. 可以通过注释明确告诉 rollupjs 这段代码不会产生副作用, 可以移除它.
// input.js
import { foo } from './utils.js'
/*#__PURE__*/ foo()
// bundle.js
// 此时打包内容为空
基于此, 编写框架时需要合理使用/*#__PURE__*/注释. 如
export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
2.4 框架应该输出怎样的构建产物
不同类型的产物一定有对应的需求背景. 如果希望直接在html页面中使用<script>标签引入, 需要输出一种叫做 IIFE (立即调用的函数表达式 全称Immediately Invoked Function Expression) 的资源.
<body>
<script src="/path/to/vue.js"></script>
<script>
const { createApp } = Vue
// ...
</script>
</body>
// vue.global.js文件就是IIFE形式的资源
var vue = (function(){
// ...
exports.createApp = createApp;
// ...
return exports
}({}))
当进行服务端渲染时, Vuejs代码在 Node.js 环境中运行而非浏览器. 资源的模块格式应该是 cjs(CommonJS)
// rollup.config.js
const config = {
input: 'input.js',
output: {
file: 'output.js',
format: 'cjs' // 指定模块形式
}
}
在 Node.js 中通过 require语句引用资源
2.5 特性开关
- 对于用户关闭的特性, 可以利用 Tree-Shaking机制进行优化
- 通过特性开关为框架添加新的特性, 而不用担心资源体积变大. 也可以通过特性开关来支持遗留API, 不使用遗留API时使最终打包体积最小化.
利用预定义常量插件来实现
// vuejs构建资源时, 如果是供打包工具使用的 __FEATURE_OPTIONS_API__ 会被替换为 __VUE_OPTIONS_API__ . 用户可以通过设置 __VUE_OPTIONS_API__ 预定义常量来选择是否开启选项式api
{
__FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true
}
// 2.x options
if(__FEATURE_OPTIONS_API__){
currentInstance = instance
pasueTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
2.6 错误处理
错误处理是框架开发重要的环节.
提供统一的错误处理接口registerErrorHandler函数可以注册自定义错误处理程序, 在callWithErrorHandling函数内部捕获错误后传递给用户注册的错误处理程序.
// 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)
}
}
// 用户侧使用代码
import utils from 'utils.js'
utils.registerErrorHandler(e => {
console.log(e)
})
utils.foo(() => { /*...*/ })
// vuejs
const app = createApp(App)
app.config.errorHandler = () => {
// 错误处理
}
错误处理的能力由用户控制, 既可以选择忽略错误也可以调用上报程序将错误上报给监控系统.
2.7 良好的TypeScript类型支持
使用ts编写代码与对ts类型支持友好是两件事.
// 函数接收任意类型, 返回值的类型由参数决定.
function foo(val: any){
return val
}
const res = foo('str')
// const res: any
function foo<T extends any>(val: T): T{
return val
}
const res = foo('str')
// const res: 'str'
源码中runtime-core/src/apiDefineComponent.ts 整个文件会在浏览器中执行的代码只有三行, 但全部代码接近200行, 这些代码都是在为类型支持服务.
除了做类型推导, 从而达到更好的类型支持外, 还需要考虑对 TSX 的支持, 后续章节会讨论这部分内容.
总结
- 友好的警告信息帮助快速定位问题. 利用Tree-Shaking机制配合构建工具预定义常量的能力, 实现开发环境中打印警告信息, 生产环境中不包含用于提升开发体验的代码.
- 不同类型的产物. 通过
<script>标签直接引用,需要 IIFE 格式的资源. 通过<script type="module">引用, 需要 ESM 格式的资源. (ESM格式的资源有两种: 用于浏览器的esm-browser.js和用于打包工具的esm-bundler.js. 区别在于对预定义常量__DEV__的处理, 前者将其替换为字面量true或false, 后者替换为process.env.NODE_ENV !== 'production'语句) - 框架会提供多种能力或功能. 比如明确使用组合式API而不使用对象式API, 可以通过特性开关关闭对应的特性, 打包时用于实现关闭功能的代码会被 Tree-Shaking 机制排除.
- 提供统一的错误处理接口, 用户可以通过注册自定义错误处理函数来处理全部的框架异常.
《vuejs设计与实现》 1.权衡的艺术
《vuejs设计与实现》 2.框架设计的核心要素