可组合的Vue
如果无法正常显示,请先停止浏览器的去广告插件。
1. 可组合的 Vue
Composable Vue,
ANTHONY FU
Hangzhou, China 2021
编写可组合可复用的 Vue 函数的最佳实践与技巧
2. Anthony Fu
Vue
核心成员 / Vite 团队成员
VueUse, Slidev, Type Challenges
全职开源
antfu
antfu7
Anthony Fu
antfu.me
等项目创作者
3. Gold Sponsors
Leniolabs_
Nuxt
Vue Mastery Evan You
Sponsors
IU
hiroki osameHunter Liu Ben Hong PENG Rui 琚致远 Jan-HenrikFallDownTh...
Johann
Backers
在 GitHub 上赞助我
4. Vue Composition API
组合式 API
5. 什么是组合式
API?
在
中引入的一种新的编写 组件的方式。
Vue 3
Vue
<script>
export default {
data() {
return {
dark: false
}
},
computed: {
light() {
return !this.dark
}
},
methods: {
toggleDark() {
this.dark = !this.dark
}
}
}
</script>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const dark = ref(false)
const light = computed(() => !dark.value)
return {
dark,
light,
toggleDark() {
dark.value = !dark.value
}
}
}
}
</script>
6. 为什么引入组合式 API ?
对象式 API 存在的问题
不利于复用
潜在命名冲突
上下文丢失
有限的类型支持
按 API 类型组织
组合式 API 提供的能力
极易复用 ( 原生 JS 函数 )
可灵活组合 ( 生命周期钩子可多次使用 )
提供更好的上下文支持
更好的 TypeScript 类型支持
按功能 / 逻辑组织
可独立于 Vue 组件使用
7. 什么是可组合的函数
可复用逻辑的集合,专注点分离
export function useDark(options: UseDarkOptions = {}) {
const preferredDark = usePreferredDark()
// <--
const store = useLocalStorage('vueuse-dark', 'auto') // <--
return computed<boolean>({
get() {
return store.value === 'auto'
? preferredDark.value
: store.value === 'dark'
},
set(v) {
store.value = v === preferredDark.value
? 'auto' : v ? 'dark' : 'light'
},
})
}
在 VueUse 中可用 : usePreferredDark useLocalStorage useDark
Dark
8. 组合关系
useLocalStorage
useStorage
useDark
useEventListener
usePreferredDark
其中每一个函数都可以独立使用
专注点分离
useMediaQuery
9. 建立
" 连结 "
不同于 , 的
React Vue
模式
` setup() `
只会在组件建立时执行一次,并建立数据与逻辑之间的连结。
建立 输入 → 输出 的连结
输出会自动根据输入的改变而改变
EXCEL
= ? ²+ ? ²=2x2+4x4=20
?
?
2
²
?
4
?
?
4
²
?
16
20
中的公式
10. 模式和技巧
编写可复用,可组合的逻辑
11. 同时兼容 Vue 2 和 Vue 3
Tree-shakeable ESM
TypeScript
兼容
核心包含 110+ 组合式函数
丰富的生态系统 8+ 扩展包
CDN
Vue
v4.11.0
组合式 API 工具包
85k/month
docs & demos
Stars
4k
114 functions
12. Ref
Reactive
import { ref } from 'vue' import { reactive } from 'vue'
let foo = 0
let bar = ref(0) const foo = { prop: 0 }
const bar = reactive({ prop: 0 })
foo = 1
bar = 1 // ts-error foo.prop = 1
bar.prop = 1
PROS
显式调用,类型检查
相比 Reactive 局限更少
CONS
` .value `
PROS
自动 Unwrap ( 即不需要 ` .value ` )
CONS
在类型上和一般对象没有区别
使用 ES6 解构会使响应性丢失
需要使用箭头函数包装才能使用 ` watch `
13. 自动解包
核心
Ref
在众多情况下,我们可以减少 ` .value ` 的使用
直接接受 Ref 作为监听对
象,并在回调函数中返回解包后的值
` watch `
Ref
在模版中自动解包
使用 Reactive 解包嵌套的 Ref
const counter = ref(0)
watch(counter, count => {
console.log(count) // same as `counter.value`
})
<template>
<button @click="counter += 1">
Counter is {{ counter }}
</button>
</template>
import { ref, reactive } from 'vue'
const foo = ref('bar')
const data = reactive({ foo, id: 10 })
data.foo // 'bar'
14. ` unref ` - Ref
的反操作
核心
如果传入一个 Ref ,返回其值
否则原样返回
实现
function unref<T>(r: Ref<T> | T): T {
return isRef(r) ? r.value : r
}
使用
import { unref, ref } from 'vue'
const foo = ref('foo')
unref(foo) // 'foo'
const bar = 'bar'
unref(bar) // 'bar'
15. 接受 Ref 作为函数参数
模式
实现
纯函数
function add(a: number, b: number) {
return a + b
}
用例
let a = 1
let b = 2
let c = add(a, b) // 3
接受 Ref 作为参数,
返回一个响应式的结果
同时接受传入值和 Ref
function add(a: Ref<number>, b: Ref<number>) {
return computed(() => a.value + b.value)
}
const a = ref(1)
const b = ref(2)
const c = add(a, b)
c.value // 3
function add(
a: Ref<number> | number,
b: Ref<number> | number
) {
return computed(() => unref(a) + unref(b))
}
const a = ref(1)
const c = add(a, 5)
c.value // 6
16. MaybeRef
类型工具
技巧
type MaybeRef<T> = Ref<T> | T
在 VueUse 中我们大量地使用 ` MaybeRef ` 来支持可选择性的响应式参数
export function useTimeAgo(
time: Date | number | string | Ref<Date | number | string>,
) {
return computed(() => someFormating(unref(time)))
}
import { computed, unref, Ref } from 'vue'
type MaybeRef<T> = Ref<T> | T
export function useTimeAgo(
time: MaybeRef<Date | number | string>,
) {
return computed(() => someFormating(unref(time)))
}
17. 让你的函数变得更灵活
就像乐高,让你的函数可以适应不同的使用场景。
模式
构造一个 " 特殊的 " REF
绑定上一个现有的 REF
import { useTitle } from '@vueuse/core'
import { ref, computed } from 'vue'
import { useTitle } from '@vueuse/core'
const title = useTitle()
title.value = 'Hello World'
//
Ref
网页的标题随
改变
const name = ref('Hello')
const title = computed(() => {
return `${name.value} - World`
})
useTitle(title) // Hello - World
name.value = 'Hi' // Hi - World
在 VueUse 中可用 : useTitle
18. ` useTitle `
` useTitle `
用例
的实现
import { ref, watch } from 'vue'
import { MaybeRef } from '@vueuse/core'
export function useTitle(
newTitle: MaybeRef<string | null | undefined>
) {
const title = ref(newTitle || document.title)
watch(title, (t) => {
if (t != null)
document.title = t
}, { immediate: true })
return title
}
<-- 1.
<-- 2.
重复使用用户提供的 Ref, 或者建立一个新的
将页面标题与 Ref 进行同步
19. 重复使用已有
Ref
如果将一个
传递给
构造函数,它将会原样将其返回。
核心
` ref `
` ref() `
const foo = ref(1)
// Ref<1>
const bar = ref(foo) // Ref<1>
foo === bar // true
function useFoo(foo: Ref<string> | string) {
//
const bar = isRef(foo) ? foo : ref(foo)
不需要额外操作
与上面的代码等效
//
const bar = ref(foo)
/* ... */
}
这个技巧在编写不确定参数类型的函数时十分有用。
20. ` ref ` / ` unref `
技巧
可以很好的配合 ` ref ` 和 ` unref ` 进行使用。
使用 ` ref() ` 当你想要想要将其标准化为 Ref
使用 ` unref() ` 当你想要获得其值
` MaybeRef<T> `
type MaybeRef<T> = Ref<T> | T
function useBala<T>(arg: MaybeRef<T>) {
const reference = ref(arg) //
ref
const value = unref(arg)
//
}
得到
得到值
21. 由 以在使用可组合的函数式,同时获得
Ref 组成的对象
模式
` ref `
import { ref, reactive } from 'vue'
function useMouse() {
return {
x: ref(0),
y: ref(0)
}
}
const { x, y } = useMouse()
const mouse = reactive(useMouse())
mouse.x === x.value // true
和 ` reactive ` 的好处。
可以直接使用 ES6 解构其中的 Ref 使用
根据使用方式,当想要自动解包的功能时,可以
使用 ` reactive ` 将其转换为对象
22. 将异步操作转换为
“ 同步 ”
使用组合式 我们甚至可以将异步请求转换为 同步 的
技巧
API,
“
”
异步
const data = await fetch('https://api.github.com/').then(r => r.json())
// use data
组合式 API
const { data } = useFetch('https://api.github.com/').json()
const user_url = computed(() => data.value?.user_url)
先建立数据间的 “ 连结 ” ,然后再等待异步请求返回将数据填充。概念和 React 中的 SWR (stale-while-revalidate)
类似。
23. ` useFetch `
用例
export function useFetch<R>(url: MaybeRef<string>) {
const data = shallowRef<T | undefined>()
const error = shallowRef<Error | undefined>()
fetch(unref(url))
.then(r => r.json())
.then(r => data.value = r)
.catch(e => error.value = e)
return {
data,
error
}
}
在 VueUse 中可用 : useFetch
24. 副作用自动清除
中原生的
和
模式
会在组件销毁时自动解除其内部的依赖监听。
我们可以编写我们的函数时,遵循同样的模式。
Vue
` watch `
` computed ` API
import { onUnmounted } from 'vue'
export function useEventListener(target: EventTarget, name: string, fn: any) {
target.addEventListener(name, fn)
onUnmounted(() => {
target.removeEventListener(name, fn) // <--
})
}
在 VueUse 中可用 : useEventListener
25. ` effectScope ` RFC
即将到来
一个新的 API 用于自动收集副作用,计划在 Vue 3.2 中引入
//
在函数在
Scope
内创建的
effect, computed, watch, watchEffect
const scope = effectScope(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(double.value))
watchEffect(() => console.log('Count: ', double.value))
})
清除
//
Scope
stop(scope)
内的所有
Effect
详见 https://github.com/vuejs/rfcs/pull/212
等将会被自动收集
26. 类型安全的
Provide / Inject
使用 提供的
类型工具来在不同的上下文中共享类型。
核心
Vue
` InjectionKey<T> `
// context.ts
import { InjectionKey } from 'vue'
export interface UserInfo {
id: number
name: string
}
export const injectKeyUser: InjectionKey<UserInfo> = Symbol()
27. 类型安全的
Provide / Inject
从同一个模组中为
和
引入相同的
` provide `
` inject `
核心
Key
// parent.vue
import { provide } from 'vue'
import { injectKeyUser } from './context' // child.vue
import { inject } from 'vue'
import { injectKeyUser } from './context'
export default {
setup() {
provide(injectKeyUser, {
id: '7', //
name: 'Anthony'
})
}
} export default {
setup() {
const user = inject(injectKeyUser)
// UserInfo | undefined
类型错误
if (user)
console.log(user.name) // Anthony
}
}
28. 状态共享
由于组合式 天然提供的灵活性,状态可以独立于组件被创建并使用。
模式
API
// shared.ts
import { reactive } from 'vue' // A.vue
import { state } from './shared.ts'
export const state = reactive({
foo: 1,
bar: 'Hello'
}) state.foo += 1
// B.vue
import { state } from './shared.ts'
console.log(state.foo) // 2
⚠️ 此方案不兼容 SSR!
29. 兼容
SSR 的状态共享
使用
和
来共享应用层面的状态。
模式
` provide `
` inject `
export const myStateKey: InjectionKey<MyState> = Symbol()
export function createMyState() {
const state = {
/* ... */
}
return {
install(app: App) {
app.provide(myStateKey, state)
}
}
}
export function useMyState(): MyState {
return inject(myStateKey)!
}
// main.ts
const App = createApp(App)
app.use(createMyState())
// A.vue
在任何组件中使用这个函数来获得状态对象
//
const state = useMyState()
Vue Router v4
也使用的类似的方式
` createRouter() `
` useRouter() `
30. 技巧
useVModel
一个让使用 props 和 emit 更加容易的工具
export function useVModel(props, name) {
const emit = getCurrentInstance().emit
return computed({
get() {
return props[name]
},
set(v) {
emit(`update:${name}`, v)
}
})
}
在 VueUse 中可用 : useVModel
export default defineComponent({
setup(props) {
const value = useVModel(props, 'value')
return { value }
}
})
<template>
<input v-model="value" />
</template>
31. 以上所述,均可使用于 Vue 2 和 3
32. ` @vue/composition-api `
库
为 Vue 2 提供组合式 API 的插件。
vuejs/composition-api
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
import { ref, reactive } from '@vue/composition-api'
33. 即将到来
Vue 2.7
Vue 2.7 计划
将 ` @vue/composition-api ` 整合进 Vue 2 的核心
在 SFC 中 ` <script setup> ` 支持语法
将 Vue 2 代码库迁移到 TypeScript
Vue 2 将继续支持 IE11
LTS
34. 库
Vue Demi
创建 Vue 2 和 3 兼容的插件 / 库
vueuse/vue-demi
// same syntax for both Vue 2 and 3
import { ref, reactive, defineComponent } from 'vue-demi'
35. 快速回顾
建立 “ 连结 ”
接受 Ref 作为函数参数
返回由 Ref 组成的对象
使用 ref / unref 让函数变得更加灵活
将异步操作转换为 “ 同步 ”
副作用自动清除
类型安全的 Provide / Inject
状态共享
` useVModel `
36. 谢谢!
幻灯片可以在我的网站
antfu.me
上下载