Vue 3的新特性解析与开发实践指南

390 阅读4分钟

1、使用defineCustomElement定义组件

在 Vue 3 中,defineCustomElement 是用来创建自定义元素的 API,它允许开发者将 Vue 组件封装成自定义元素(Custom Element),自定义元素是 Web Components 的一部分,可以在其他框架或者原生 HTML、JavaScript 环境中使用。这对于构建可复用的 UI 组件非常有用,以下是defineCustomElement 的解析和使用方法

1-1. 什么是自定义元素(Custom Element)?

自定义元素是浏览器原生支持的组件化技术,具有以下特点:

  • 跨框架:可以在 Vue、React、Angular 或原生 HTML 中使用
  • 封装性:样式和行为封装在组件内部,不会影响外部
  • 生命周期:支持自定义生命周期钩子(如 connectedCallbackdisconnectedCallback 等)

1-2. defineCustomElement 的作用

defineCustomElement 是 Vue 3 提供的 API,用于将 Vue 组件转换为自定义元素。转换后的组件可以像原生 HTML 标签一样使用

1-3. 使用 defineCustomElement 的步骤

(1) 定义 Vue 组件

首先,定义一个普通的 MyComponent.vue 组件

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const message = ref('Hello, Custom Element!');

const handleClick = () => {
  alert('Button clicked!');
};
</script>

(2) 将MyComponent.vue组件转换为自定义元素

使用 defineCustomElement 将 Vue 组件转换为自定义元素

customElements对象是Web Components API的一部分,用于定义自定义元素。 ‌在JavaScript中,customElements对象是由浏览器提供的,用于创建和管理自定义元素。你可以通过调用customElements.define()方法来定义一个新的自定义元素。这个方法接受两个参数:元素的名称和元素的构造函数

// main.js
import { defineCustomElement } from 'vue';
import MyComponent from './MyComponent.vue';

// 将 Vue 组件转换为自定义元素
const MyCustomElement = defineCustomElement(MyComponent);

// 注册自定义元素
customElements.define('my-component', MyCustomElement);

(3) 在 HTML 中使用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vue Custom Element</title>
</head>
<body>
  <!-- 在 HTML 中使用 MyComponent.vue 组件-->
  <my-component></my-component>
  <script src="./main.js"></script>
</body>
</html>

  1. 传递 Props

可以通过属性(Attributes)或属性(Properties)向自定义元素传递数据

(1) 通过属性传递

属性值只能是字符串,需要通过 props 接收

<my-component message="Hello from Attribute!"></my-component>

通过 JavaScript 设置属性值,可以传递任意类型的数据

const element = document.querySelector('my-component');
element.message = 'Hello from Property!';

在组件中接收:

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 接收父组件传递过来的属性
defineProps({
  message: String,
});

const message = ref('Hello, Custom Element!');

const handleClick = () => {
  alert('Button clicked!');
};
</script>

  1. 监听事件

自定义元素可以触发自定义事件,父组件可以通过 addEventListener 监听

// MyComponent.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 接收父组件传递过来的属性
defineProps({
  message: String,
});

const message = ref('Hello, Custom Element!');

// 自定义元素可以触发自定义事件,父组件可以通过 `addEventListener` 监听
const handleClick = () => {
  const event = new CustomEvent('custom-click', {
    detail: { message: 'Button clicked!' },
  });
  dispatchEvent(event);
};
</script>

在父组件中监听:

const element = document.querySelector('my-component');
element.addEventListener('custom-click', (event) => {
  console.log(event.detail.message); // 输出: Button clicked!
});

  1. 生命周期钩子
class MyCustomElement extends HTMLElement {
  connectedCallback() {
    console.log('Custom element added to the DOM');
  }

  disconnectedCallback() {
    console.log('Custom element removed from the DOM');
  }
}

customElements.define('my-custom-element', MyCustomElement);

  1. 样式封装 自定义元素支持 Shadow DOM,可以将样式封装在组件内部
class MyCustomElement extends HTMLElement {
  connectedCallback() {
    console.log('Custom element added to the DOM');
  }

  disconnectedCallback() {
    console.log('Custom element removed from the DOM');
  }
}

customElements.define('my-custom-element', MyCustomElement);

