深圳幻海软件技术有限公司 欢迎您!

Vue.js设计与实现-框架设计的核心要素

2023-02-28

1、写在前面在前面文章中,了解到框架的设计是一种权衡的艺术,是需要宏观把握各方面因素从而尽可能完善。其实,框架设计比想象中的复杂,并不是只有功能实现就完事,需要考虑如何提供给使用者构建产物、如何让其快速定位问题,需要有良好的使用交互体验。2、提升用户体验衡量框架是否优秀的重要指标,就是看它的开发体验

1、写在前面

在前面文章中,了解到框架的设计是一种权衡的艺术,是需要宏观把握各方面因素从而尽可能完善。其实,框架设计比想象中的复杂,并不是只有功能实现就完事,需要考虑如何提供给使用者构建产物、如何让其快速定位问题,需要有良好的使用交互体验。

2、提升用户体验

衡量框架是否优秀的重要指标,就是看它的开发体验是否达到预期,需要为用户提供良好直观的交互指引和错误警示,帮助用户快速理解和错误定位。

3、控制代码体积

在进行项目开发中,在实现相同功能代码越简洁、越少,在进行打包压缩体积后,在浏览器渲染加载资源的时间更少,就能够换取提升用户的使用体验。在前面提到要给用户完善丰富的警告信息,那么也就意味着编写更多的代码,这就违背了控制代码体积的初衷。

对此,Vue3框架在设计源码时,采用_DEV_进行判断是否为开发环境,从而让一些代码只在开发时进行调用,也会构建对应的开发资源。在Vue.js中采用的打包工具是rollup,_DEV_ 是使用插件进行预定义的,在进行资源输出时用于区分生产环境还是开发环境。

if(_DEV_ && !res){
  warn(`Failed to mount app: mount target selector "${container}" returned null.`)
}
  • 1.
  • 2.
  • 3.

当在构建生产环境的资源时,_DEV_的值为false,这时候上面的这段代码将永远不会进行执行,这样就成为了dead code,在构建资源时就会被打包工具所移除(Tree-shaking)。

4、Tree-shaking

事实上,在我们实际项目开发中,有些Vue.js内置的组件压根没有用不上,那么在构建生产环境的资源时就不能让它出现,也就是Tree-shaking。Tree-shaking概念是由rollup推广普及的,指的是消除那些永远不会被执行的代码,即排除dead code。

实现Tree-shaking功能,前提是依赖于文件的模块必须是ESM(ES Module),即文件的静态结构是ESM。

在rollup中Tree-shaking是如何工作的呢?

demo文件结构:

|-- home
|  |__ package.json
|  |__ ping.js
|  |__ chuan.js
  • 1.
  • 2.
  • 3.
  • 4.

安装rollup:

yarn add rollup -D
  • 1.

ping.js和chuan.js文件内容:

