一首歌的时间music.163.com/#/song?id=1…
逻辑复用
1. 组合式函数
什么是“组合式函数”?
"组合式函数":是一个利用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))
</scrip个t>
<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>
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 }
}
tip:
每一个调用useMouse()的组件实例会创建其独有的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字符串作为输入,所有它只执行一次请求,然后就完成了,但如果我们想让它在每次url变化时都重新请求呢?那我们可以让它同时允许接收ref作为参数:
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// 在请求之前重设状态...
data.value = null
error.value = null
// unref() 解包可能为 ref 的值
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if (isRef(url)) {
// 若输入的 URL 是一个 ref,那么启动一个响应式的请求
watchEffect(doFetch)
} else {
// 否则只请求一次
// 避免监听器的额外开销
doFetch()
}
return { data, error }
}
这个版本的uesFeth()现在同时可以接收静态url字符串和url字符串的ref.当通过isRef()检测到url是一个动态ref时,它会使用watchEffect()启动一个响应式的effect。该effct会立刻执行一次,并再次过程中将url的ref作为依赖进行跟踪。当url的ref发生变化时,数据就会被重置,并重新请求。
约定和最佳实践
命名:组合式函数约定用驼峰命名法,并以“use”作为开头。
输入参数:尽管其响应式不依赖ref,组合式函数仍可接收ref参数。如果编写的组合式函数会被其他开发者使用,最好在处理输入参数兼容ref而不只是原始的值。unref()工具函数会对此非常有帮助
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用
watch()显式地监听此 ref,或者在watchEffect()中调用unref()来进行正确的追踪。
返回值
我们在组合式函数中使用的是ref()而不是reactive().我们推荐的约定是组合式函数始终返回一个包含多个ref的函数ref的普通的非响应式对象。这样对该对象在组建中被结构为ref之后仍然保持着响应式:
// x 和 y 是两个 ref
const { x, y } = useMouse()
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用
reactive()包装一次,这样其中的 ref 会被自动解包,例如:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
副作用
在组合式函数中的确可以执行副作用:
如果你的应用用到了服务端渲染(ssr),请确保在组件挂载后才调用的声明周期钩子中执行DOM相关的副作用
确保在onUnmounted()时清除副作用。
使用限制
组合式函数在
<script setup>或setup()钩子中,应始终被同步地调用。在某些场景下,你也可以在像onMounted()这样的生命周期钩子中使用他们。 这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
将生命周期钩子注册到该组件实例上
将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
TIP:
<script setup>是唯一在调用await之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
- 将生命周期钩子注册到该组件实例上
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
通过抽取组合式函数改善代码结构
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
在选项式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)
}
// ...其他选项
}
2. 自定义指令 (Custom Directives)
介绍
自定义指令主要是为了重用涉及普通元素的底层DOM访问逻辑。
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
在
<script setup>中,以V开头的驼峰命名的变量都可以被用作一个自定义指令。
在没有使用<script setup>的情况下,自定义指令需要通过directives选项注册:
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
将一个自定义指令全局注册到应用层级也是一种常见的做法:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
TIP:
只有当所需功能只能通过直接的DOM操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用v-bind这样的内置指令声明地使用模板,也对服务端渲染更友好。
指令钩子
一般指令的定义对象可以提供集中钩子函数(都是可选的):
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
钩子参数
1.el:指令绑定到的元素。这可以用于直接操作DOM。
2.binding:一个对象,包含一下属性。
2.1:alue:传递给指令的值。如在v-my-directive="1+1"中,值是2.
2.2:oldValue:之前的值,仅在beforeUpdate和update中可用。无论值是都更改,可用。
2.3:arg:传递给指令的参数(如果有的话)。例如在v-my-directive:foo中参数是"foo".
2.4:modifies:一个包含修饰符的对象(如果有的话),在v-my-directive.fpp.bar中,修饰符对象是{foo:true,bar:true}。
2.5:instance:使用该指令的组件实例。
2.6:dir:指令的定义对象。
3.vnode:代表绑定元素的底层VNode.
4.prevNode:之前的渲染中代表指令所绑定的元素的VNode.仅在beforeUpdate和update钩子中可用。
<div v-example:foo.bar="baz">
//binding参数会是一个这样的对象:
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
//和内置指令类似,自定义指令的参数也可以是动态的:
<div v-example:[arg]="value"></div>
简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在mounted和update上实现相同的行为,除此之外并不需要其他钩子。这种情况下,我们可以直接用一个函数来定义指令。
<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>
3. 插件
介绍 插件(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! 了。
TIP: 请谨慎使用全局属性,如果在整个应用中使用不同插件注入的太多全局属性,很容易让应用变得难以理解和维护。
插件中的Provide/inject
在插件中,我们可以通过 provide 来为插件用户供给一些内容。举例来说,我们可以将插件接收到的 options 参数提供给整个应用,让任何组件都能使用这个翻译字典对象。
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
app.provide('i18n', options)
}
}
现在,插件用户就可以在他们的组件中以 i18n 为 key 注入并访问插件的选项对象了。
<script setup>
import { inject } from 'vue'
const i18n = inject('i18n')
console.log(i18n.greetings.hello)
</script>