Vue 3.5 对自定义元素defineCustomElement的支持进一步增强,增加了多个实用的 API 和功能:

  • 通过 configureApp 选项支持自定义元素的应用程序配置,允许在自定义元素初始化时配置 Vue 应用实例,例如全局插件、混入或依赖注入

  • 添加 useHost()、useShadowRoot() 和 this.$host API,用于访问自定义元素的宿主元素和影子根

  • 支持通过传递 shadowRoot: false 来在没有 Shadow DOM 的情况下挂载自定义元素,默认情况下, Vue 自定义元素会使用 Shadow DOM 进行封装,以提高样式隔离性

  • 支持提供 nonce 选项,该选项将附加到自定义元素注入的 <style>标签, 用于指定 CSP(内容安全策略)的 nonce 值,确保内联样式和脚本符合安全策略

这些新的仅自定义元素选项可以通过第二个参数传递给 defineCustomElement

useHost() 是一个在 Vue 3 的自定义渲染器中使用的方法,它允许你访问宿主(host)元素。这在创建自定义渲染器或在封装原生组件时非常有用。例如,如果你正在创建一个封装了原生 <canvas> 元素的 Vue 组件,你可能需要访问底层的 DOM 元素来操作它

import { defineComponent, useHost } from 'vue';
 
export default defineComponent({
  setup() {
    const host = useHost();
    // 你可以在这里操作 host 元素
    console.log(host);
    return {};
  }
});

useShadowRoot() 同样是在自定义渲染器中使用的,它允许你访问 Shadow DOM 的根节点。这对于创建封装了 Shadow DOM 的组件特别有用,例如 Web Components

import { defineComponent, useShadowRoot } from 'vue';
 
export default defineComponent({
  setup() {
    const shadowRoot = useShadowRoot();
    // 你可以在这里操作 Shadow DOM
    console.log(shadowRoot);
    return {};
  }
});

在 Vue 2 中,this.$host 是用来访问宿主元素的。但在 Vue 3 中,特别是在使用标准的 Vue API 和不涉及自定义渲染器的情况下,并没有直接暴露 this.$host

如果你想在 Vue 3 中访问宿主元素,通常的做法是使用 ref 和 onMounted 钩子来获取对 DOM 元素的引用

import { defineComponent, onMounted, ref } from 'vue';
 
export default defineComponent({
  setup() {
    const hostRef = ref(null);
    onMounted(() => {
      if (hostRef.value) {
        // 访问宿主元素
        console.log(hostRef.value);
      }
    });
    return { hostRef };
  }
});

shadowRoot| nonce| configureApp

import { defineCustomElement } from 'vue'
import MyElement from './MyElement.ce.vue';
 
const MyElement = defineCustomElement(MyElement,{
  configureApp(app) {
    // 配置 Vue 应用实例,例如添加插件或全局变量
    app.provide('someGlobal', 'value') // 全局依赖注入
    app.directive('focus', {
        mounted(el) {
            el.focus(); 
        }
    }); // 注册指令
    app.config.errorHandler = function() {
      // 错误处理逻辑
    };

  },
  
  shadowRoot: true, // 使用 Shadow DOM
  
  nonce: generateNonce(), // 设置 CSP nonce值
  
  styles: [`
    :host {
      display: block;
      color: v-bind(color);
    }
  `],
})

// 使用浏览器原生 Crypto API 生成安全随机值(推荐)
// 生成示例:f85a7c4e3b1d9a02
function generateNonce() {
  const buffer = new Uint8Array(16);
  window.crypto.getRandomValues(buffer);
  return Array.from(buffer, byte => byte.toString(16).padStart(2, '0')).join('');
}


 
customElements.define('my-element', MyElement)

在 defineCustomElement中的configureApp配置注册指令

const MyElement = defineCustomElement({
  template: `<input v-auto-slug />`
}, {
  configureApp(app) {
    app.directive('auto-slug', {
      mounted(el) {
        el.addEventListener('input', (e) => {
          e.target.value = e.target.value
            .toLowerCase()
            .replace(/\s+/g, '-');
        });
      }
    });
  }
});

在 defineCustomElement中的configureApp配置依赖注入

defineCustomElement({
  template: `<child-component />`
}, {
  configureApp(app) {
    // 注入全局数据,依赖注入
    app.provide('apiKey', '123-456-789');
    // 注入全局方法,依赖注入
    app.provide('formatDate', (date) => new Date(date).toLocaleString());
  }
});

子组件通过 inject 使用:

const apiKey = inject('apiKey');
const formatDate = inject('formatDate');

总结

defineCustomElement 是 Vue 3 中用于将 Vue 组件转换为自定义元素的 API,具有以下优势:

  • 跨框架:可以在任何框架或原生 HTML 中使用
  • 封装性:支持 Shadow DOM 和样式封装
  • 灵活性:支持 Props、事件和生命周期钩子

通过 defineCustomElement,可以将 Vue 组件无缝集成到现有项目中,或将其发布为独立的 Web Components