// ping.js
export function foo(obj){
    obj && obj.foo
}
export function bar(obj){
    obj && obj.bar
}
export function fun(obj){
    obj && obj.fun
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

注意,我们在chuan.js文件中并没有导入ping.js中的bar函数。

import {foo, fun} from "./ping"
// 告知rollup这是个纯函数,不会产生任何副作用,可以放心tree-shaking
/*#__PURE__*/  foo()
fun()
  • 1.
  • 2.
  • 3.
  • 4.

使用命令进行构建,以chuan.js文件作为打包的入口,输出ESM的文件模块,打包后的输出文件是bundle.js。

npx roolup chuan.js -f esm -o bundle.js
  • 1.

在执行命令打包成功过后,我们可以输出的bundle.js文件中只包含fun函数的相关代码。而bar函数因为入口文件没有导入,不会打包进去,而foo函数前面加了/*#__PURE__*/代码,即告知rollup这是个纯函数,不会产生任何副作用,可以放心tree-shaking。这样,打包后的文件就只剩fun函数了。

//bundle.js
function fun(obj){
    obj && obj.fun;
}
fun();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

什么是tree-shaking的副作用呢?

经常提到的副作用其实就是:当调用函数时,会对外部产生影响,在rollup中如果函数调用时产生了副作用,就不能将其移除,因为会潜在的影响其他代码。

打包工具是怎么知道哪些代码可以放心移除呢?

rollup提供/*#__PURE__*/代码,可以告知rollup这是个纯函数,不会产生任何副作用,可以放心tree-shaking。上面代码片段中,在foo函数前添加/*#__PURE__*/,就可以做个标记告知rollup可以对其tree-shaking,打包后的bundle.js文件也对应将代码进行移除。其实/*#__PURE__*/可以用于任何代码,不单单是函数。

在Vue.js框架的源码中,我们可以发现大量的/*#__PURE__*/,但其实这并不会对使用者产生心智负担,因为通常产生副作用的代码都是模块内顶级调用的,而没有被顶级调用的代码是不会产生副作用的。

什么是顶级调用?

fun();//顶级调用
function fun(obj){
    obj && obj.fun;
}
function foo(){
  fun();//函数中调用
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

5、构建产物

Vue.js会不同的环境输出不同的包,vue.global.js用于开发环境,vue.global.prod.js用于生产环境,在构建产物时,会根据不同的需求输出不同的构建产物。

<body>
  <script src="./vue.js"></script>
  <script>
  const {createApp} = Vue;
  //...
  </script>
</body>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

首先用户直接可以在html页面中使用script引入框架使用,根据不同需求构建不同产物,需要输出一种IIFE(立即调用的函数表达式)格式的资源。

const Vue = (function(){
  //...
  exports.createApp = createApp;
  //...
  return exports
})()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

这样,在使用<script>标签可以直接引入vue.global.js文件后,可以在html文件中进行全局使用了。在使用打包工具rollup时,我们需要配置format:"iife"来允许输出立即执行函数。

//rollup.config.js
const config = {
  input:"input.js",
  output:{
    file:"output.js",
    format:"iife"//指定模块形式
  }
}
export default config
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

时至今日,主流浏览器不仅支持在<script>标签引用IIFE格式的资源,还能够直接引入ESM格式的资源,通过rollup配置format:"esm"即可输出vue.esm-browser.js文件。

<script type="module" src="./vue.esm-browser.js"></script>
  • 1.

_DEV_根据不同的值构建不同的产物,当_DEV_设置为true时,可用于生产环境从而被Tree-Shaking移除代码。但是,当我们构建提供给打包工具的ESM格式的资源时,不能直接设置_DEV_的值,要使用process.env.NODE_ENV1=="production"进行替换。在带有-bundler后缀的资源文件会变成:

if(process.env.NODE_ENV1=="production"){
  warn(`useCssModule() is not supported in the global build.`)
}
  • 1.
  • 2.
  • 3.

6、特性开关

在设计框架时会为用户提供诸多特性功能,用户可以根据自己的需要开启或关闭对应的特性。

  • 对于用户关闭的特性,可以使用Tree-Shaking机制将代码从资源中清除。
  • 开关特性可以提升框架的灵活性,可以通过特性开关在框架任意添加新特性,在框架升级时可以开启使用上版本API的支持,从而减少代码打包资源的体积。

在Vue.js3框架设计中,为了减少框架升级对于之前版本的兼容性,为用户提供了__VUE_OPTIONS_API__开关来判断是否要开启对options API的兼容。

7、错误处理

错误处理是框架设计最重要的环节,可以决定用户应用程序的健壮性,降低开发者在处理报错时的心智负担。在utils.js的模块可以导出一个包含foo函数,接收一个回调函数作为参数,调用foo函数时会执行回调函数。

//utils.js
export default {
  foo(fn){
    fn && fn();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

使用进行开发时,直接导入utils.js文件调用foo函数。

import utils from "utils.js";
utils.foo(()=>{
  //...
})
  • 1.
  • 2.
  • 3.
  • 4.

如果用户提供了回调函数在执行时报错,那么就需要用户不断地使用try...catch...捕获和抛出错误,毫无疑问这会增加用户的心智负担。如果Vue.js框架已经在内部封装了许多try...catch...的错误处理函数callWithErrorHandling,那么用户在使用的时候代码就简洁很多,而且能够为用户提供统一处理的接口。

//utils.js
let handlerError = null;
export default {
  foo(fn){
    callWithErrorHandling(fn)
  },
  //用户可以调用该函数注册统一的错误处理函数
  registerErrorHandling(fn){
    handlError = fn
  }
}
function registerErrorHandling(fn){
  try{
    fn && fn()
  }catch(e){
    //将捕获到的错误抛出给用户进行处理
    handlerError(e)
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

此外,Vue.js提供了registerErrorHandler函数注册错误处理程序,callWithErrorHandling函数捕获错误后会抛出给用户注册的错误处理程序。

import utils from "utils.js";
//注册错误处理程序
utils.registerErrorHandler(e=>{
  console.log(e)
})
utils.foo(()=>{//...})
utils.bar(()=>{//...})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

此时用户的代码非常简洁健壮,错误的处理能力就完全由用户控制,也可以在Vue.js文件中注册统一的错误处理函数。

import App from "App.vue"
const app = createApp(App);
app.config.errHandler = ()=>{
  //错误处理程序
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

8、写在最后

在本文中主要介绍了开发体验、tree-shaking以及错误处理等对框架设计的重要性,提供良好的警告信息可以有助于开发者快速定位问题,框架提供错误处理API能够提升用户应用程序的健壮性。tree-shaking可以用于打包文件的时候移除dead code,根据不同的环境构建不同的资源产物,从而减少代码打包的体积。此外还给用户提供了各种特性开关,__VUE_OPTIONS_API__可以用于设置Vue.js3对于option API的兼容性。