《Vue.js 设计与实现》—— 02 框架设计核心要素

这篇具有很好参考价值的文章主要介绍了《Vue.js 设计与实现》—— 02 框架设计核心要素。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

框架设计并非仅仅实现功能那么简单,里面有很多学问。例如:

  • 框架应该给用户提供哪些构建产物?产物的模块格式如何?

  • 当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?

  • 开发版本和生产版本的构建有何区别?

  • 热更新(hot module replacement,HMR)需要框架层面的支持,是否也应该考虑?

  • 当框架提供了多个功能,而用户只需要其中几个功能时,用户能否选择关闭其他功能从而减少最终资源的打包体积?

1. 提升开发体验

衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何,以 Vue.js 3 为例:

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

当创建一个组件并试图将其挂载到一个不存在的 DOM 节点时,就会收到一条警告信息:

[Vue warn]: Failed to mount app: mount target selector "#not-exist" returned null.

这条信息让我们能够清晰且快速地定位问题。如果 Vue.js 内部不做任何处理,那么很可能得到的是 JavaScript 层面的错误信息,如 Uncaught TypeError: Cannot read property 'xxx' of null,而根据此信息很难知道问题所在。

因此,在框架设计和开发过程中,提供友好的警告信息至关重要。始终提供友好的警告信息不仅能够帮助用户快速定位问题,节省用户的时间,还能够让框架收获良好的口碑,让用户认可框架的专业性。

在 Vue.js 的源码中,经常能够看到 warn 函数的调用,例如:

warn(`Failed to mount app: mount target selector "${container}" returned null.`)

除了提供必要的警告信息外,还有很多其他方面可以作为切入口,进一步提升用户的开发体验。例如,在 Vue.js 3 中,当在控制台打印一个 ref 数据时:

const count = ref(0)
console.log(count)

打印结果是一个 ref 对象,很不直观,但调用 count.value 后,得到的就是响应式对象的值,变得非常直观。

那么有没有办法在直接打印 count 时让输出的信息更友好呢?当然可以,浏览器允许我们编写自定义的 formatter,从而自定义输出形式。

在 Vue.js 3 的源码中,有一个名为 initCustomFormatter 的函数,用来在开发环境下初始化自定义 formatter。

以 Chrome 为例,打开 DevTools 的设置,然后勾选 “Console” -> “Enable custom formatters” 选项,如下:

《Vue.js 设计与实现》—— 02 框架设计核心要素

然后刷新浏览器并查看控制台,会发现输出内容变得非常直观,如下:

《Vue.js 设计与实现》—— 02 框架设计核心要素

2. 控制代码体积

框架的大小也是衡量框架的标准之一。在实现同样功能的情况下,代码越少越好,这样体积就会越小,最后浏览器加载资源的时间也就越少。前面说到,框架提供越完善的警告信息越好,但这意味着要编写更多的代码,那么如何在这个基础上实现代码体积的控制呢?

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

if (__DEV__ && !res) { // 打印警告信息的前提时 __DEV__ 为 true
  warn(
    `Failed to mount app: mount target selector "${container}" returned null.`
  )
}

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

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

当 Vue.js 构建用于开发环境的资源时,会把 __DEV__ 常量设置为 true;当构建生产环境的资源时,会把 __DEV__ 常量设置为 false

因为生产环境下判断条件始终为假,这段永远不会执行的代码称为 dead code,它不会出现在最终产物中,在构建资源时就会被移除。这样就做到了在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积。

3. 良好的 Tree-Shaking

仅仅通过 __dev__ 变量控制代码量是远远不够的。还以 Vue.js 为例,其内建了很多组件,例如 <Transition> 组件,如果项目中没有用到该组件,其对应的代码就不需要也不应该包含在最终的构建资源中。那么如何做到这一点呢?答案就是 Tree-Shaking

在前端领域,这个概念是由 rollup.js 普及的。简单地说,Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是排除 dead code,现在无论是 rollup.js 还是webpack,都支持 Tree-Shaking

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

- demo
	- package.json
	- input.js
	- utils.js

首先安装 rollup.js:

yarn add rollup -D # 或 npm install rollup -D

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

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

// utils.js
export function foo(obj) {
    obj && obj.foo
}
export function bar(obj) { // bar 函数未被使用
    obj && obj.bar
}

接着,执行如下命令进行构建:

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

构建后,输出的 bundle.js 的内容为:

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

这说明 Tree-Shaking 起了作用,我们并没有使用 bar 函数,因此它作为 dead code 被删除了。但是仔细观察会发现,foo 函数的执行也没有什么意义,仅仅是读取了对象的值,所以它的执行似乎没什么必要。既然把这段代码删了也不会对程序产生影响,那为什么 rollup.js 不把这段代码也作为 dead code 移除呢?

这涉及 Tree-Shaking 中的第二个关键点 —— 副作用。如果一个函数调用会产生副作用,那么就不能将其移除

简单地说,副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。

但是,上面的代码只读取对象的值,怎么会产生副作用呢?其实是有可能的,如果 obj 对象是一个通过 Proxy 创建的代理对象,那么读取对象属性时,就会触发代理对象的 get 夹子(trap),在 get 夹子中是可能产生副作用的。至于到底会不会产生副作用,只有代码真正运行的时候才能知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度。

即然静态地分析代码很困难,所以像 rollup.js 这类工具都会提供一个机制,让我们手动明确地告诉 rollup.js 该段代码是一个纯函数,不会产生副作用,可以移除它。具体实现如下:

import {foo} from './utils'

/*#__PURE__*/ foo() // 前面的 __PURE__ 告知是一个纯函数,不会产生副作用,可以移除

此时再次执行构建命令并查看 bundle.js 文件,就会发现它的内容是空的。

因此,我们在编写框架的时候需要合理使用 /*#__PURE__* 注释。Vue.js 3 的源码里面大量使用了该注释。

那么,这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。

什么是顶级调用呢?如下:

foo() // 顶级调用

function bar() {
    foo() // 函数内调用 -- 没有副作用,除非 bar() 顶级调用
}

/*#__PURE__*/ 注释不仅仅作用于函数,它可以应用于任何语句上。该注释也不是只有 rollup.js 才能识别,webpack 以及压缩工具(如 terser)都能识别它。

4. 输出构建产物

前面说到 Vue.js 会为开发环境和生产环境输出不同的包,如 vue.global.js 用于开发环境,它包含必要的警告信息,而 vue.global.prod.js 用于生产环境,不包含警告信息。实际上,Vue.js 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物。

不同类型的产物一定有对应的需求背景,因此需要从需求讲起。首先我们希望用户可以直接在 HTML 页面中使用 <script> 标签引入框架并使用:

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

为了实现这个需求,需要输出一种叫作 IIFE 格式的资源。IIFE 的全称是 Immediately Invoked Function Expression,即“立即调用的函数表达式”,易于用 JavaScript 来表达:

(function () {
    // ...
})

如以上代码所示,这是一个立即执行的函数表达式。实际上,vue.global.js 文件就是 IIFE 形式的资源,它的代码结构如下所示:

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

这样当我们使用 <script> 标签直接引入 vue.global.js 文件后,全局变量 Vue 就是可用的了。在 rollup.js 中,可以通过配置 format: 'iife' 来输出这种形式的资源:

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

export default config

随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 的支持都不错,所以用户除了能够使用 <script> 标签引用 IIFE 格式的资源外,还可以直接引入 ESM 格式的资源,例如 Vue.js 3 还会输出 vue.esm-browser.js 文件,用户可以直接用 <script type="module"> 标签引入:

<script type="module" src="/path/to/vue.esm-browser.js"></script>

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

为什么 vue.esm-browser.js 文件中会有 -browser 字样?其实对于 ESM 格式的资源来说,Vue.js 还会输出一个 vue.esm-bundler.js 文件,其中 -browser 变成了 -bundler。为什么这么做呢?我们知道,无论是 rollup.js 还是 webpack,在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来代替 main 字段指向的资源。

可以打开 Vue.js 源码中的 packages/vue/package.json 文件看一下:

{
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
}

其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思是说,如果项目是使用webpack 构建的,那么你使用的 Vue.js 资源就是 vue.runtime.esm-bundler.js,也就是说,带有 -bundler 字样的 ESM 资源是给 rollup.js 或 webpack 等打包工具使用的,而带有 -browser 字样的 ESM 资源是直接给 <script type="module"> 使用的。它们之间有何区别?这就不得不提到上文中的 __DEV__ 常量。当构建用于 <script> 标签的 ESM 资源时,如果是用于开发环境,那么 __DEV__ 会设置为 true;如果是用于生产环境,那么 __DEV__ 常量会设置为 false,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,不能直接把 __DEV__ 设置为 truefalse,而要使用(process.env.NODE_ENV !== 'production')替换 __DEV__ 常量。例如下面的源码:

if (__DEV__) {
  warn(`useCssModule() is not supported in the global build.`)
}

在带有 -bundler 字样的资源中会变成:

if ((process.env.NODE_ENV !== 'production')) {
  warn(`useCssModule() is not supported in the global build.`)
}

这样做的好处是,用户可以通过 webpack 配置自行决定构建资源的目标环境,但是最终效果其实一样,这段代码也只会出现在开发环境中。用户除了可以直接使用 <script> 标签引入资源外,我们还希望用户可以在 Node.js 中通过 require 语句引用资源,例如:

const Vue = require('vue')

为什么会有这种需求呢?因为当进行服务端渲染时,Vue.js 的代码是在 Node.js 环境中运行的。在 Node.js 环境中,资源的模块格式应该是 CommonJS,简称 cjs。为了能够输出 cjs 模块的资源,可以通过修改 rollup.config.js 的配置 format: 'cjs' 来实现:

5. 特征开关

在设计框架时,框架会给用户提供诸多特性(或功能),例如提供 A、B、C 三个特性给用户,同时还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 truefalse 来代表开启或关闭对应的特性,这将会带来很多益处,如:

  • 对于用户关闭的特性,可以利用 Tree-Shaking 机制让其不打包在最终的资源中。
  • 该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性,而不用担心资源体积变大。
  • 当框架升级时,也可以通过特性开关来支持遗留 API,这样新用户可以选择不使用遗留 API,从而使最终打包的资源体积最小化。

那如何实现特性开关呢?其原理和前面提到的 __DEV__ 常量一样,本质上是利用 rollup.js 的预定义常量插件来实现。拿 Vue.js 3 源码中的一段 rollup.js 配置来说:

{
  __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
}

其中 __FEATURE_OPTIONS_API__ 类似于 __DEV__。在 Vue.js 3 的源码中搜索,可以找到很多类似于如下代码的判断分支:

// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

当 Vue.js 构建资源时,如果构建的资源是供打包工具使用的(即带有 -bundler 字样的资源),那么上面的代码在资源中会变成:

// support for 2.x options
if (__VUE_OPTIONS_API__) { // 这里不一样
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

其中 __VUE_OPTIONS_API__ 是一个特性开关,用户可以通过设置 __VUE_OPTIONS_API__ 预定义常量的值来控制是否要包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件来实现:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性
})

最后解释一下 __VUE_OPTIONS_API__ 开关有什么用。在 Vue.js 2 中,我们编写的组件叫作Options API;在 Vue.js 3 中,推荐使用 Composition API 来编写代码。为了兼容 Vue.js 2,在 Vue.js 3 中仍然可以使用 Options API 的方式编写代码。但是如果明确知道自己不会使用选项 API,用户就可以使用 __VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue.js 的这部分代码就不会包含在最终的资源中,从而减小资源体积。

6. 错误处理

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

假设我们开发了一个工具模块,代码如下:

// utils.js
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) {
        // ...
    }
})

但是这会增加用户的负担。如果 utils.js 提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。

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

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

事实上,可以进一步将错误处理程序封装在一个函数上,假设称为 callWithErrorHandling

// utils.js
export default {
    foo(fn) {
        callWithErrorHandling(fn)
    },
    bar(fn) {
        callWithErrorHandling(fn)
    },
    // ...
}

function callWithErrorHandling(fn) {
    try {
        fn && fn()
    } catch(e) {
        /* ... */
    } 
}

简洁还不是封装函数的主要目的,我们能为用户提供统一的错误处理接口,如:

// utils.js
let handleError = null

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

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

这样用户侧的代码就会非常简洁且健壮:

import utils from '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 = () => {
  // 错误处理程序
}

7. 良好的 TS 支持

TypeScript 是由微软开源的编程语言,简称 TS,它是 JavaScript 的超集,能够为 JavaScript 提供类型支持。使用 TS 的好处有很多,如代码即文档、编辑器自动提示、一定程度上能够避免低级 bug、代码的可维护性更强等。因此对 TS 类型的支持是否完善也成为评价一个框架的重要指标。

如何衡量一个框架对 TS 类型支持的水平呢?这里有一个常见的误区,很多人以为只要是使用 TS 编写框架,就等价于对 TS 类型支持友好,其实这两种完全不同。

举例来说。下面是使用 TS 编写的函数:

function foo(val: any) {
    return val
}

这个函数直接将参数作为返回值,这说明返回值的类型是由参数决定的,如果参数是 number 类型,那么返回值也是 number 类型。但是,假设有下面的代码:

const res = foo('str') // 参数为字符串类型,理论上 res 也为字符串类型,但是却推断成了 any 类型

为了达到理想状态,只需要对 foo 函数做简单的修改即可:

function foo<T extends any>(val: T): T {
    return val
}

const res = foo('str') // 这时就会将 res 推断为 "str" 字符串字面量了

通过这个例子可以认识到,使用 TS 编写代码与对 TS 类型支持友好是两件事。在编写大型框架时,想要做到完善的 TS 类型支持很不容易,可以查看 Vue.js 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在浏览器中运行的代码其实只有 3 行,但是全部的代码接近 200 行,其实这些代码都是在为类型支持服务。由此可见,框架想要做到完善的类型支持,需要付出相当大的努力。