2、Lazy Hydration(懒加载水合)

Vue 3.5 引入了懒加载水合策略,通过 hydrateOnVisible() 选项,仅在组件可见时进行水合。这一功能可以减少不必要的资源消耗,提高页面初次加载的性能

<script setup>
import { hydrateOnVisible } from 'vue';

const lazyComponent = hydrateOnVisible(() => import('./MyComponent.vue'));
</script>

此特性对于使用 SSR 的项目,尤其是在大规模应用中,能够显著提升用户体验

3、useId:一致的唯一 ID 生成

Vue 3.5新增的 useId() API 可以生成在 SSR 和客户端之间一致的唯一 ID,解决了在 SSR 中可能出现的 ID 不一致问题

<script setup>
import { useId } from 'vue';

const id = useId();
</script>

<template>
  <label :for="id">Name:</label>
  <input :id="id" type="text" />
</template>

这对于生成表单元素和无障碍属性的 ID,确保 SSR 应用中不会导致水合不匹配

4、 useTemplateRef:动态模板引用

Vue 3.5 之前获取dom元素的方法:

<template>
  // 步骤1 ref="myElement"
  <div ref="myElement">目标元素</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
const myElement = ref(null); // 步骤2

onMounted(() => {
  console.log(myElement.value); // 输出 DOM 元素
});
</script>

Vue 3.5 引入了一种通过 useTemplateRef() API 获取模板引用的新方法,支持动态 ref 绑定到变化的 ID

<script setup>
import { useTemplateRef } from 'vue';

const inputRef = useTemplateRef('input');
</script>

<template>
  <input ref="input">
</template>

相比之前仅限于静态 ref 属性的方法,useTemplateRef() 提供了更灵活的引用方式,也更容易理解

5、 onWatcherCleanup、onEffectCleanup:观察者清理回调,

Vue 3.5 引入了一个全局导入的 API onWatcherCleanup(),用于在观察者中注册清理回调,避免内存泄漏

类似 onEffectCleanup,专为 watch 设计的清理钩子

import { watch, onWatcherCleanup } from 'vue';

watch(id, (newId) => {
  //1这里
  const controller = new AbortController();
  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 回调逻辑
  });
  //2这里
  onWatcherCleanup(() => {
    // 中止陈旧请求
    controller.abort();
  });
});

这对于在组件卸载之前或者下一次 watch 回调执行之前进行资源清理非常有用

onEffectCleanup

在响应式 effect 清理前执行自定义逻辑(如取消定时器/请求)

专为 watchEffect 设计的清理钩子

watchEffect(() => {
  const timer = setInterval(doSomething, 1000);
  onEffectCleanup(() => clearInterval(timer));
});

6、watch、watchEffect的暂停和恢复

Vue 3.5 中对watch进行的扩展,在一些场景中,可能需要暂停 watch 或者 watchEffect 中的回调,满足业务条件后再恢复执行。Vue 3.5 为此提供了 pause() 和 resume() 方法

const { stop, pause, resume } = watchEffect(() => {
  // 观察逻辑
});

// 暂时暂停观察者
pause();

// 恢复观察者
resume();

// 停止观察
stop();

示例,使用 pause() 和 resume()

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="toggleWatch">Toggle Watch</button>
  </div>
</template>
 
<script setup>
import { ref, watch } from 'vue';
 
const count = ref(0);
let watcher; // 用于存储 watcher 的引用
 
function setupWatcher() {
  watcher = watch(count, (newVal, oldVal) => {
    console.log(`Count changed from ${oldVal} to ${newVal}`);
  });
}
 
function toggleWatch() {
  if (watcher.active) {
    watcher.pause(); // 暂停 watcher
    console.log('Watcher paused');
  } else {
    watcher.resume(); // 恢复 watcher
    console.log('Watcher resumed');
  }
}
 
setupWatcher(); // 初始化 watcher
</script>

这使得对观察者的控制更加灵活,满足复杂的业务需求

7、 Teleport 和过渡(Teleport & Transition)增强,延迟挂载

内置 <Teleport> 组件的一个已知限制是,传送目标元素必须在传送组件挂载时存在。这阻止了用户将内容传送到 Vue 渲染的其他元素中

在 Vue 3.5 中,引入了一个 defer 属性,使得 <Teleport> 组件可以延迟挂载目标元素,这对于控制复杂 UI 布局非常有帮助

// defer 的使用
<Teleport defer target="#container">...</Teleport>
<div id="container"></div>

8、响应式 Props 解构

直接解构 defineProps 保留响应性,无需 toRefs

const { count = 0 } = defineProps(['count']); // 自动追踪 count 变化