第2章-框架设计的核心要素

114 阅读5分钟

2.1 提升用户的开发体验

框架设计和开发过程中,提供有好的警告信息至关重要。

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

当我们创建一个Vue应用并试图将其挂载到一个不存在的DOM节点时,就会收到一条警告信息

【vue warn】: failed to mount target selector '#not-exist' return null

2.2 控制框架代码的体积

如果我们去看Vue.js的源码,就会发现每一个warn函数的调用都会配合_DEV_常量的检查,例如:

if(_DEV_ && !res) {
  warn(`Failed to mount app:mount target selcetor '${container}' return null.`)
}

可以看到,打印警告信息的前期是:_DEV_常量一定为true,这里的_DEV_常量就是达到目的的关键。

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

Vue.js在输出资源的时候,会输出两个版本,其中一个用于开发环境,如vue.global.js,另一个用于生产环境

,如vue.global.prod.js,通过文件名来区分。

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

简单地说,Tree-Sharking指的就是消除那些永远不会被执行的代码。

想要实现Tree-Shaking,必须满足一个条件,即模块必须是ESM(ES Module),因为Tree-Shaking依赖ESM的静态结构,我们以rollup.js为例看看Tree-Shaking如何工作,其目录结构如下:

| ---- demo

| ---- -------- package.json

| ---- -------- input.js

| ---- -------- utils.js

首先安装rollup.js:

npm i rollup -D

下面是input.js和utils.js文件的内容:

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

代码很简单,我们在utils.js文件中定义并导出两个函数,分别是foo函数和bar函数,然后在input.js中导入了foo函数并执行。注意,我们并没有导入bar函数。

npx rollup input.js -f esm -o bundle.js

这命令的意思是,以input.js文件为入口,输出ESM,输出的文件叫做bundle.js。命令执行成功后,我们打开bundle.js来查看一下它的内容:

// bundle.js
function foo(obj) {
  obj && obj.foo
}
foo()

可以看到,其中并不包含bar函数,这说明Tree-Shaking起了作用。由于我们没有是有bar函数,所以这段dead code被删除了。但是仔细发现,foo函数的执行也并没有产生什么意义,仅仅读取了对象的值,所以似乎也没什么作用,那么rollup.js并没有把他删除呢。这就涉及Tree-Shaking中第二个关键点---副作用。如果一个函数传递函数产生副作用了,那么就不能被删除。

因为静态地分析JavaScript代码很困难,所以rollup.js这类工具都会提供一个机制,让我们明确地告诉rollup.js,这段代码没问题,不会产生副作用,你可以移除它。下面修改input.js:

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

注释代码/#PURE/,其作用就是告诉rollup.js,对于foo函数的调用不会产生副作用,你可以放心地对其进行Tree-Shaking,此时再次执行构建命令查看bundle.js文件,就会发生他的内容是空的,这说明Tree-Shaking成功了

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

<script src="/path/to/vue.js"></script>
<script>
  const { createApp } = Vue
</script>

vue.global.js文件就是IIFE形式的资源

var Vue = (function () {
  // ...
  export.createApp = createApp
  // ...
  return exports
})({})

除了使用script标签引用IIFE格式的资源外,还可以直接引入ESM格式的资源,Vue3可以直接使用

为了输出ESM格式的资源,rollup.js的输出格式需要配置为:format: "esm"

// rollup.config.js
const config = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'iife'
  }
}
export default config

2.5 特性开关

  • 对于用户关闭的特性,我们可以利用Tree-Shaking机制让其不包括含在最终的资源中。
  • 未框架增加新的特性而不担心资源体积变大。可以兼容遗留的API。
{
  __FEATURE_OPTIONS_API__: isBunderESMBuild ?  `_VUE_OPTIONS_API__` : true
}

在Vue.js3的源码中搜索:

if(__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}
// webpack.DefinePlugin
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,
  __VUE_PROD_DEVTOOLS__: false
})

2.6、错误处理

错误处理事开发框架过程中非常重要的环节。框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担。

为了大家更加直观地感受错误处理的重要性,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下:

export default {
  foo(fn) {
    fn && fn()
  }
}

该模块导出一个对象,其中foo属性是一个函数,接收一个回调函数作为参数,调用foo函数时会执行该回调函数,在用户侧使用时:

import utils from 'utils.js'
utils.foo(() => {
  // ...
})

大家思考一下,如果用户提供的回调函数在执行的时候出错了,怎么办?此时两个办法,第一个办法是让用户自行处理,这需要用户自己执行try...catch:

import utils from 'utils.js'
utils.foo(() => {
  try {
    // ...
  }catch(e) {
    // ...
  }
})

但是这会增加用户的负担。需要在多个类似的函数中加异常try...catch处理

第二个办法是我们代替用户统一处理错误,如下代码所示:

export default {
  foo(fn) {
    try {
        fn && fn()
    }catch(e){/**/}
  },
  bar(fn) {
    try {
        fn && fn()
    }catch(e){/**/}
  }
}

在每个函数内都增加try...catch代码块,实际上,我们可以进一步将错误处理程序封装为一个函数,假设叫它callWicthErrorHandling:

export default {
  foo(fn) {
    callWicthErrorHandling(fn)
  },
  bar(fn) {
    callWicthErrorHandling(fn)
  }
}
function callWicthErrorHandling(fn) {
    try {
        fn && fn()
    }catch(e){
      console.log(e)
    }
}

提供统一的额错误处理接口:

let handleError = null
export default {
  foo(fn) {
    callWicthErrorHandling(fn)
  },
  registerErrorHandler(fn) {
    handlerError = fn
  }
}
function callWicthErrorHandling(fn) {
    try {
        fn && fn()
    }catch(e) {
      handlerError(e)
    }
} 

我们提供了registerErrorHandler函数,用户可以使用它注册错误处理程序,然后在callWicthErrorHandling函数内部捕获错误后,把错误传递给用户注册的错误程序。

import utils form 'utils.js'
utils.registerErrorHandler((e) => {
  console.log(e)
})
utils.foo(() => {/*...*/})
utils.bar(() => {/*...*/})

在Vue.js错误处理的原理,你可以在源码中搜索到callWithErrorHandling函数。另外,在Vue.js中,我们也可以注册统一的错误处理函数:

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

2.7、良好的TypeScript类型支持

TypeScript是由微软开源的编程语言,简称TS,它是JavaScript的超集,能够为JavaScript提供类型支持。