vuejs设计与实现-框架设计的核心要素

90 阅读5分钟

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 的支持, 后续章节会讨论这部分内容.

总结

  1. 友好的警告信息帮助快速定位问题. 利用Tree-Shaking机制配合构建工具预定义常量的能力, 实现开发环境中打印警告信息, 生产环境中不包含用于提升开发体验的代码.
  2. 不同类型的产物. 通过<script>标签直接引用,需要 IIFE 格式的资源. 通过<script type="module">引用, 需要 ESM 格式的资源. (ESM格式的资源有两种: 用于浏览器的esm-browser.js和用于打包工具的esm-bundler.js. 区别在于对预定义常量__DEV__的处理, 前者将其替换为字面量truefalse, 后者替换为process.env.NODE_ENV !== 'production'语句)
  3. 框架会提供多种能力或功能. 比如明确使用组合式API而不使用对象式API, 可以通过特性开关关闭对应的特性, 打包时用于实现关闭功能的代码会被 Tree-Shaking 机制排除.
  4. 提供统一的错误处理接口, 用户可以通过注册自定义错误处理函数来处理全部的框架异常.

《vuejs设计与实现》 1.权衡的艺术

《vuejs设计与实现》 2.框架设计的核心要素