一个优秀的框架设计一定要考虑这些要素

浅谈框架设计的核心要素

引言

当今,Vue.js、React等优秀开源框架在Web前端领域已经红了半边天,熟练掌握一门框架已然成为了每一名Web前端领域从业者必备的技术素养。而在学习框架的过程之中,你有没有想过这个框架是如何设计的?它为什么要这么设计?深思起来,要想设计一个优秀的框架要比想象中的复杂许多,并不是说把相关的功能全部开发完成就大功告成了,“能用”只是最基本的要求,“好用”才具备核心竞争力!本文主要以Vue.js为例,围绕设计一个优秀的框架谈一谈要考虑到的方方面面。

提升用户的开发体验

是否拥有良好的开发体验是衡量一个框架是否足够优秀的重要指标之一,是一个框架能否迅速推广、普及的基石。就如我们日常生活中,那些真正好用的玩意向来不缺拥趸(dun)者,而另一些反人类的设计往往招来人们不禁的恶意。

拿Vue.js举一个小例子:

carbon (17).png

当我们创建一个Vue.js应用实例并试图将其挂载到一个不存在的DOM节点not-exist上时,就会受到一条来自Vue.js的警告信息,如下:

carbon (18).png

这条信息清晰地告诉我们挂载失败了,并说明了失败的原因,从而让我们能够据此清晰快速地定位问题位置并纠正。试想一下,如果Vue.js内部不对错误信息做任何的处理,那么我们很可能得到的是JavaScript层面的错误信息,如下:

carbon (19).png

这是一种多么令人抓狂的情景,尤其会对新入门的没有什么开发经验的程序员预备役造成毁灭性打击,就好比下载一个VScode而不允许使用插件,那我……还下载它干嘛???

除了提供必要的警告信息外,还有许多能够有效提升用户体验的方法,比如在某些条件下直观化RefImpl等响应式数据类型的输出,在这里就不细说了。总之,框架的设计要尽量地降低初学者的学习成本,那些反人类的设计大可不必。

控制框架代码的体积

框架的大小也是衡量一个框架是否优秀的标准之一。毕竟,在实现同样功能的情况下,当然是所用的代码量越少体积越小,最后浏览器加载资源的耗时才越少。

那么,如何减小项目打包体积呢?我想,实际开发过项目的人应该对一个项目分为开发版本和生产版本、项目依赖分为开发依赖和生产依赖有所了解,这就是一种通用的尽可能减小项目代码体积的手段。譬如,在项目开发中,清晰精准的报错提示是必不可少的,但是到了项目生产部署中,报错提示相关的代码就可以从项目之中剔除掉以减小代码打包体积,毕竟一个项目的上线至少经过了研发人员最基本的测试,不会有人把开发时报错停止运行的代码还部署上线吧?

在Vue.js中,使用的构建工具是rollup.js,请看下列Vue.js源码:

carbon (22).png

这是一个Vue.js的报错提示,里面的__DEV__常量实际上就是通过rollup.js的插件配制来预定义的:当项目处于开发环境下,值为true,打印报错信息;当项目处于生产环境下,值为false,则不打印报错信息。框架正是通过这些不起眼的配置来一步步减小项目正式打包部署时的代码体积。

框架要做到良好的Tree-Shaking

这里的Tree-Shaking指的是消除那些永远不会被执行的代码,即排除dead code(有垃圾回收机制那味儿了)。至于哪些代码是不会被执行的,那大可不必在此细说。我就说说一个比较特殊的情况:在Vue.js中,凡是不会产生副作用的统统Tree-Shaking,Vue.js才不会管这代码想干嘛呢!(副作用:当对一个函数的调用会对外部产生影响,如修改了全局变量,就称该函数有副作用)

如下代码:

carbon (21).png

虽然在input.js文件中导入并调用了函数foo,但是在Vue.js的正式打包之中,这一部分输出内容为空,因为foo函数并没有副作用,就不会被包含在项目代码之中。

