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的兼容性。