Vue3 官方文档速通(中)https://developer.aliyun.com/article/1511952?spm=a2c6h.13148508.setting.33.f8774f0euyBLtl
4. 状态管理
4.1 什么是状态管理
多个组件共享一个共同的状态,应用场景:1.多个视图可能都依赖于同一份状态。2.来自不同视图的交互也可能需要更改同一份状态。
可行方法 1:将共享状态“提升”到共同的祖先组件上去,再通过 props 传递下来。如果组件数结构很深,会导致 Prop 逐级透传问题。
可行方法 2:通过模板引用获取父子组件实例,会导致代码难以维护。
最简单直接的方法:抽取共享状态放在全局单例中管理。
4.2 用响应式 API 做简单状态管理
用 reactive() 来创建一个响应式对象,并将它导入到多个组件中:
// store.js import { reactive } from "vue"; export const store = reactive({ count: 0, });
<!-- ComponentA.vue --> <script setup> import { store } from "./store.js"; </script> <template>From A: {{ store.count }}</template>
<!-- ComponentB.vue --> <script setup> import { store } from "./store.js"; </script> <template>From B: {{ store.count }}</template>
但是这样做有个问题,任意一个导入了 store 的组件都可以随意修改它的状态,是不太容易维护的。建议在 store 上定义方法,方法的名称应该要能表达出行动的意图:
// store.js import { reactive } from "vue"; export const store = reactive({ count: 0, increment() { this.count++; }, });
<template> <button @click="store.increment()">From B: {{ store.count }}</button> </template>
4.3 SSR 相应细节
服务端渲染 (SSR) 的应用,由于 store 是跨多个请求共享的单例,上述模式可能会导致问题。
4.4 Pinia
Pinia 有 Vue 核心团队维护。官方建议使用 Pinia。Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。
5. 测试
5.1 为什么需要测试
自动化测试能预防无意引入的 bug,并鼓励开发者将应用分解为可测试、可维护的函数、模块、类和组件。这能够帮助你和你的团队更快速、自信地构建复杂的 Vue 应用。
5.2 何时测试
越早越好!官方建议你尽快开始编写测试。拖得越久,应用就会有越多的依赖和复杂性,想要开始添加测试也就越困难。
5.3 测试的类型
单元测试:检查给定函数、类或组合式函数的输入是否产生预期的输出或副作用。
组件测试:检查你的组件是否正常挂载和渲染、是否可以与之互动,以及表现是否符合预期。
端到端测试:检查跨越多个页面的功能,并对生产构建的 Vue 应用进行实际的网络请求。这些测试通常涉及到建立一个数据库或其他后端。
5.4 单元测试
编写单元测试是为了验证小的、独立的代码单元是否按预期工作。侧重于逻辑上的正确性,只关注应用整体功能的一小部分。将捕获函数的业务逻辑和逻辑正确性的问题。以 increment 函数为例:如果任何一条断言失败了,那么问题一定是出在 increment 函数上:
// helpers.js export function increment(current, max = 10) { if (current < max) { return current + 1; } return current; }
// helpers.spec.js import { increment } from "./helpers"; describe("increment", () => { test("increments the current number by 1", () => { expect(increment(0, 10)).toBe(1); }); test("does not increment the current number over the max", () => { expect(increment(10, 10)).toBe(10); }); test("has a default max of 10", () => { expect(increment(10)).toBe(10); }); });
单元测试通常适用于独立的业务逻辑、组件、类、模块或函数,不涉及 UI 渲染、网络请求或其他环境问题。一个组件可以通过两种方式测试:
白盒:单元测试,白盒测试知晓一个组件的实现细节和依赖关系。它们更专注于将组件进行更 独立 的测试。这些测试通常会涉及到模拟一些组件的部分子组件,以及设置插件的状态和依赖性(例如 Pinia)。
黑盒:组件测试,黑盒测试不知晓一个组件的实现细节。这些测试尽可能少地模拟,以测试组件在整个系统中的集成情况。它们通常会渲染所有子组件,因而会被认为更像一种“集成测试”。请查看下方的组件测试建议作进一步了解。
Vitest 正是一个针对此目标设计的单元测试框架,它由 Vue / Vite 团队成员开发和维护。在 Vite 的项目集成它会非常简单,而且速度非常快。
5.5 组件测试
组件测试应该捕捉组件中的 prop、事件、提供的插槽、样式、CSS class 名、生命周期钩子,和其他相关的问题。当进行测试时,请记住,测试这个组件做了什么,而不是测试它是怎么做到的。
5.6 端到端(E2E)测试
端到端测试的重点是多页面的应用表现,针对你的应用在生产环境下进行网络请求。他们通常需要建立一个数据库或其他形式的后端,甚至可能针对一个预备上线的环境运行。
端到端测试通常会捕捉到路由、状态管理库、顶级组件(常见为 App 或 Layout)、公共资源或任何请求处理方面的问题。如上所述,它们可以捕捉到单元测试或组件测试无法捕捉的关键问题。
5.7 用例指南
添加 Vitest 到项目中:
npm install -D vitest happy-dom @testing-library/vue
更新 Vite 配置:
// vite.config.js import { defineConfig } from "vite"; export default defineConfig({ // ... test: { // 启用类似 jest 的全局测试 API globals: true, // 使用 happy-dom 模拟 DOM // 这需要你安装 happy-dom 作为对等依赖(peer dependency) environment: "happy-dom", }, });
接着,创建名字以 \*.test.js 结尾的文件。放在项目根目录下的 test 目录中,或者放在源文件旁边的 test 目录中。Vitest 会使用命名规则自动搜索它们:
// MyComponent.test.js import { render } from "@testing-library/vue"; import MyComponent from "./MyComponent.vue"; test("it should work", () => { const { getByText } = render(MyComponent, { props: { /* ... */ }, }); // 断言输出 getByText("..."); });
最后,在 package.json 之中添加测试命令,然后运行它:
{ // ... "scripts": { "test": "vitest" } }
npm run test
6. 服务端渲染(SSR)
6.1 总览
6.1.1 什么是 SSR?
Vue 也支持将组件在服务端直接渲染成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML“激活”(hydrate) 为能够交互的客户端应用。
6.1.2 为什么要用 SSR?
更快的首屏加载:服务端渲染的 HTML 无需等到所有的 JS 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。更快的数据库连接。
统一的心智模型:你可以使用相同的语言以及相同的声明式、面向组件的心智模型来开发整个应用,而不需要在后端模板系统和前端框架之间来回切换。
更好的 SEO:搜索引擎爬虫可以直接看到完全渲染的页面。
6.1.3 SSR 的弊端
开发限制:浏览器端特定的代码只能在某些生命周期钩子中使用;一些外部库可能需要特殊处理才能在服务端渲染的应用中运行。
更多的构建、部署要求:服务端渲染的应用需要一个能让 Node.js 服务器运行的环境,不像完全静态的 SPA 那样可以部署在任意的静态文件服务器上。
更高的服务端负载:在 Node.js 中渲染一个完整的应用要比仅仅托管静态文件更加占用 CPU 资源,因此如果你预期有高流量,请为相应的服务器负载做好准备,并采用合理的缓存策略。
6.1.4 SSR vs. SSG
静态站点生成 (Static-Site Generation,缩写为 SSG),也被称为预渲染,是另一种流行的构建快速网站的技术。
如果用服务端渲染一个页面所需的数据对每个用户来说都是相同的,那么我们可以只渲染一次,提前在构建过程中完成,而不是每次请求进来都重新渲染页面。预渲染的页面生成后作为静态 HTML 文件被服务器托管。
SSG 保留了和 SSR 应用相同的性能表现:它带来了优秀的首屏加载性能。同时,它比 SSR 应用的花销更小,也更容易部署,因为它输出的是静态 HTML 和资源文件。这里的关键词是静态:SSG 仅可以用于消费静态数据的页面,即数据在构建期间就是已知的,并且在多次部署期间不会改变。每当数据变化时,都需要重新部署。
如果你调研 SSR 只是为了优化为数不多的营销页面的 SEO (例如 /、/about 和 /contact 等),那么你可能需要 SSG 而不是 SSR。SSG 也非常适合构建基于内容的网站,比如文档站点或者博客。
6.2 基础教程
6.2.1 渲染一个应用
创建一个新的文件夹,cd 进入
执行 npm init -y
在 package.json 中添加 "type": "module" 使 Node.js 以 ES modules mode 运行
执行 npm install vue
创建一个 example.js 文件:
// 此文件运行在 Node.js 服务器上 import { createSSRApp } from "vue"; // Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下 import { renderToString } from "vue/server-renderer"; const app = createSSRApp({ data: () => ({ count: 1 }), template: `<button @click="count++">{{ count }}</button>`, }); renderToString(app).then((html) => { console.log(html); });
接着运行:
node example.js
它应该会在命令行中打印出如下内容:
<button>1</button>
然后我们可以把 Vue SSR 的代码移动到一个服务器请求处理函数里,它将应用的 HTML 片段包装为完整的页面 HTML。接下来的几步我们将会使用 express,执行 npm install express,创建下面的 server.js 文件:
import express from "express"; import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; const server = express(); server.get("/", (req, res) => { const app = createSSRApp({ data: () => ({ count: 1 }), template: `<button @click="count++">{{ count }}</button>`, }); renderToString(app).then((html) => { res.send(` <!DOCTYPE html> <html> <head> <title>Vue SSR Example</title> </head> <body> <div id="app">${html}</div> </body> </html> `); }); }); server.listen(3000, () => { console.log("ready"); });
最后,执行 node server.js,访问 http://localhost:3000。你应该可以看到页面中的按钮了。
6.2.2 客户端激活
点击按钮是无效的,因为这段 HTML 在客户端是完全静态的,浏览器中没有加载 Vue。为了让按钮可以交互,让 Vue 创建一个与服务端完全相同的应用实例,并将每个组件与它应该控制的 DOM 节点相匹配,并添加 DOM 事件监听器。使用 createSSRApp():
// 该文件运行在浏览器中 import { createSSRApp } from "vue"; const app = createSSRApp({ // ...和服务端完全一致的应用实例 }); // 在客户端挂载一个 SSR 应用时会假定 // HTML 是预渲染的,然后执行激活过程, // 而不是挂载新的 DOM 节点 app.mount("#app");
6.2.3 代码结构
服务器和客户端共享相同的应用代码,称它们为通用代码。将应用的创建逻辑拆分到一个单独的文件 app.js 中:
// app.js (在服务器和客户端之间共享) import { createSSRApp } from "vue"; export function createApp() { return createSSRApp({ data: () => ({ count: 1 }), template: `<button @click="count++">{{ count }}</button>`, }); }
客户端导入通用代码:
// client.js import { createApp } from "./app.js"; createApp().mount("#app");
服务器导入通用代码:
// server.js (不相关的代码省略) import { createApp } from "./app.js"; server.get("/", (req, res) => { const app = createApp(); renderToString(app).then((html) => { // ... }); });
在浏览器中加载客户端文件,还需要:
添加 server.use(express.static('.')) 到 server.js,托管客户端文件。
将 添加到 HTML 外壳以加载客户端入口文件。
通过在 HTML 外壳中添加 Import Map 以支持在浏览器中使用 import \* from 'vue'。
6.3 更通用的解决方案
生产就绪一个完整的 SSR 应用,实现会非常复杂,官方推荐Nuxt,一个构建于 Vue 生态系统之上的全栈框架,官方强烈建议你试一试。
6.4 书写 SSR 友好的代码
遵循以下原则:
响应性在服务端是不必要的。默认情况是禁用的。
避免在 setup() 或者 的根作用域中使用会产生副作用且需要被清理的代码。如 setInterval
通用代码不能访问平台特有的 API,如 window 或 document
SSR 环境下应用模块通常只在服务器启动时初始化一次。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染。
如果预渲染的 HTML 的 DOM 结构不符合客户端应用的期望,就会出现激活不匹配。
服务端自定义指令和客户端不一样,服务器是 getSSRProps 指令钩子。
七 最佳实践
1. 生产部署
1.1 开发环境 vs 生产环境
开发环境中提供了许多功能来提升开发体验,这些功能在生产环境中并不会被使用,应该移除所有未使用的。
1.2 不使用构建工具
不适用构建工具,从 CDN 或其他源来加载 Vue,使用的是生产环境版本(以 .prod.js 结尾的构建文件)。
1.3 使用构建工具
通过 create-vue(基于 Vite)或是 Vue CLI(基于 webpack)搭建的项目都已经预先做好了针对生产环境的配置。
1.4 追踪运行时错误
import { createApp } from 'vue' const app = createApp(...) app.config.errorHandler = (err, instance, info) => { // 向追踪服务报告错误 }
2. 性能优化
2.1 概述
Vue 很优秀了,一般不用优化,但遇到特殊场景需要微调。
页面加载性能:首次访问时,应用展示出内容与达到可交互状态的速度。
更新性能:应用响应用户输入更新的速度。
2.2 分析选项
2.3 页面加载优化
如果用例对页面加载性能很敏感,请避免将其部署为纯客户端的 SPA,而是让服务器直接发送包含用户想要查看的内容的 HTML 代码。纯客户端渲染存在首屏加载缓慢的问题,这可以通过服务器端渲染 (SSR) 或静态站点生成 (SSG) 来缓解。
尽可能的采用构建步骤
引入新的依赖项时要小心包体积膨胀!
代码分割:按需加载文件。页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。
2.4 更新优化
在 Vue 之中,一个子组件只会在其至少一个 props 改变时才会更新。所以尽量让传给子组件的 props 尽量保持稳定。
v-once 是一个内置的指令,可以用来渲染依赖运行时数据但无需再更新的内容。它的整个子树都会在未来的更新中被跳过。
v-memo 是一个内置指令,可以用来有条件地跳过某些大型子树或者 v-for 列表的更新。
2.5 通用优化
渲染大型列表,会变得很慢,可以通过列表虚拟化来提升性能。现有库vue-virtual-scroller、vue-virtual-scroll-grid、vueuc/VVirtualList
3. 无障碍访问
3.1 跳过链接
你应该在每个页面的顶部添加一个直接指向主内容区域的链接,这样用户就可以跳过在多个网页上重复的内容。通常这个链接会放在 App.vue 的顶部。
3.2 内容结构
确保设计可以支持易于访问的实现是无障碍访问最重要的部分之一。设计不仅要考虑颜色对比度、字体选择、文本大小和语言,还要考虑应用中的内容是如何组织的。
3.3 语义化表单
当创建一个表单,你可能使用到以下几个元素:<form>、<label>、<input>、<textarea> 和 <button>。标签通常放置在表格字段的顶部或左侧。
3.4 规范
万维网联盟 (W3C) Web 无障碍访问倡议 (WAI) 为不同的组件制定了 Web 无障碍性标准
4. 安全
4.1 报告漏洞
建议始终使用最新版本的 Vue 及其官方配套库,以确保你的应用尽可能地安全。
4.2 首要规则:不要使用无法信赖的模板
使用 Vue 时最基本的安全规则就是不要将无法信赖的内容作为你的组件模板。使用无法信赖的模板相当于允许任意的 JS 在你的应用中执行。更糟糕的是,如果在服务端渲染时执行了这些代码,可能会导致服务器被攻击。举例来说:
Vue.createApp({ template: `<div>` + userProvidedString + `</div>`, // 永远不要这样做! }).mount("#app");
4.3 Vue 自身的安全机制
无论是使用模板还是渲染函数,内容都是自动转义的。从而防止脚本注入。这意味着在这个模板中:
<h1>{{ userProvidedString }}</h1>
如果 userProvidedString 包含了:
'<script>alert("hi")</script>';
那么它将被转义为如下的 HTML:
<script>alert("hi")</script>
4.4 潜在的危险
在任何 Web 应用中,允许以 HTML、CSS 或 JS 形式执行未经无害化处理的、用户提供的内容都有潜在的安全隐患,因此这应尽可能避免。
4.5 最佳实践
最基本的规则就是只要你允许执行未经无害化处理的、用户提供的内容 (无论是 HTML、JS 还是 CSS),你就可能面临攻击。无论是使用 Vue、其他框架,或是不使用框架,道理都是一样的。
官方建议你熟读这些资源:HTML5 安全手册, OWASP 的跨站脚本攻击 (XSS) 防护手册
4.6 后端协调
类似跨站请求伪造 (CSRF/XSRF) 和跨站脚本引入 (XSSI) 这样的 HTTP 安全漏洞,主要由后端负责处理,因此它们不是 Vue 职责范围内的问题。
4.7 服务端渲染(SSR)
在使用 SSR 时请确保遵循SSR 文档给出的最佳实践来避免产生漏洞。
八 进阶主题
1. 使用 Vue 的多种方式
世上没有一种方案可以解决所有问题,所以 Vue 被设计成灵活的框架。
独立脚本:Vue 可以以一个单独 JS 文件的形式使用,无需构建步骤!
作为 Web Component 嵌入: Vue 来构建标准的 Web Component,这些 Web Component 可以嵌入到任何 HTML 页面中,无论它们是如何被渲染的。
单页面应用(SPA):一些应用在前端需要具有丰富的交互性、较深的会话和复杂的状态逻辑。
全栈/SSR:纯客户端的 SPA 在首屏加载和 SEO 方面有显著的问题,
JAMStack/SSR:如果所需的数据是静态的,那么服务端渲染可以提前完成。这一技术通常被称为静态站点生成 (SSG),也被称为 JAMStack。
Web 以外:Vue 可以构建桌面应用、移动端应用、3DWebGL
2. 组合式 Api 常见问答
2.1 什么是组合式 API
组合式 API (Composition API) 是一系列 API 的集合,使用函数而不是声明选项的方式书写 Vue 组件
响应式 API:例如 ref() 和 reactive()
生命周期钩子:例如 onMounted() 和 onUnmounted()
依赖注入:例如 provide() 和 inject()
2.2 为什么要有组合式 API
更好的逻辑复用、更灵活的代码组织、更好的类型推导、更小的生产包体积
2.3 与选项式 API 的关系
在写组合式 API 的代码时也运用上所有普通 JS 代码组织的最佳实践。组合式 API 能够覆盖所有状态逻辑方面的需求。
2.4 与 ClassApi 的关系
官方不再推荐在 Vue 3 中使用 Class API,因为组合式 API 提供了很好的 TypeScript 集成,并具有额外的逻辑重用和代码组织优势。
2.5 与 React Hooks 的对比
React Hooks 在组件每次更新时都会重新调用。
3. 深入响应式系统
Vue 最标志性的功能就是其低侵入性的响应式系统。
3.1 什么是响应性
这个 update() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态,A0 和 A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber):
let A2; function update() { A2 = A0 + A1; }
whenDepsChange() 函数有如下的任务:
当一个变量被读取时进行追踪。例如我们执行了表达式 A0 + A1 的计算,则 A0 和 A1 都被读取到了。
如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于 A0 和 A1 在 update() 执行时被访问到了,则 update() 需要在第一次调用之后成为 A0 和 A1 的订阅者。
探测一个变量的变化。例如当我们给 A0 赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。
whenDepsChange(update);
3.2 Vue 中响应性是怎么工作的
在 JS 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters,Vue 3 中则使用了 Proxy 来创建响应式对象:
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key); return target[key]; }, set(target, key, value) { target[key] = value; trigger(target, key); }, }); } function ref(value) { const refObject = { get value() { track(refObject, "value"); return value; }, set value(newValue) { value = newValue; trigger(refObject, "value"); }, }; return refObject; }
4. 渲染机制
4.1 虚拟 DOM
虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
这里所说的 vnode 即一个纯 JS 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。
一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。
如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。
const vnode = { type: "div", props: { id: "hello", }, children: [ /* 更多 vnode */ ], };
4.2 渲染管线
Vue 组件挂载时会发生如下几件事:编译:Vue 模板被编译为渲染函数、挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树、更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。
4.3 模板 vs 渲染函数
Vue 模板会被预编译成虚拟 DOM 渲染函数。
5. 渲染函数 & JSX
5.1 基础用法
Vue 提供了一个 h() 函数用于创建 vnodes:
import { h } from "vue"; const vnode = h( "div", // type { id: "foo", class: "bar" }, // props [ /* children */ ] );
h() 函数的使用方式非常的灵活:
// 除了类型必填以外,其他的参数都是可选的 h("div"); h("div", { id: "foo" }); // attribute 和 property 都能在 prop 中书写 // Vue 会自动将它们分配到正确的位置 h("div", { class: "bar", innerHTML: "hello" }); // 像 `.prop` 和 `.attr` 这样的的属性修饰符 // 可以分别通过 `.` 和 `^` 前缀来添加 h("div", { ".name": "some-name", "^width": "100" }); // 类与样式可以像在模板中一样 // 用数组或对象的形式书写 h("div", { class: [foo, { bar }], style: { color: "red" } }); // 事件监听器应以 onXxx 的形式书写 h("div", { onClick: () => {} }); // children 可以是一个字符串 h("div", { id: "foo" }, "hello"); // 没有 props 时可以省略不写 h("div", "hello"); h("div", [h("span", "hello")]); // children 数组可以同时包含 vnodes 与字符串 h("div", ["hello", h("span", "hello")]);
得到的 vnode 为如下形式:
const vnode = h("div", { id: "foo" }, []); vnode.type; // 'div' vnode.props; // { id: 'foo' } vnode.children; // [] vnode.key; // null
5.2 JSX/TSX
JSX 是 JS 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:
const vnode = <div>hello</div>;
在 JSX 表达式中,使用大括号来嵌入动态值:
const vnode = <div id={dynamicId}>hello, {userName}</div>;
5.3 渲染函数案例
// v-if h('div', [ok.value ? h('div', 'yes') : h('span', 'no')]) // 等价于 <div> <div v-if="ok">yes</div> <span v-else>no</span> </div> // v-for h( 'ul', // assuming `items` is a ref with array value items.value.map(({ id, text }) => { return h('li', { key: id }, text) }) ) // 等价于 <ul> <li v-for="{ id, text } in items" :key="id"> {{ text }} </li> </ul> // v-on h( 'button', { onClick(event) { /* ... */ } }, 'click me' ) // 等价于 <button @click=""> click me </button> // 事件修饰符 h('input', { onClickCapture() { /* 捕捉模式中的监听器 */ }, onKeyupOnce() { /* 只触发一次 */ }, onMouseoverOnceCapture() { /* 单次 + 捕捉 */ } }) // 等价于 <input @click.capture="" @keyup.once='' @mouseover.once.capture=""/> // 组件 import Foo from './Foo.vue' import Bar from './Bar.jsx' function render() { return h('div', [h(Foo), h(Bar)]) } // 等价于 <div> <Foo /> <Bar /> </div> // 传递插槽 // 单个默认插槽 h(MyComponent, () => 'hello') // 等价于 <MyComponent>hello</MyComponent> // 具名插槽 // 注意 `null` 是必需的 // 以避免 slot 对象被当成 prop 处理 h(MyComponent, null, { default: () => 'default slot', foo: () => h('div', 'foo'), bar: () => [h('span', 'one'), h('span', 'two')] }) // 等价于 <MyComponent> <template #default>default slot</template> <template #foo> <div>foo</div> </template> <template #bar> <span>one</span> <span>two</span> </template> </MyComponent> // 内置组件 import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue' export default { setup () { return () => h(Transition, { mode: 'out-in' }, /* ... */) } } // 等价于 <Transition mode='out-in'></Transition> // v-model export default { props: ['modelValue'], emits: ['update:modelValue'], setup(props, { emit }) { return () => h(SomeComponent, { modelValue: props.modelValue, 'onUpdate:modelValue': (value) => emit('update:modelValue', value) }) } } // 等价于 <SomeComponent v-model=""></SomeComponent> // 自定义指令 import { h, withDirectives } from 'vue' // 自定义指令 const pin = { mounted() { /* ... */ }, updated() { /* ... */ } } // <div v-pin:top.animate="200"></div> const vnode = withDirectives(h('div'), [ [pin, 200, 'top', { animate: true }] ]) // 模板引用 import { h, ref } from 'vue' export default { setup() { const divEl = ref() // <div ref="divEl"> return () => h('div', { ref: divEl }) } }
6. Vue 与 Web Components
Web Components 是一组 web 原生 API 的统称,允许开发者创建可复用的自定义元素 (custom elements)。
自定义元素的主要好处是,它们可以在使用任何框架,甚至是在不使用框架的场景下使用。
6.1 在 Vue 中使用自定义元素
默认情况下,Vue 会将非原生的 HTML 标签优先当作 Vue 组件处理,而将“渲染一个自定义元素”作为后备选项。要让 Vue 知晓特定元素应该被视为自定义元素并跳过组件解析,我们可以指定 compilerOptions.isCustomElement 这个选项:
// 仅在浏览器内编译时才会工作 // 如果使用了构建工具,请看下面的配置示例 app.config.compilerOptions.isCustomElement = (tag) => tag.includes("-");
// vite.config.js import vue from "@vitejs/plugin-vue"; export default { plugins: [ vue({ template: { compilerOptions: { // 将所有带短横线的标签名都视为自定义元素 isCustomElement: (tag) => tag.includes("-"), }, }, }), ], };
// vue.config.js module.exports = { chainWebpack: (config) => { config.module .rule("vue") .use("vue-loader") .tap((options) => ({ ...options, compilerOptions: { // 将所有以 ion- 开头的标签都视为自定义元素 isCustomElement: (tag) => tag.startsWith("ion-"), }, })); }, };
6.2 使用 Vue 构建自定义元素
Vue 提供了一个和定义一般 Vue 组件几乎完全一致的 defineCustomElement 方法来支持创建自定义元素。这个方法接收的参数和 defineComponent 完全相同。但它会返回一个继承自 HTMLElement 的自定义元素构造器:
<my-vue-element></my-vue-element>
import { defineCustomElement } from "vue"; const MyVueElement = defineCustomElement({ // 这里是同平常一样的 Vue 组件选项 props: {}, emits: {}, template: `...`, // defineCustomElement 特有的:注入进 shadow root 的 CSS styles: [`/* inlined css */`], }); // 注册自定义元素 // 注册之后,所有此页面中的 `<my-vue-element>` 标签 // 都会被升级 customElements.define("my-vue-element", MyVueElement); // 你也可以编程式地实例化元素: // (必须在注册之后) document.body.appendChild( new MyVueElement({ // 初始化 props(可选) }) );
6.3 Web Components vs Vue Components
自定义元素和 Vue 组件之间确实存在一定程度的功能重叠:它们都允许我们定义具有数据传递、事件发射和生命周期管理的可重用组件。然而,Web Components 的 API 相对来说是更底层的和更基础的。
7. 动画技巧
Vue 除了 <Transition> 和 <TransitionGroup>,还有其他的方式制作动画。
7.1 基于 Css class 的动画
对于那些不是正在进入或离开 DOM 的元素,可以通过给它们动态添加 CSS class 来触发动画:
const disabled = ref(false); function warnDisabled() { disabled.value = true; setTimeout(() => { disabled.value = false; }, 1500); }
<div :class="{ shake: disabled }"> <button @click="warnDisabled">Click me</button> <span v-if="disabled">This feature is disabled!</span> </div>
.shake { animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; transform: translate3d(0, 0, 0); } @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } }
7.2 状态驱动的动画
有些过渡效果可以通过动态插值来实现,比如在交互时动态地给元素绑定样式。看下面这个例子:
const x = ref(0); function onMousemove(e) { x.value = e.clientX; }
<div @mousemove="onMousemove" :style="{ backgroundColor: `hsl(${x}, 80%, 50%)` }" class="movearea" > <p>Move your mouse across this div...</p> <p>x: {{ x }}</p> </div>
.movearea { transition: 0.3s background-color ease; }
7.3 基于侦听器的动画
通过发挥一些创意,我们可以基于一些数字状态。配合侦听器给任何东西加上动画。例如,我们可以将数字本身变成动画:
import { ref, reactive, watch } from "vue"; import gsap from "gsap"; const number = ref(0); const tweened = reactive({ number: 0, }); watch(number, (n) => { gsap.to(tweened, { duration: 0.5, number: Number(n) || 0 }); });
Type a number: <input v-model.number="number" /> <p>{{ tweened.number.toFixed(0) }}</p>