马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
课题摘要:本文探讨了Vue.js中的逻辑复用,重点先容了组合式函数、自定义指令和插件。组合式函数利用Vue的组合式API封装有状态逻辑,便于在多个组件中复用。例如,useMouse组合式函数可以跨组件追踪鼠标位置。自定义指令允许重用涉及DOM操作的逻辑,如v-focus指令使元素自动聚焦。插件为Vue添加全局功能,如i18n插件提供全局翻译功能。这些工具使Vue应用开发更高效、可维护,同时保持代码的清晰和组织。
一、组合式函数
什么是“组合式函数”?
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用 有状态逻辑 的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在差别地方格式化时间,我们大概会抽取一个可复用的日期格式化函数。这个函数封装了 无状态的逻辑:它在吸收一些输入后立即返回所盼望的输出。复用无状态逻辑的库有很多,好比你大概已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管分析随时间而厘革的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也大概是像触摸手势或与数据库的毗连状态这样的更复杂的逻辑。
鼠标跟踪器示例
假如我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
- <script setup>
- import { ref, onMounted, onUnmounted } from 'vue'
- const x = ref(0)
- const y = ref(0)
- function update(event) {
- x.value = event.pageX
- y.value = event.pageY
- }
- onMounted(() => window.addEventListener('mousemove', update))
- onUnmounted(() => window.removeEventListener('mousemove', update))
- </script>
- <template>Mouse position is at: {{ x }}, {{ y }}</template>
复制代码 但是,假如我们想在多个组件中复用这个雷同的逻辑呢?我们可以把这个逻辑以一个组合式函数的情势提取到外部文件中:
- // mouse.js
- import { ref, onMounted, onUnmounted } from 'vue'
- // 按照惯例,组合式函数名以“use”开头
- export function useMouse() {
- // 被组合式函数封装和管理的状态
- const x = ref(0)
- const y = ref(0)
- // 组合式函数可以随时更改其状态。
- function update(event) {
- x.value = event.pageX
- y.value = event.pageY
- }
- // 一个组合式函数也可以挂靠在所属组件的生命周期上
- // 来启动和卸载副作用
- onMounted(() => window.addEventListener('mousemove', update))
- onUnmounted(() => window.removeEventListener('mousemove', update))
- // 通过返回值暴露所管理的状态
- return { x, y }
- }
复制代码 下面是它在组件中使用的方式:
- <script setup>
- import { useMouse } from './mouse.js'
- const { x, y } = useMouse()
- </script>
- <template>Mouse position is at: {{ x }}, {{ y }}</template>
复制代码 Mouse position is at: 0, 0
焦点逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的 组合式 API。现在, useMouse() 的功能可以在任何组件中轻易复用了。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 聚集命名为组合式 API。
举例来说,我们可以将添加和打扫 DOM 事件监听器的逻辑也封装进一个组合式函数中:
- // event.js
- import { onMounted, onUnmounted } from 'vue'
- export function useEventListener(target, event, callback) {
- // 如果你想的话,
- // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
- onMounted(() => target.addEventListener(event, callback))
- onUnmounted(() => target.removeEventListener(event, callback))
- }
复制代码 有了它,之前的 useMouse() 组合式函数可以被简化为:
- // mouse.js
- import { ref } from 'vue'
- import { useEventListener } from './event'
- export function useMouse() {
- const x = ref(0)
- const y = ref(0)
- useEventListener(window, 'mousemove', (event) => {
- x.value = event.pageX
- y.value = event.pageY
- })
- return { x, y }
- }
复制代码 异步状态示例
useMouse() 组合式函数没有吸收任何参数,因此让我们再来看一个需要吸收一个参数的组合式函数示例。在做异步数据哀求时,我们常常需要处理差别的状态:加载中、加载乐成和加载失败。
- <script setup>
- import { ref } from 'vue'
- const data = ref(null)
- const error = ref(null)
- fetch('...')
- .then((res) => res.json())
- .then((json) => (data.value = json))
- .catch((err) => (error.value = err))
- </script>
- <template>
- <div v-if="error">Oops! Error encountered: {{ error.message }}</div>
- <div v-else-if="data">
- Data loaded:
- <pre>{{ data }}</pre>
- </div>
- <div v-else>Loading...</div>
- </template>
复制代码 假如在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:
- // fetch.js
- import { ref } from 'vue'
- export function useFetch(url) {
- const data = ref(null)
- const error = ref(null)
- fetch(url)
- .then((res) => res.json())
- .then((json) => (data.value = json))
- .catch((err) => (error.value = err))
- return { data, error }
- }
复制代码 现在我们在组件里只需要:
- <script setup>
- import { useFetch } from './fetch.js'
- const { data, error } = useFetch('...')
- </script>
复制代码 吸收响应式状态
useFetch() 吸收一个静态 URL 字符串作为输入——因此它只会执行一次 fetch 而且就此结束。假如我们想要在 URL 改变时重新 fetch 呢?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器。
举例来说, useFetch() 应该可以大概吸收一个 ref:
- const url = ref('/initial-url')
- const { data, error } = useFetch(url)
- // 这将会重新触发 fetch
- url.value = '/new-url'
复制代码 大概吸收一个 getter 函数:
- // 当 props.id 改变时重新 fetch
- const { data, error } = useFetch(() => `/posts/${props.id}`)
复制代码 我们可以用 watchEffect() 和 toValue() API 来重构我们现有的实现:
- // fetch.js
- import { ref, watchEffect, toValue } from 'vue'
- export function useFetch(url) {
- const data = ref(null)
- const error = ref(null)
- const fetchData = () => {
- // reset state before fetching..
- data.value = null
- error.value = null
- fetch(toValue(url))
- .then((res) => res.json())
- .then((json) => (data.value = json))
- .catch((err) => (error.value = err))
- }
- watchEffect(() => {
- fetchData()
- })
- return { data, error }
- }
复制代码 toValue() 是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值。假如参数是 ref,它会返回 ref 的值;假如参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于 unref(),但对函数有特别处理。
注意 toValue(url) 是在 watchEffect 回调函数的 内部 调用的。这确保了在 toValue() 规范化期间访问的任何响应式依赖项都会被侦听器跟踪。
这个版本的 useFetch() 现在能吸收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,而且会跟踪 toValue(url) 期间访问的任何依赖项。假如没有跟踪到依赖项 (例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。
这是 更新后的 useFetch(),为了便于演示,添加了人为延迟和随机错误。
约定和最佳实践
命名
组合式函数约定用驼峰命名法命名,并以“use”作为开头。
输入参数
即便不依赖于 ref 或 getter 的响应性,组合式函数也可以吸收它们作为参数。假如你正在编写一个大概被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() 工具函数来实现:
- import { toValue } from 'vue'
- function useFeature(maybeRefOrGetter) {
- // 如果 maybeRefOrGetter 是一个 ref 或 getter,
- // 将返回它的规范化值。
- // 否则原样返回。
- const value = toValue(maybeRefOrGetter)
- }
复制代码 假如你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它可以大概被正确追踪,请确保要么使用 watch() 显式地监视 ref 或 getter,要么在 watchEffect() 中调用 toValue()。
前面讨论过的 useFetch() 实现 提供了一个接受 ref、getter 或普通值作为输入参数的组合式函数的具体示例。
返回值
你大概已经注意到了,我们一直在组合式函数中使用 ref() 而不是 reactive()。我们保举的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
- // x 和 y 是两个 ref
- const { x, y } = useMouse()
复制代码 从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性毗连。与之相反,ref 则可以维持这一响应性毗连。
假如你更希望以对象属性的情势来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样此中的 ref 会被自动解包,例如:
- const mouse = reactive(useMouse())
- // mouse.x 链接到了原来的 x ref
- console.log(mouse.x)
复制代码- Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
复制代码 副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器大概哀求数据),但请注意以下规则:
- 假如你的应用用到了 服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如: onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。
- 确保在 onUnmounted() 时清理副作用。举例来说,假如一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。当然也可以像之前的 useEventListener() 示例那样,使用一个组合式函数来自动帮你做这些事。
使用限制
组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被 同步 调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。
这些限制很重要,由于这些是 Vue 用于确定当前活泼的组件实例的上下文。访问活泼的组件实例很有必要,这样才能:
- 将生命周期钩子注册到该组件实例上
- 将盘算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
在选项式 API 中使用组合式函数
假如你正在使用选项式 API,组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回,以便暴露给 this 及其模板:
- import { useMouse } from './mouse.js'
- import { useFetch } from './fetch.js'
- export default {
- setup() {
- const { x, y } = useMouse()
- const { data, error } = useFetch('...')
- return { x, y, data, error }
- },
- mounted() {
- // setup() 暴露的属性可以在通过 `this` 访问到
- console.log(this.x)
- }
- // ...其他选项
- }
复制代码 与其他模式的比力
和 Mixin 的对比
Vue 2 的用户大概会对 mixins 选项比力认识。它也让我们可以大概把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
- 不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们保举在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
- 命名空间冲突:多个来自差别作者的 mixin 大概会注册雷同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量举行重命名来避免雷同的键名。
- 隐式的跨 mixin 互换:多个 mixin 需要依赖共享的属性名来举行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
基于上述理由,我们不再保举在 Vue 3 中继承使用 mixin。保留该功能只是为了项目迁移的需求和照顾认识它的用户。
和无渲染组件的对比
在组件插槽一章中,我们讨论过了基于作用域插槽的 无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。
组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。
我们保举在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图结构时使用无渲染组件。
和 React Hooks 的对比
假如你有 React 的开发履历,你大概注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部门灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性体系,这和 React hooks 的执行模子有本质上的差别。这一话题在 组合式 API 的常见标题 中有更过细的讨论。
二、自定义指令
先容
除了 Vue 内置的一系列指令(好比 v-model 或 v-show)之外,Vue 还允许你注册自定义的指令(Custom Directives)。
我们已经先容了两种在 Vue 中重用代码的方式:组件和组合式函数。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会吸收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
- <script setup>
- // 在模板中启用 v-focus
- const vFocus = {
- mounted: (el) => el.focus()
- }
- </script>
- <template>
- <input v-focus />
- </template>
复制代码- const focus = {
- mounted: (el) => el.focus()
- }
- export default {
- directives: {
- // 在模板中启用 v-focus
- focus
- }
- }
复制代码 假设你还未点击页面中的其他地方,那么上面这个 input 元素应该会被自动聚焦。该指令比 autofocus attribute 更有用,由于它不但仅可以在页面加载完成后见效,还可以在 Vue 动态插入元素后见效。
在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus 即可以在模板中以 v-focus 的情势使用。
在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:
- export default {
- setup() {
- /*...*/
- },
- directives: {
- // 在模板中启用 v-focus
- focus: {
- /* ... */
- }
- }
- }
复制代码 和组件类似,自定义指令在模板中使用前必须先注册。在上面的例子中,我们使用 directives 选项完成了指令的局部注册。
将一个自定义指令全局注册到应用层级也是一种常见的做法:
- const app = createApp({})
- // 使 v-focus 在所有组件中都可用
- app.directive('focus', {
- /* ... */
- })
复制代码 只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽大概地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
指令钩子
一个指令的定义对象可以提供几种钩子函数(都是可选的):
- const myDirective = {
- // 在绑定元素的 attribute 前
- // 或事件监听器应用前调用
- created(el, binding, vnode) {
- // 下面会介绍各个参数的细节
- },
- // 在元素被插入到 DOM 前调用
- beforeMount(el, binding, vnode) {},
- // 在绑定元素的父组件
- // 及他自己的所有子节点都挂载完成后调用
- mounted(el, binding, vnode) {},
- // 绑定元素的父组件更新前调用
- beforeUpdate(el, binding, vnode, prevVnode) {},
- // 在绑定元素的父组件
- // 及他自己的所有子节点都更新后调用
- updated(el, binding, vnode, prevVnode) {},
- // 绑定元素的父组件卸载前调用
- beforeUnmount(el, binding, vnode) {},
- // 绑定元素的父组件卸载后调用
- unmounted(el, binding, vnode) {}
- }
复制代码 钩子参数
指令的钩子会传递以下几种参数:
- el:指令绑定到的元素。这可以用于直接操作 DOM。
- binding:一个对象,包含以下属性。
- value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
- oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指令的参数(假如有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
- modifiers:一个包含修饰符的对象(假如有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
- instance:使用该指令的组件实例。
- dir:指令的定义对象。
- vnode:代表绑定元素的底层 VNode。
- prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
举例来说,像下面这样使用指令:
- <div v-example:foo.bar="baz">
复制代码 binding 参数会是一个这样的对象:
- {
- arg: 'foo',
- modifiers: { bar: true },
- value: /* `baz` 的值 */,
- oldValue: /* 上一次更新时 `baz` 的值 */
- }
复制代码 和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
- <div v-example:[arg]="value"></div>
复制代码 这里指令的参数会基于组件的 arg 数据属性响应式地更新。
Note
除了 el 外,其他参数都是只读的,不要更改它们。若你需要在差别的钩子间共享信息,保举通过元素的 dataset attribute 实现。
简化情势
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现雷同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
- <div v-color="color"></div>
复制代码- app.directive('color', (el, binding) => {
- // 这会在 `mounted` 和 `updated` 时都调用
- el.style.color = binding.value
- })
复制代码 对象字面量
假如你的指令需要多个值,你可以向它传递一个 JavaScript 对象字面量。别忘了,指令也可以吸收任何合法的 JavaScript 表达式。
- <div v-demo="{ color: 'white', text: 'hello!' }"></div>
复制代码- app.directive('demo', (el, binding) => {
- console.log(binding.value.color) // => "white"
- console.log(binding.value.text) // => "hello!"
- })
复制代码 在组件上使用
不保举在组件上使用自定义指令。当组件具有多个根节点时大概会出现预期外的行为。
当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。
- <MyComponent v-demo="test" />
复制代码- <!-- MyComponent 的模板 -->
- <div> <!-- v-demo 指令会被应用在此处 -->
- <span>My component content</span>
- </div>
复制代码 需要注意的是组件大概含有多个根节点。当应用到一个多根组件时,指令将会被忽略且抛出一个警告。和 attribute 差别,指令不能通过 v-bind="$attrs" 来传递给一个差别的元素。
三、插件
先容
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:
- import { createApp } from 'vue'
- const app = createApp({})
- app.use(myPlugin, {
- /* 可选的选项 */
- })
复制代码 一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会吸收到安装它的应用实例和传递给 app.use() 的额外选项作为参数:
- const myPlugin = {
- install(app, options) {
- // 配置此应用
- }
- }
复制代码 插件没有严酷定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:
- 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。
- 通过 app.provide() 使一个资源可被注入进整个应用。
- 向 app.config.globalProperties 中添加一些全局实例属性或方法。
- 一个大概上述三种都包含了的功能库(例如 vue-router)。
编写一个插件
为了更好地理解如何构建 Vue.js 插件,我们可以试着写一个简单的 i18n(国际化 (Internationalization) 的缩写)插件。
让我们从设置插件对象开始。建议在一个单独的文件中创建并导出它,以保证更好地管理逻辑,如下所示:
- // plugins/i18n.js
- export default {
- install: (app, options) => {
- // 在这里编写插件代码
- }
- }
复制代码 我们希望有一个翻译函数,这个函数吸收一个以 . 作为分隔符的 key 字符串,用来在用户提供的翻译字典中查找对应语言的文本。盼望的使用方式如下:
- <h1>{{ $translate('greetings.hello') }}</h1>
复制代码 这个函数应当可以大概在任意模板中被全局调用。这一点可以通过在插件中将它添加到 app.config.globalProperties 上来实现:
- // plugins/i18n.js
- export default {
- install: (app, options) => {
- // 注入一个全局可用的 $translate() 方法
- app.config.globalProperties.$translate = (key) => {
- // 获取 `options` 对象的深层属性
- // 使用 `key` 作为索引
- return key.split('.').reduce((o, i) => {
- if (o) return o[i]
- }, options)
- }
- }
- }
复制代码 我们的 $translate 函数会吸收一个例如 greetings.hello 的字符串,在用户提供的翻译字典中查找,并返回翻译得到的值。
用于查找的翻译字典对象则应当在插件被安装时作为 app.use() 的额外参数被传入:
- import i18nPlugin from './plugins/i18n'
- app.use(i18nPlugin, {
- greetings: {
- hello: 'Bonjour!'
- }
- })
复制代码 这样,我们一开始的表达式 $translate('greetings.hello') 就会在运行时被替换为 Bonjour! 了。
插件中的 Provide / Inject
在插件中,我们可以通过 provide 来为插件用户供给一些内容。举例来说,我们可以将插件吸收到的 options 参数提供给整个应用,让任何组件都能使用这个翻译字典对象。
- // plugins/i18n.js
- export default {
- install: (app, options) => {
- app.provide('i18n', options)
- }
- }
复制代码 现在,插件用户就可以在他们的组件中以 i18n 为 key 注入并访问插件的选项对象了。
- <script setup>
- import { inject } from 'vue'
- const i18n = inject('i18n')
- console.log(i18n.greetings.hello)
- </script>
复制代码- export default {
- inject: ['i18n'],
- created() {
- console.log(this.i18n.greetings.hello)
- }
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |