跳转到内容

框架设计的核心要素

框架设计核心要素除了功能的实现,还有良好的报错定位、良好的按需导入减少打包体积等。

提升用户的开发体验

Vue.js 3 源码有一个 warn 函数,用于抛出相对应的错误提示开发者在使用框架时有什么错误。如下方代码:

js
createApp(App).mount('#not-exist')

Vue.js 会根据无法根据所提供的选择器找到对应的 DOM 节点抛出报错。如果 Vue.js 内部没有做处理,则浏览器控制台会抛出 JavaScript 底层错误,开发者不好排查问题所在。Vue.js 3 源码的 warn 函数实际上也是调用了 console.warn 方法。

js
[Vue warn]: Failed to mount app: mount target selector '#not-exist' returned null.
js
Uncaught TypeError: Cannot read property 'xxx' of null

除了更好定位的报错信息提示外,还有其他地方可以入手让开发者体验更好,例如浏览器允许自定义编写 formatter 自定义 console.log 输出变量。Vue.js 源码有一个 initCustomFormatter 函数,用于初始化自定义的 formatter。浏览器开启使用自定义 formatter 功能后控制台输出变量时,Vue.js 会根据 initCustomFormatter 函数的返回值来决定是否使用自定义的 formatter

js
RefImpl {_rawValue: 0, _shallow: false, __v_isRef: true, _value: 0}
js
Ref<0>

控制框架代码体积

框架的大小也是衡量框架标准之一,框架代码体积越小,浏览器加载所需资源越少。不过前面提到 Vue.js 提供了完善的报错信息,这无疑需要更多的代码。因此 Vue.js 使用了环境检查。

js
if (__DEV__ && !res) {
  warn(`Failed to resolve component: ${componentName}`)
}

上方示例代码中的 __DEV__ 就是用于环境判断。Vue.js 对 rollup.js 做了项目构建,如果是开发环境,__DEV__true ,如果是生产环境,__DEV__false。因此 Vue.js 在开发环境下会保留报错信息,而在生产环境下不会有这方面的代码,从而减小代码体积。

做到良好的 Tree-Shaking

Tree-Shaking 是消除永远不会被执行的代码(dead code)的过程,主要在前端领域应用。rollup.js 和 webpack 等构建工具都支持 Tree-Shaking。模块必须是 ESM(ES Module),因为 Tree-Shaking 依赖 ESM 的静态结构.

javascript
import { foo } from './utils.js'
foo()
javascript
export function foo(obj) {
  obj && obj.foo
}
export function bar(obj) {
  obj && obj.bar
}

执行构建命令 npx rollup input.js -f esm -o bundle.js 后,bundle.js 中只包含 foo 函数的代码,bar 函数作为 dead code 被删除,说明 Tree-Shaking 起作用了.

缺点

如果函数调用会产生副作用(如修改全局变量),则不能将其移除. JavaScript 动态性使得静态分析副作用困难,但可以通过注释 /*#__PURE__*/ 明确告知构建工具该代码不会产生副作用,从而进行 Tree-Shaking.

Vue.js 中的 Tree-Shaking

  • 合理使用注释:Vue.js 3 源码中大量使用 /*#__PURE__*/ 注释,主要应用于模块内函数的顶级调用,因为这些顶级调用可能产生副作用,而函数内调用则不会.
  • 注释作用:该注释不仅作用于函数,还可以应用于任何语句,且被 rollup.js、webpack 和压缩工具(如 terser)等识别,有助于进一步优化代码,减少最终构建资源的体积.

框架应该输出怎样的构建产物

  • 环境区分
    • vue.global.js:用于开发环境,包含必要的警告信息,采用 IIFE 格式,便于直接通过 <script> 标签在 HTML 页面中引入使用.
    • vue.global.prod.js:用于生产环境,不包含警告信息,同样采用 IIFE 格式,减少代码体积,提高性能.
  • 使用场景区分
    • vue.esm-browser.js:ESM 格式资源,用于直接通过 <script type="module"> 标签在浏览器中引入,适用于现代浏览器对原生 ESM 的支持.
    • vue.esm-bundler.js:ESM 格式资源,提供给打包工具(如 rollup.js、webpack)使用,通过 package.json 中的 module 字段指定. 与 -browser 版本的区别在于对 __DEV__ 常量的处理方式,使用 (process.env.NODE_ENV !== 'production') 替换,以便用户通过 webpack 配置自行决定构建资源的目标环境.
  • Node.js 环境支持: 为了支持服务端渲染,Vue.js 还提供 CommonJS(cjs)格式的资源,可通过修改 rollup.config.js 配置 format: 'cjs' 来输出,便于在 Node.js 中通过 require 语句引用.
javascript
var Vue = (function(exports){
  // ...
  exports.createApp = createApp;
  // ...
  return exports
}({}))
javascript
const configIIFE = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'iife' // 指定模块形式
  }
}
javascript
const configESM = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'esm' // 指定模块形式
  }
}
javascript
// 输出  格式资源
const configCJS = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'cjs' // 指定模块形式
  }
}

特性开关

  • 灵活性:框架通过特性开关提供多种特性供用户选择,用户可按需开启或关闭特性,不会因添加新特性或升级框架导致资源体积无谓增大.
  • 支持遗留 API:框架升级时,特性开关可用于兼容遗留 API,新用户可关闭这些遗留 API,使打包资源体积最小化.
  • 原理:与 __DEV__ 常量类似,利用构建工具(如 rollup.js)的预定义常量插件来实现.

