前言
2019年2月6號,React 釋出 16.8.0 版本,新增 Hooks 特性。隨即,Vue 在 2019 的各大 JSConf 中也宣告了 Vue3.0 最重要的 RFC,即 Function-based API。Vue3.0 將拋棄之前的 Class API 的提案,選擇了 Function API。目前,vue 官方 也提供了 Vue3.0 特性的嚐鮮版本,前段時間叫 vue-function-api
,目前已經改名叫 composition-api
。
一、Composition API
首先,我們得了解一下,Composition API 設計初衷是什麼?
- 邏輯組合和複用
- 型別推導:Vue3.0 最核心的點之一就是使用 TS 重構,以實現對 TS 絲滑般的支援。而基於函式 的 API 則天然對型別推導很友好。
- 打包尺寸:每個函式都可作為 named ES export 被單獨引入,對 tree-shaking 很友好;其次所有函式名和 setup 函式內部的變數都能被壓縮,所以能有更好的壓縮效率。
我們再來具體瞭解一下 邏輯組合和複用 這塊。
開始之前,我們先回顧下目前 Vue2.x 對於邏輯複用的方案都有哪些?如圖
其中 Mixins 和 HOC 都可能存在 ①模板資料來源不清晰 的問題。
並且在 mixin 的屬性、方法的命名以及 HOC 的 props 注入也可能會產生 ②名稱空間衝突的問題。
最後,由於 HOC 和 Renderless Components 都需要額外的元件例項來做邏輯封裝,會導致③無謂的效能開銷。
1、基本用法
OK,大致瞭解了 Composition API 設計的目的了,接下來,我們來看看其基本用法。
安裝
npm i @vue/composition-api -S
複製程式碼
使用
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
複製程式碼
如果,你專案是用的 TS,那麼請使用 createComponent
來定義元件,這樣你才能使用型別推斷
import { createComponent } from '@vue/composition-api'
const Component = createComponent({
// ...
})
複製程式碼
由於我本身專案使用的就是 TS,所以這裡 JS 的一些用法我就不過多提及,上個尤大的例子之後就不提了
import { value, computed, watch, onMounted } from 'vue'
const App = {
template: `
<div>
<span>count is {{ count }}</span>
<span>plusOne is {{ plusOne }}</span>
<button @click="increment">count++</button>
</div>
`,
setup() {
// reactive state
const count = value(0)
// computed state
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
複製程式碼
OK,回到 TS,我們看看其基本用法,其實用法基本一致。
<template>
<div class="hooks-one">
<h2>{{ msg }}</h2>
<p>count is {{ count }}</p>
<p>plusOne is {{ plusOne }}</p>
<button @click="increment">count++</button>
</div>
</template>
<script lang="ts">
import { ref, computed, watch, onMounted, Ref, createComponent } from '@vue/composition-api'
export default createComponent({
props: {
name: String
},
setup (props) {
const count: Ref<number> = ref(0)
// computed
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log('onMounted')
})
// expose bindings on render context
return {
count,
plusOne,
increment,
msg: `hello ${props.name}`
}
}
})
</script>
複製程式碼
2、組合函式
我們已經瞭解到 Composition API 初衷之一就是做邏輯組合,這就有了所謂的組合函式。
尤大在 Vue Function-based API RFC 中舉了一個滑鼠位置偵聽的例子,我這裡舉一個帶業務場景的例子吧。
場景:我需要在一些特定的頁面修改頁面 title,而我又不想做成全域性。
傳統做法我們會直接將邏輯丟到 mixins 中,做法如下
import { Vue, Component } from 'vue-property-decorator'
declare module 'vue/types/vue' {
interface Vue {
setTitle (title: string): void
}
}
function setTitle (title: string) {
document.title = title
}
@Component
export default class SetTitle extends Vue {
setTitle (title: string) {
setTitle.call(this, title)
}
}
複製程式碼
然後在頁面引用
import SetTitle from '@/mixins/title'
@Component({
mixins: [ SetTitle ]
})
export default class Home extends Vue {
mounted () {
this.setTitle('首頁')
}
}
複製程式碼
那麼,讓我們使用 Composition API 來做處理,看看又是如何做的
export function setTitle (title: string) {
document.title = title
}
複製程式碼
然後在頁面引用
import { setTitle } from '@/hooks/title'
import { onMounted, createComponent } from '@vue/composition-api'
export default createComponent({
setup () {
onMounted(() => {
setTitle('首頁')
})
}
})
複製程式碼
能看出來,我們只需要將需要複用的邏輯抽離出來,然後只需直接在 setup()
中直接使用即可,非常的方便。
當然你硬要做成全域性也不是不行,這種情況一般會做成全域性指令,如下
import Vue, { VNodeDirective } from 'vue'
Vue.directive('title', {
inserted (el: any, binding: VNodeDirective) {
document.title = el.dataset.title
}
})
複製程式碼
頁面使用如下
<template>
<div class="home" v-title data-title="首頁">
home
</div>
</template>
複製程式碼
有些小夥伴可能看完這個場景會覺得,我這樣明顯使用全域性指令的方式更便捷啊,Vue3.0 組合函式的優勢在哪呢?
別急,上面的例子其實只是為了告訴大家如何將你們目前 Mixins 使用組合函式做改造。
在之後的實戰環節,還有很多真實場景呢,如果你等不及,可以直接跳過去看第二章。
3、setup() 函式
setup()
是 Vue3.0 中引入的一個新的元件選項,setup 元件邏輯的地方。
i. 初始化時機
setup()
什麼時候進行初始化呢?我們看張圖
setup 是在元件例項被建立時, 初始化了 props 之後呼叫,處於 created 前。
這個時候我們能接收初始 props 作為引數。
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component({
setup (props) {
console.log('setup', props.test)
return {}
}
})
export default class Hooks extends Vue {
@Prop({ default: 'hello' })
test: string
beforeCreate () {
console.log('beforeCreate')
}
created () {
console.log('created')
}
}
複製程式碼
控制檯列印順序如下
其次,我們從上面的所有例子能發現,setup()
和 data()
很像,都可以返回一個物件,而這個物件上的屬性則會直接暴露給模板渲染上下文:
<template>
<div class="hooks">
{{ msg }}
</div>
</template>
<script lang="ts">
import { createComponent } from '@vue/composition-api'
export default createComponent({
props: {
name: String
},
setup (props) {
return {
msg: `hello ${props.name}`
}
}
})
</script>
複製程式碼
ii. reactivity api
與 React Hooks 的函式增強路線不同,Vue Hooks 走的是 value 增強路線,它要做的是如何從一個響應式的值中,衍生出普通的值以及 view。
在 setup()
內部,Vue 則為我們提供了一系列響應式的 API,比如 ref,它返回一個 Ref 包裝物件,並在 view 層引用的時候自動展開
<template>
<div class="hooks">
<button @click="count++">{{ count }}</button>
</div>
</template>
<script lang="ts">
import { ref, Ref, createComponent } from '@vue/composition-api'
export default createComponent({
setup (props) {
const count: Ref<number> = ref(0)
console.log(count.value)
return {
count
}
}
})
</script>
複製程式碼
然後便是我們常見的 computed 和 watch 了
import { ref, computed, Ref, createComponent } from '@vue/composition-api'
export default createComponent({
setup (props) {
const count: Ref<number> = ref(0)
const plusOne = computed(() => count.value + 1)
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
return {
count,
plusOne
}
}
})
複製程式碼
而我們通過計算產生的值,即使不進行型別申明,也能直接拿到進行其型別做推導,因為它是依賴 Ref 進行計算的
setup()
中其它的內部 API 以及生命週期函式我這就不過多介紹了,想了解的直接檢視 原文
4、Props 型別推導
關於 Props 型別推導,一開始我就有說過,在 TS 中,你想使用型別推導,那麼你必須在 createComponent 函式來定義元件
import { createComponent } from '@vue/composition-api'
const MyComponent = createComponent({
props: {
msg: String
},
setup(props) {
props.msg // string | undefined
return {}
}
})
複製程式碼
當然,props 選項並不是必須的,假如你不需要執行時的 props 型別檢查,你可以直接在 TS 型別層面進行申明
import { createComponent } from '@vue/composition-api'
interface Props {
msg: string
}
export default createComponent({
props: ['msg'],
setup (props: Props, { root }) {
const { $createElement: h } = root
return () => h('div', props.msg)
}
})
複製程式碼
對於複雜的 Props 型別,你可以使用 Vue 提供的 PropType 來申明任意複雜度的 props 型別,不過按照其型別申明來看,我們需要用 any 做一層強制轉換
export type Prop<T> = { (): T } | { new(...args: any[]): T & object } | { new(...args: string[]): Function }
export type PropType<T> = Prop<T> | Prop<T>[]
複製程式碼
import { createComponent } from '@vue/composition-api'
import { PropType } from 'vue'
export default createComponent({
props: {
options: (null as any) as PropType<{ msg: string }>
},
setup (props) {
props.options // { msg: string } | undefined
return {}
}
})
複製程式碼
二、業務實踐
目前為止,我們對 Vue3.0 的 Composition API 有了一定的瞭解,也清楚了其適合使用的一些實際業務場景。
而我在具體業務中又做了哪些嚐鮮呢?接下來,讓我們一起進入真正的實戰階段
1、列表分頁查詢
場景:我需要對業務中的列表做分頁查詢,其中包括頁碼、頁碼大小這兩個通用查詢條件,以及一些特定條件做查詢,比如關鍵字、狀態等。
在 Vue2.x 中,我們的做法有兩種,如圖所示
- 最簡單的方式就是直接將通用查詢儲存到一個地方,需要使用查詢的地方直接引入即可,然後在頁面做一系列重複的操作,這個時候最考驗
Ctrl + C
、Ctrl + V
的功力了。 - 將其通用的變數和方法抽離到
mixins
當中,然後頁面直接使用即可,可免去一大堆重複的工作。但是當我們頁面存在一個以上的分頁列表時,問題就來了,我的變數會被沖掉,導致查詢出錯。
所以現在,我們試著使用 Vue3.0 的特性,將其重複的邏輯抽離出來放置到 @/hooks/paging-query.ts
中
import { ref, Ref, reactive } from '@vue/composition-api'
import { UnwrapRef } from '@vue/composition-api/dist/reactivity'
export function usePaging () {
const conditions: UnwrapRef<{
page: Ref<number>,
pageSize: Ref<number>,
totalCount: Ref<number>
}> = reactive({
page: ref(1),
pageSize: ref(10),
totalCount: ref(1000)
})
const handleSizeChange = (val: number) => {
conditions.pageSize = val
}
const handleCurrentChange = (val: number) => {
conditions.page = val
}
return {
conditions,
handleSizeChange,
handleCurrentChange
}
}
複製程式碼
然後我們在具體頁面中對其進行組合去使用
<template>
<div class="paging-demo">
<el-input v-model="query"></el-input>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="cons.page"
:page-sizes="[10, 20, 30, 50]"
:page-size.sync="cons.pageSize"
layout="prev, pager, next, sizes"
:total="cons.totalCount">
</el-pagination>
</div>
</template>
<script lang="ts">
import { usePaging } from '@/hooks/paging-query'
import { ref, Ref, watch } from '@vue/composition-api'
export default createComponent({
setup () {
const { conditions: cons, handleSizeChange, handleCurrentChange } = usePaging()
const query: Ref<string> = ref('')
watch([
() => cons.page,
() => cons.pageSize,
() => query.value
], ([val1, val2, val3]) => {
console.log('conditions changed,do search', val1, val2, val3)
})
return {
cons,
query,
handleSizeChange,
handleCurrentChange
}
}
})
</script>
複製程式碼
從這個例子我們能看出來,暴露給模板的屬性來源非常清晰,直接從 usePaging()
返回;並且能夠隨意重新命名,所以也不會有名稱空間衝突的問題;更不會有額外的元件例項帶來的效能損耗。
怎麼樣,有沒有點真香的感覺了。
2、user-select 元件
場景:在我負責的業務中,有一個通用的業務元件,我稱之為 user-select,它是一個人員選擇元件。如圖
關於改造前後的對比我們先看張圖,好大致有個瞭解
在 Vue2.x 中,它通用的業務邏輯和資料並沒有得到很好的處理,大致原因和上面那個案例原因差不多。
然後我每次想要使用的時候需要做以下操作,這充分鍛鍊了我 Ctrl + C
、Ctrl + V
的功力
<template>
<div class="demo">
<user-select
:options="users"
:user.sync="user"
@search="adminSearch" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'
import UserSelect from '@/views/service/components/user-select.vue'
@Component({
components: { UserSelect }
})
export default class Demo extends Vue {
user = []
users: User[] = []
@Prop()
visible: boolean
@Action('userSearch') userSearch: Function
adminSearch (query: string) {
this.userSearch({ search: query, pageSize: 200 }).then((res: Ajax.AjaxResponse) => {
this.users = res.data.items
})
}
}
</script>
複製程式碼
那麼使用 Composition API 後就能避免掉這個情況麼?答案肯定是能避免掉。
我們先看看,使用 Vue3.0 進行改造 setup
中的邏輯如何
import { ref, computed, Ref, watch, createComponent } from '@vue/composition-api'
import { userSearch, IOption } from '@/hooks/user-search'
export default createComponent({
setup (props, { emit, root }) {
let isFirstFoucs: Ref<boolean> = ref(false)
let showCheckbox: Ref<boolean> = ref(true)
// computed
// 當前選中選項
const chooseItems: Ref<string | string[]> = ref(computed(() => props.user))
// 選項去重(包含物件的情況)
const uniqueOptions = computed(() => {
const originArr: IOption[] | any = props.customSearch ? props.options : items.value
const newArr: IOption[] = []
const strArr: string[] = []
originArr.forEach((item: IOption) => {
if (!strArr.includes(JSON.stringify(item))) {
strArr.push(JSON.stringify(item))
newArr.push(item)
}
})
return newArr
})
// watch
watch(() => chooseItems.value, (val) => {
emit('update:user', val)
emit('change', val)
})
// methods
const remoteMethod = (query: string) => {
// 可丟擲去自定義,也可使用內部整合好的方法處理 remote
if (props.customSearch) {
emit('search', query)
} else {
handleUserSearch(query)
}
}
const handleFoucs = (event) => {
if (isFirstFoucs.value) {
return false
}
remoteMethod(event.target.value)
isFirstFoucs.value = true
}
const handleOptionClick = (item) => {
emit('option-click', item)
}
// 顯示勾選狀態,若是單選則無需顯示 checkbox
const isChecked = (value: string) => {
let checked: boolean = false
if (typeof chooseItems.value === 'string') {
showCheckbox.value = false
return false
}
chooseItems.value.forEach((item: string) => {
if (item === value) {
checked = true
}
})
return checked
}
return {
isFirstFoucs, showCheckbox, // ref
uniqueOptions, chooseItems, // computed
handleUserSearch, remoteMethod, handleFoucs, handleOptionClick, isChecked // methods
}
}
})
複製程式碼
然後我們再將可以重複使用的邏輯和資料抽離到 hooks/user-search.ts
中
import { ref, Ref } from '@vue/composition-api'
export interface IOption {
[key: string]: string
}
export function userSearch ({ root }) {
const items: Ref<IOption[]> = ref([])
const handleUserSearch = (query: string) => {
root.$store.dispatch('userSearch', { search: query, pageSize: 25 }).then(res => {
items.value = res.data.items
})
}
return { items, handleUserSearch }
}
複製程式碼
然後即可在元件中直接使用(當然你可以隨便重新命名)
import { userSearch, IOption } from '@/hooks/user-search'
export default createComponent({
setup (props, { emit, root }) {
const { items, handleUserSearch } = userSearch({ root })
}
})
複製程式碼
最後,避免掉命名衝突的後患,有做了業務整合後,我現在使用 <user-select>
元件只需這樣即可
<user-select :user.sync="user" />
複製程式碼
哇,瞬間清爽好多。
總結
文章到這,又要和各位小夥伴說再見了。
在嚐鮮 Vue3.0 期間,整體給我的感覺還是挺不錯的。如果你也想在業務中做一些 Vue3.0 新特性嘗試,不妨現在就開始試試吧。
這樣當 Vue3.0 真的釋出的那天,或許你已經對這塊的用法和原理比較熟了。
最後,如果文章對你有幫助的話,麻煩各位小夥伴動動小手點個贊吧 ~
前端交流群:731175396
前端公眾號:「合格前端」定期推送高質量博文,不定期進行免費技術直播分享,你想要的都在這了