【Vue.js设计与实现】第二章:框架设计的核心要素

137 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

2.1 提升用户的开发体验

衡量一个框架是否足够优秀的指标之一就是 看它的开发体验如何。

在框架设计和开发过程中,提供友好地警告信息至关重要。始终提供友好地警告信息可以帮助用户快速定位问题,节省用户的时间。

例如Vue中随处可见的 warn() 函数

warn('Failed to mount app: XXXXXX')

在Vue3源码中,initCustomerFormatter 函数用来在开发环境下初始化自定义formatter的。

2.2 控制框架代码的体积

框架的大小也是衡量框架的指标之一

这里就出现了一个和2.1相悖的点了。首先,框架要提供警告信息,而且是尽可能完善的警告信息,这也就意味着我们要编写更多代码去提供更加完善的警告信息,这就和这一条控制代码体积相悖了。那Vue3是如何解决的呢?

在Vue3的源码中,可以发现每一个 Warn 函数的调用都会配合 __DEV__ 常量的检查,而这个常量 __DEV__ 就是关键。

if(__DEV__ && !res){
    warn('Failed to mount app:XXXXX')
}

Vue使用rollup.js进行项目构建,这里的 __DEV__ 实际上是通过rollup的插件配置来预定义的,功能上类似webpack的DefinePlugin插件。

在开发环境的时候,__DEV__会设置为 true,而在生产环境的时候则会设置为 false。 这样,上面这段代码在生产环境中永远不会被执行,这段永远不会被执行的代码称为 dead code,它不会出现在最终产物中。

通过这种方式,Vue实现了在 开发环境中为用户提供良好地警告信息,同时不会增加生产环境代码的体积。

2.3 框架要做到良好的Tree-Shaking

Tree-Shaking指的就是消除那些永远不会被执行的代码,即排除dead code。

rollup.js和webpack都支持Tree-Shaking,Tree-Shaking依赖ESM的静态结构,因此,想要实现Tree-Shaking必须要满足一个条件: 模块必须是ESM(ESModule)。

2.3.1 函数调用副作用

现在有2个函数,如下

export function foo(obj){
    obj && obj.foo
}
export function bar(obj){
    obj && obj.bar
}
// 调用foo,而不调用bar
foo()

上面这段代码打包后,经过Tree-Shaking就会变成下面这段代码

function foo(obj){
    obj && obj.foo
}
foo()

可见,这其中的bar函数被作为dead code删除了。但是我们再看一下foo函数,发现它的执行并没有什么意义,即使把这段代码删掉也不会对我们的程序有什么影响,为什么它没有被当作dead code删除掉呢?

这就和副作用有关了, 一个函数调用如果会产生副作用,那么就不可以将其移除。 副作用的意思就是,当调用函数时会对外部产生影响。比如说上面的obj对象,假如它是一个代理对象,那么当我们访问其foo属性时,就会触发代理对象的get夹子,而get夹子里会执行什么,对整个应用造成什么影响只有在真正运行代码的时候才能知道。

使用工具来分析dead code很困难,但是作为编写代码的我们,对某一段代码是否需要被打包,是否有用应该是了然于胸的,所以我们可以通过下面这种方式来告诉rollup,某一段代码可以放心对其进行Tree-Shaking

import {foo} from './utils'
/*#__PURE__*/ foo()

通常产生副作用的代码都是模块内函数的顶级调用。

// 顶级调用
foo()
function bar(){
  foo() // 函数内调用
}

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

不同类型的产物一定有对应的需求背景

2.4.1 可以在HTML页面中使用 <script> 直接引入框架并使用

要实现这个需求,我们要输出一种叫做 IIEF 格式的资源,即立即执行函数。

实际上 vue.global.js就是IIFE形式的。

// vue.global.js
var Vue = (function(exports){
    // ******
}({}))

这样,我们在 <script> 标签中引入vue.global.js文件后,全局变量Vue就是可用的了。

在rollup中可以通过以下配置来实现输出IIFE格式的资源

// follup.config.js
const config = {
    input:'input.js',
    output:{
        file:'output.js',
        format:'iife' // 指定模块形式
    }
}

2.4.2 在Node.js中通过 require 语句引入资源

进行服务端渲染时,Vue的代码在Node环境中运行,此时资源的模块格式应该是CommonJS(简称:cjs),这个时候,我们只需要将 format 属性的值设为 cjs 即可。

2.5 特性开关

特性开关可以让我们自由选择使用框架的哪些特性,其实现原理和上面的常量 __DEV__ 一样,本质上是利用rollup的预定义常量插件来实现的。

比如说,在Vue3中有这样一个特性开关 __VUE_OPTIONS_API__ ,打开它,就可以在Vue3中兼容Vue2的OptionsAPI,如果明确不用兼容Vue2的OptionsAPI的话可以将这个特性开关设为 false,这样在打包Vue的时候,这部分代码就不会在最终资源里。

2.6 错误处理

框架错误处理机制的好坏直接决定了用户应用程序的健壮性。

为用户提供统一的的错误处理接口,如下

// utils.js
let handleErroe = null
export default {
    foo(fn){
        callWithErrorHandling(fn)
    },
    // 用户可以调用该函数注册统一的错误处理函数
    registerErrorHandler(fn){
        handleError = fn
    }
}

function callWithErrorHandling(fn){
    try{
        fn && fn()
    } catch (e) {
        // 将捕获到的错误传递给用户自定义的错误处理程序
        handleError(e)
    }
}

上面的代码,用户可以使用 registerErrorHandler 注册错误处理程序,然后在 callWithErrorHandling 函数内部捕获错误后,把错误传递给用户注册的错误处理程序。

import utils from 'utils.js'
// 注册错误处理程序
utils.registerErrorHandler((e) => console.log(e))
utils.foo(()=>{/* .... */})

像上面这样,错误处理的能力将完全交由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报给监控系统。

上面也是Vue的错误处理的原理,可以在源码中搜索 callWithErrorHandling 函数来查看。

在Vue中我们也可以通过下面这种方式注册统一的错误处理函数。

import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = () => {
    //  错误处理程序
}

2.7 良好的TypeScript类型支持

TS作为JS的超集,能够为JS提供类型支持,它有着 代码即文档 的好处。除此之外还有编辑器自动提示,一定程度上避免低级bug、代码可维护性更强等优点。

一个常见的误区: 使用TS编写框架 = 对TS类型支持友好。 这其实是两件完全不同的事情。

在Vue的源码中,runtime-core/src/apiDefineComponent.ts 文件,整个文件近200行,但是真正在浏览器中运行的代码只有3行,这些都是在为类型支持服务。

2.8 总结

一个框架的核心要素:

  • 良好的用户开发体验:完善警告机制
  • 框架代码的体积
  • 良好的Tree-Shaking
  • 输出不同类型的构建产物
  • 特性开关
  • 错误处理机制
  • TS类型支持