框架设计的核心要素
框架设计核心要素除了功能的实现,还有良好的报错定位、良好的按需导入减少打包体积等。
提升用户的开发体验
Vue.js 3 源码有一个 warn
函数,用于抛出相对应的错误提示开发者在使用框架时有什么错误。如下方代码:
createApp(App).mount('#not-exist')
Vue.js 会根据无法根据所提供的选择器找到对应的 DOM 节点抛出报错。如果 Vue.js 内部没有做处理,则浏览器控制台会抛出 JavaScript 底层错误,开发者不好排查问题所在。Vue.js 3 源码的 warn
函数实际上也是调用了 console.warn
方法。
[Vue warn]: Failed to mount app: mount target selector '#not-exist' returned null.
Uncaught TypeError: Cannot read property 'xxx' of null
除了更好定位的报错信息提示外,还有其他地方可以入手让开发者体验更好,例如浏览器允许自定义编写 formatter
自定义 console.log
输出变量。Vue.js 源码有一个 initCustomFormatter
函数,用于初始化自定义的 formatter
。浏览器开启使用自定义 formatter
功能后控制台输出变量时,Vue.js 会根据 initCustomFormatter
函数的返回值来决定是否使用自定义的 formatter
。
RefImpl {_rawValue: 0, _shallow: false, __v_isRef: true, _value: 0}
Ref<0>
控制框架代码体积
框架的大小也是衡量框架标准之一,框架代码体积越小,浏览器加载所需资源越少。不过前面提到 Vue.js 提供了完善的报错信息,这无疑需要更多的代码。因此 Vue.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 的静态结构.
import { foo } from './utils.js'
foo()
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
语句引用.
var Vue = (function(exports){
// ...
exports.createApp = createApp;
// ...
return exports
}({}))
const configIIFE = {
input: 'input.js',
output: {
file: 'output.js',
format: 'iife' // 指定模块形式
}
}
const configESM = {
input: 'input.js',
output: {
file: 'output.js',
format: 'esm' // 指定模块形式
}
}
// 输出 格式资源
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, }
jsif (__FEATURE_OPTIONS_API__) { currentInstance = instance; pauseTracking(); applyOptions(instance, Component); resetTracking(); currentInstance = null; }
js// 插件配置,开启特性 new webpack.DefinePlugin({ __VUE_OPTIONS_API__: JSON.stringify(true) })
效果:关闭特性开关后,相关代码在构建时会被 Tree-Shaking 移除,从而减小最终打包资源的体积.
错误处理
下面看一个代码示例:
export default {
foo(fn) {
fn && fn();
}
}
import utils from './utils.js';
utils.foo(() => {
// ...
})
如果使用者在使用 foo
方法时出错该怎么解决?一个方法是用户自己处理,即每使用一次都加一个 try...catch
,但这样无疑会增加使用者的负担。另一个方法是框架提供统一的错误处理机制,在框架内部捕获错误并处理,这么做的好处是可以提供一个错误处理接口给用户。
utils.foo(() => {
try {
// ...
} catch(e) {
console.error(e)
}
})
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 中也可以注册统一的错误处理函数:
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 的支持.