更多文章可关注:GopherBlog、GopherBlog副站文章来源地址https://www.toymoban.com/news/detail-438051.html

到了这里,关于《Vue.js 设计与实现》—— 02 框架设计核心要素的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Vue.js设计与实现阅读-2

    上一篇我们了解到了 命令式和声明式的区别,前者关注过程,后者关注结果 了解了虚拟dom存在的意义,使找出差异这个过程消耗的性能最小化 学习了进行时、编译时、编译时+进行时的特点,初步知道了vue3是采用编译时+进行时的框架 2、1 提升用户体验 提供友好的警告信息

    2024年02月02日
    浏览(55)
  • Vue.js设计与实现阅读-3

    前言 前面一章我们了解了,开发体验是衡量一个框架的重要指标之一。提供友好的警告信息至关重要,但是越详细的警告信息,意味着框架体积越大。为了解决这一问题,可以利用 Tree-Shaking 机制,配合构建工具预定义常量,例如 __ DEV __ ,从而实现只在开发环境中打印警告

    2024年02月02日
    浏览(21)
  • vue.js毕业设计,基于vue.js前后端分离在线小说电子书阅读小程序系统设计与实现

    用户首次登陆系统需要注册一个用户作为账号,用户在登录平台后,可以进行平台的操作。主要模块包括以下几点: 登录功能:注册普通账号登录;登录后可以修改用户的基本信息,也可以退出。 资讯功能:后台录入资讯,在微信小程序在线电子书阅读系统的资讯模板展示

    2024年02月07日
    浏览(44)
  • 前端框架Vue-Vue核心

    目录 第1章 Vue核心 1.1 Vue简介 1.1.2 Vue的特点 1.1.3 与其它 JS 框架的关联 1.1.4 学习Vue之前要掌握的JavaScript基础知识 1.1.5 Vue官网 1.1.6 Vue周边库 1.3 模块语法 1.3.1 模板的理解 1.3.2 插值语法 1.3.3 指令语法 1.4 数据绑定 1.4.1. 单向数据绑定 1.4.2. 双向数据绑定 1.4.3 el与data的两种写法

    2024年02月13日
    浏览(58)
  • Vue.js的核心概念

    Vue.js 是一个流行的 JavaScript 框架,用于构建交互式的 web 界面。 它的核心概念包括: 1. 响应式系统 :Vue 使用响应式系统来确保 UI 与 underlying data 保持同步。当数据变化时,视图自动更新。 2. 组件系统 :Vue 应用由可复用的组件构成。每个组件都有自己的视图、业务逻辑和样

    2024年01月18日
    浏览(39)
  • 使用JavaScript和Vue.js框架开发的电子商务网站,实现商品展示和购物车功能

    引言: 随着互联网的快速发展和智能手机的普及,电子商务行业正迎来一个全新的时代。越来越多的消费者选择网上购物,而不再局限于传统的实体店。这种趋势不仅仅是改变了消费者的习惯购物,也给企业带来了巨大的商机。为了不断满足消费者的需求,电子商务网站需要

    2024年02月15日
    浏览(47)
  • 人工智能三个核心要素:算法、算力、数据

    人工智能(Artificial Intelligence,简称AI)是指计算机系统模拟、模仿和执行人类智能的能力。它是一门研究如何使计算机能够像人类一样思考、学习、推理和解决问题的科学和技术领域。 人工智能通过利用大量的数据和强大的计算能力,以及各种算法和技术,使计算机系统能

    2024年02月07日
    浏览(35)
  • Spring框架核心与设计思想

    我们一般所说的Spring指的是Spring Framework(Spring 框架),它是一个开源的框架,Spring支持广泛的应用场景,它可以让Java企业级的应用程序开发变得更简单,官方一点的回答:spring是J2EE应用程序框架,是轻量级的IoC和AOP的容器框架,主要是针对javaBean的生命周期进行管理的轻量级

    2023年04月15日
    浏览(33)
  • 扩展检测和响应:零信任安全的核心要素

    面对不断增长的攻击面,扩展和增强威胁检测和响应能力是XDR在安全功效方面的主要结果。这一成果不仅有助于全面保护,而且有助于更好地实施零信任安全。 默认情况下,这种方法不信任任何用户或任何设备,只允许访问需要的资源。为了更好地了解零信任和 XDR 的共同点

    2024年02月04日
    浏览(35)
  • 软信天成:数据治理三大核心要素是什么?

    近年来,信息技术的快速发展和深入应用让数据获得了前所未有的增长,著名研究机构IDC预测:到2025年,全球数据预计增长至175ZB。随着数据价值的日益凸显,无数企业开始布局数字化战略转型,如何从庞杂的企业数据体现出业务价值已经被越来越多的企业所重视。 企业数据

    2024年02月05日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包