Vue.js 3 中的特性开关应用

  • 选项 API 兼容:Vue.js 3 推荐使用 Composition API,但仍兼容 Vue.js 2 的组件选项 API. 用户可通过 __VUE_OPTIONS_API__ 开关关闭选项 API 兼容特性.

    javascript
    {
      __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
    }
    js
    if (__FEATURE_OPTIONS_API__) {
      currentInstance = instance;
      pauseTracking();
      applyOptions(instance, Component);
      resetTracking();
      currentInstance = null;
    }
    js
    //  插件配置,开启特性
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: JSON.stringify(true)
    })
  • 效果:关闭特性开关后,相关代码在构建时会被 Tree-Shaking 移除,从而减小最终打包资源的体积.

错误处理

下面看一个代码示例:

js
export default {
  foo(fn) {
    fn && fn();
  }
}
js
import utils from './utils.js';
utils.foo(() => {
  // ...
})

如果使用者在使用 foo 方法时出错该怎么解决?一个方法是用户自己处理,即每使用一次都加一个 try...catch ,但这样无疑会增加使用者的负担。另一个方法是框架提供统一的错误处理机制,在框架内部捕获错误并处理,这么做的好处是可以提供一个错误处理接口给用户。

js
utils.foo(() => {
  try {
    // ...
  } catch(e) {
    console.error(e)
  }
})
js
let handlerFn = null
export default {
  foo(fn) {
    callWithErrorHanding(fn)
  },
  // 用户调用该方法传参错误处理函数
  registerErrorHandler(errFn) {
    handlerFn = errFn
  }
}

// 错误捕获函数封装
const callWithErrorHanding = (fn) => {
  try {
    fn && fn();
  } catch (e) {
    handlerFn(e)
  }
}

框架提供错误捕获函数后,用户可以自己选择如何处理,可以置之不理,也可以调用程序上报给监控系统。Vue.js 错误处理的原理就是如此,可以源码中搜索到 callWithErrorHandling 函数,该函数就是 Vue.js 提供的错误处理函数。另外,在 Vue.js 中也可以注册统一的错误处理函数:

js
import App from './App.vue'
const app = createApp(App)
app.config.errorHandler = (err, vm, info) => {
  // 处理错误
}

良好的 TypeScript 类型支持

TypeScript 的好处:

  • 代码即文档:通过类型注释,代码本身就具有一定的文档性质,便于理解和维护.
  • 编辑器自动提示:类型信息能让编辑器提供自动提示功能,提高开发效率.
  • 避免低级 bug:在一定程度上,类型检查能够帮助发现潜在的错误,减少低级 bug.
  • 增强代码可维护性:清晰的类型定义有助于维护和扩展代码.

衡量框架对 TS 类型支持的水平:

  • 误区:使用 TS 编写框架并不等同于对 TS 类型支持友好. 例如,下面的函数虽然用 TS 编写,但返回值类型丢失了:
    typescript
    function foo(val: any) {
      return val;
    }
    const res = foo('str'); // res 的类型是 any
  • 正确做法:通过泛型等手段,使类型信息得以保留和推导:
    typescript
    function foo<T extends any>(val: T): T {
      return val;
    }
    const res = foo('str'); // res 的类型是 'str'
  • 框架实现完善的 TS 类型支持的难度:在大型框架中,完善的类型支持需要付出大量努力,如 Vue.js 的 apiDefineComponent.ts 文件,大部分代码都是为类型支持服务的,真正运行的代码很少.

除了类型推导外,框架还需考虑对 TSX 的支持,TSX 是 TypeScript 的 JSX 扩展,允许在 TypeScript 中使用 JSX 语法.

总结

  • 提升用户开发体验:良好的报错定位有助于开发者快速排查问题,如 Vue.js 3 的 warn 函数提供清晰的错误提示;自定义 console.log 输出格式(如 Vue.js 的 initCustomFormatter 函数)可使变量信息更直观.
  • 控制框架代码体积:使用环境检查(如 __DEV__ 常量)在不同环境下输出不同代码,开发环境保留报错信息,生产环境移除以减小体积.
  • 良好的 Tree-Shaking:利用构建工具(如 rollup.js、webpack)和 ESM 模块的静态结构,消除未执行的代码,减少打包体积;通过注释 /*#__PURE__*/ 告知工具代码无副作用,进一步优化.
  • 合理的构建产物输出:根据环境和使用场景输出不同格式的资源,如开发环境的 vue.global.js(IIFE 格式)、生产环境的 vue.global.prod.js(IIFE 格式)、浏览器直接引入的 vue.esm-browser.js(ESM 格式)、打包工具使用的 vue.esm-bundler.js(ESM 格式)以及 Node.js 环境的 CommonJS 格式资源.
  • 特性开关:提供灵活性和对遗留 API 的支持,用户可按需开启或关闭特性,避免资源体积无谓增大;框架内部通过预定义常量插件实现特性开关逻辑.
  • 错误处理:框架可提供统一的错误处理机制,内部捕获错误并提供处理接口给用户,减轻用户负担;如 Vue.js 的 callWithErrorHandling 函数和全局 errorHandler 配置.
  • 良好的 TypeScript 类型支持:代码即文档、编辑器自动提示、避免低级 bug、增强代码可维护性;衡量框架对 TS 类型支持的水平需看类型信息是否准确保留和推导,而非仅使用 TS 编写;大型框架实现完善的 TS 类型支持需付出大量努力,还需考虑对 TSX 的支持.