框架设计的核心要素
提升用户的开发体验
开发体验是衡量一个框架的重要指标之一
提供友好的警告信息至关重要,这有助于开发者快速定位问题
例如,在 vue3 中,当我们创建一个 Vue.js 应用并试图将其挂载到一个不存在的 DOM 节点是,就会收到一条警告信息
[Vue warn]:Failed to mount app: mount target selector '#not-exist' return null.
而不是
TypeError: Cannot read property 'xxx' of null
这是因为,框架内部为我们做了优化
控制框架代码的体积
- 生产环境中不需要用于提升开发体验的代码,比如打印警告信息等等
- 框架通过 Tree-Shaking 机制,配合构建工具预定义常量的能力去去除在开发环境中所用到的代码
什么是预定义常量
预定义常量一般由构建工具根据编译环境、操作系统、编译选项等因素来确定并指定其值。它们可以用于编写不同环境下的代码或在不同的编译选项下执行不同的操作。
预定义常量的使用有以下几个常见的用途:
- 条件编译:可以使用预定义常量来在编译过程中根据不同的条件选择性地编译某块代码。例如,可以使用预定义常量来判断当前是否处于调试模式,并选择性地打印调试信息。
- 平台兼容性:可以使用预定义常量来区分不同的操作系统或编译器,并根据需要进行相应的兼容性处理。例如,可以使用预定义常量来判断当前操作系统是否为 Windows,从而执行相应的操作。
- 版本控制:可以使用预定义常量来定义代码的版本号或构建版本号,并在代码中使用它们进行相应的处理。例如,可以使用预定义常量来区分不同的代码版本,并根据需求执行不同的逻辑。
总之,配合构建工具预定义常量可以对代码的编译过程进行灵活控制,从而满足不同环境下的需求,提高代码的可维护性和可移植性。
例如:在 rollup 中,我们可以用如下代码配置预定义常量:
// 定义预定义常量
define: {
// 定义一个 IS_PROD 常量,值为是否是生产环境
'IS_PROD': isProduction ? 'true' : 'false',
}
框架要做到良好的 Tree-Shaking
Tree-Shaking 是一种排除无用代码的机制
Tree-Shaking 本身基于 ESM,并且 JavaScript 是一门动态语言,通过纯静态分析的手段进行 Tree-Shaking 较大,因此大部分工具能够识别
/*#__PURE__*/
注释,在编写框架代码时没我们可以利用/*#__PURE__*/
来辅助构建工具进行 Tree-Shaking
/*#__PURE__*/ 注释的作用
告诉构建工具,如果这个函数没有被调用,Tree-Shaking 的时候可以放心去除不用担心有副作用,也就是将函数标记为纯函数。
为什么在 JavaScript 中通过纯静态分析的手段进行 Tree-Shaking 较大
- 变量可变性:由于JavaScript的动态特性,变量的值和类型可以在运行时动态变化。这意味着在编译时很难确定哪些代码是不可访问的,因此需要对代码进行动态分析来进行Tree-Shaking。
- 模块化系统:JavaScript中存在多种模块化系统,如CommonJS和ES6模块。这些模块化系统允许在运行时根据条件动态加载模块,使得静态分析变得更加困难。
- 动态引入:JavaScript中的动态引入机制,比如使用
import()
函数可以在运行时动态地引入模块。这使得编译器很难确定哪些代码是可被引入的,因此需要进行动态分析才能进行Tree-Shaking。 - 依赖关系:JavaScript中的代码之间存在复杂的依赖关系,这些依赖关系可能包括函数调用、对象属性的访问等。因此,在进行Tree-Shaking时需要考虑这些依赖关系,以确保不会错误地删除被调用的代码。
框架应该输出怎样的构建产物
不同类型的产物是为了满足不同的需求
为了能让用户通过
<script>
标签直接引用并使用,我们需要输出 IIFE 格式的资源,即立即调用的函数表达式为了让用户能够通过
<script type='module'>
引用并使用,我们需要输出 IIFE 格式的资源,即立即调用的函数表达式需要注意的是,ESM 格式的资源有两种:
- 用于浏览器的 esm-browser.js
- 用于打包工具的 esm-bundler.js
它们的区别在于对预定义常量
__DEV__
的处理:- 前者直接将
__DEV__
替换成字面量 true 或 false - 后者则将
__DEV__
常量替换为process.env.Node_ENV !== 'production'
语句
为了让实现服务端渲染的需求,我们需要输出 Common.js 格式的资源,因为当进行服务端渲染师,vue.js 的代码是在 Node.js 环境中运行的,而在 Node.js 环境中,资源的模块格式应该是 CommonJs
框架如何实现输出不同的产物
使用构建工具的打包配置功能
// rollup.config.js
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.iife.js',
format: 'iife'
},
{
file: 'dist/bundle.esm.js',
format: 'esm'
},
{
file: 'dist/bundle.cjs.js',
format: 'cjs'
}
]
};
在底层实现上,Rollup 会执行如下主要步骤来输出不同的产物:
- 解析:Rollup 会解析输入模块,找出模块之间的依赖关系,并构建一个依赖图。
- 打包:基于依赖图,Rollup 会将模块转换为可执行的 JavaScript 代码,并根据用户的配置生成相应的输出格式。
- 输出:根据用户配置的输出格式,Rollup 会将打包生成的代码写入指定的目标文件中。
特性开关
什么是特性开关
特性开关是一种软件开发中常用的技术实践,它允许开发人员在运行时通过配置文件、环境变量或其他方式动态地启用或禁用某个特定的功能或组件。特性开关可以在不重新部署或修改代码的情况下控制应用程序的行为。
使用特性开关的益处
- 对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中
- 为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性,而不用担心资源体积变大
- 同时,当框架升级时,我们也可以通过特性开关来支持遗留 API,这样新用户可以选择不使用遗留 API,从而使最终的打包资源变小
在vue3 中关闭选项式 API
define: {
__VUE_OPTIONS_API__: false // 关闭 Vue2 中的 options选项API
},
错误处理
- 框架的错误处理做得好坏直接决定用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担
- 框架需要为用户提供统一的错误处理接口,这样用户可以通过注册自定义的错误处理函数来处理全部的框架异常
在 vue3 中注册错误处理函数
import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = () => {
// 错误处理程序
}
良好的 TypeScript 类型支持
常见误区
- 使用 TS 编写框架和框架对 TS 类型支持友好是两件完全不同的事情
- 有时候为了让框架提供更加友好的类型支持,甚至要话费比实现框架功能本身更多的时间和精力
例子
优化前,返回值类型丢失
function foo(val:any) {
return val
}

优化后,能够推导出返回值的类型
function foo<T extends any>(val:T):T {
return val
}
