Vue3官方文档速通(下)

简介: Vue3官方文档速通(下)

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-scrollervue-virtual-scroll-gridvueuc/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:

&lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;

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>


目录
相关文章
|
2天前
|
JavaScript
Vue3中路由跳转的语法
Vue3中路由跳转的语法
105 58
|
5天前
|
前端开发
vue3+ts项目中使用mockjs
vue3+ts项目中使用mockjs
198 58
|
2天前
|
JavaScript 开发工具
vite如何打包vue3插件为JSSDK
【9月更文挑战第10天】以下是使用 Vite 打包 Vue 3 插件为 JS SDK 的步骤:首先通过 `npm init vite-plugin-sdk --template vue` 创建 Vue 3 项目并进入项目目录 `cd vite-plugin-sdk`。接着,在 `src` 目录下创建插件文件(如 `myPlugin.js`),并在 `main.js` 中引入和使用该插件。然后,修改 `vite.config.js` 文件以配置打包选项。最后,运行 `npm run build` 进行打包,生成的 `my-plugin-sdk.js` 即为 JS SDK,可在其他项目中引入使用。
|
3天前
|
JavaScript 开发者
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
|
2天前
|
JavaScript 前端开发 UED
让 HTML 向 Vue.js 华丽转身:如何把 `wangEditor` 仿腾讯文档项目整合进 Vue.js
让 HTML 向 Vue.js 华丽转身:如何把 `wangEditor` 仿腾讯文档项目整合进 Vue.js
vue3 reactive数据更新,视图不更新问题
vue3 reactive数据更新,视图不更新问题
|
1天前
|
JavaScript
|
1天前
vue3定义暴露一些常量
vue3定义暴露一些常量
|
4天前
|
JavaScript
vue学习(3)模板语法
vue学习(3)模板语法
34 11
|
3天前
|
JavaScript
vue学习(4)数据绑定
vue学习(4)数据绑定
16 10