有眼尖的同学可能就要问了,/*#__PURE__*/有什么?其实这就相当于一个标签,其作用就是向打包工具rollup.js担保:对于foo函数的调用绝对不会产生副作用,你就放心快乐的Tree-Shaking吧。为什么需要这样“声明”呢?你要知道,JavaScript是一门动态语言,它想静态分析哪些代码是dead code难度很大,而如果你对Vue.js响应式原理有所了解的话,你就知道如果obj是一个经过proxy代理的对象,那么foo函数对obj对象的读取会触发代理对象的get,而在get之中,是极有可能产生副作用的,如触发track函数

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

一个优秀的框架应该根据使用场景的不同而选择与之最符合的产物形式进行输出,以需求为衡量因素大致可分为以下几类

使用<script>标签引入并直接使用

示例代码如下:

carbon (23).png

在这里,Vue.js输出的是一种IIFE格式的资源。(IIFE的全称为 Immediately Invoked Function Expression,,即“立即调用的函数表达式”,在《你不知道的JavaScript 上卷》中也有介绍)实际上,vue,global.js就是一种IIFE格式的资源,当使用<strict>标签直接引入该文件后,全局变量vue就是可用的了。

使用<script type="module">直接引入ESM格式的资源

示例代码如下:

carbon (24).png

以前是不支持的,但是随着技术的发展以及主流浏览器版本的更新迭代,现如今对原生ESM的支持都还不错。当然,为了能够正常输出ESM格式的资源,rollup.js的输出格式需要配置为:format: 'esm'

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

示例代码如下:

carbon (25).png

服务端渲染中,这种引入资源方式的支持是有必要的。毕竟,在服务端渲染之中,Vue.js的代码已经不像前文一样运行在浏览器环境,而是运行在Node.js环境之中,此时资源的模块格式应为CommonJS,简称cjs。配套的,为了能够正常输入cjs模块的资源,需要修改rollup.js的配置为format: 'cjs'

特性开关

特性开关的存在为框架设计带来了灵活性。用户可以通过特性开关来设置关闭项目中不需要的特性,从而利用Tree-Shaking机制减小最终代码体积;也可以利用特性开关任意为框架添加新的特性并选择是否使用。

其中,我认为最具有标志性的可能就是Vue.js 3中选项式API与组合式API共存,如果你只想使用组合式API,那么完全可以利用特性开关关闭选项式API,这样Vue.js 3中与选项API相关的这部分代码就不会包含在最终的代码中,反之亦然。

错误处理

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

假设框架不具备错误处理机制,那用户的使用体验就会非常不好。此时,假如用户提供的回调函数在执行时出错了,那么用户就需要自己执行try...catch,一两个还好,但如果有几十上百个类似的函数需要处理呢,难道还在使用的时候逐一添加错误处理程序?那我还不如转行去学生物养鱼呢,来受这罪干嘛呢。(我就是生物专业的,上学期还在野外实习抓蜻蜓、抓螃蟹,感觉学习计算机相关知识的时间完全不够用啊,泪目)

幸好,如Vue.js这样的框架提供了错误处理机制,基本原理如下:

carbon (26).png

关于错误处理更多的细节可以在Vue.js的源码中搜索callWithErrorHandling函数

良好的TypreScript类型支持

现如今,TypeScript站在巨人JavaScript的肩膀上凭着更加丰富的语言特性及功能获得了越来越多开发者以及开发团队的青睐,GitHub上曾用JavaScript开发的许多明星项目都加入了用TypeScript重写的浪潮,对TS类型的支持是否完善也渐渐成为评价一个框架是否优秀的重要指标之一。

那如何衡量一个框架对TS类型支持的水平呢?如果你觉得只要使用了TS编写框架就代表对TS类型支持友好,那就大错特错了。在编写大型框架时,想要做到完善的TS类型支持很不容易,相比JS而言TS要消耗大量精力来做类型推导,这才是TS类型支持的关键所在。

在我看来,一个不做类型推导的TS文件仅仅是为一个JS文件换了个后缀而已,请JS少来碰瓷TS,谢谢!

3c4a9332946018edf53f450869c0ca58.jpeg

声明

  • 本文属于读书笔记一类,是作者在拜读 霍春阳 大佬的新作《Vue.js设计与实现》途中,以书中内容为蓝本,辅以个人微末的道行“填写”完成,推荐购书阅读,定有收获
  • 欢迎大佬斧正
  • 日更

Guess you like

Origin juejin.im/post/7068661226296836133