探祕 Vue3.0 - Composition API 在真實業務中的嚐鮮姿式

前言

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-apijavascript

1、Composition API

首先,咱們得了解一下,Composition API 設計初衷是什麼?html

  1. 邏輯組合和複用
  2. 類型推導:Vue3.0 最核心的點之一就是使用 TS 重構,以實現對 TS 絲滑般的支持。而基於函數 的 API 則自然對類型推導很友好。
  3. 打包尺寸:每一個函數均可做爲 named ES export 被單獨引入,對 tree-shaking 很友好;其次全部函數名和 setup 函數內部的變量都能被壓縮,因此能有更好的壓縮效率。

咱們再來具體瞭解一下 邏輯組合和複用 這塊。前端

開始以前,咱們先回顧下目前 Vue2.x 對於邏輯複用的方案都有哪些?如圖vue

其中 Mixins 和 HOC 均可能存在 ①模板數據來源不清晰 的問題。java

而且在 mixin 的屬性、方法的命名以及 HOC 的 props 注入也可能會產生 ②命名空間衝突的問題react

最後,因爲 HOC 和 Renderless Components 都須要額外的組件實例來作邏輯封裝,會致使③無謂的性能開銷git

一、基本用法

OK,大體瞭解了 Composition API 設計的目的了,接下來,咱們來看看其基本用法。github

安裝vuex

npm i @vue/composition-api -S
複製代碼

使用npm

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>
複製代碼

二、組合函數

咱們已經瞭解到 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 使用組合函數作改造

在以後的實戰環節,還有不少真實場景呢,若是你等不及,能夠直接跳過去看第二章。

三、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 以及生命週期函數我這就不過多介紹了,想了解的直接查看 原文

四、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 {}
  }
})
複製代碼

2、業務實踐

目前爲止,咱們對 Vue3.0 的 Composition API 有了必定的瞭解,也清楚了其適合使用的一些實際業務場景。

而我在具體業務中又作了哪些嚐鮮呢?接下來,讓咱們一塊兒進入真正的實戰階段

一、列表分頁查詢

場景:我須要對業務中的列表作分頁查詢,其中包括頁碼、頁碼大小這兩個通用查詢條件,以及一些特定條件作查詢,好比關鍵字、狀態等。

在 Vue2.x 中,咱們的作法有兩種,如圖所示

  1. 最簡單的方式就是直接將通用查詢存儲到一個地方,須要使用查詢的地方直接引入便可,而後在頁面作一系列重複的操做,這個時候最考驗 Ctrl + CCtrl + V 的功力了。
  2. 將其通用的變量和方法抽離到 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() 返回;而且可以隨意重命名,因此也不會有命名空間衝突的問題;更不會有額外的組件實例帶來的性能損耗。

怎麼樣,有沒有點真香的感受了。

二、user-select 組件

場景:在我負責的業務中,有一個通用的業務組件,我稱之爲 user-select,它是一我的員選擇組件。如圖

關於改造先後的對比咱們先看張圖,好大體有個瞭解

在 Vue2.x 中,它通用的業務邏輯和數據並無獲得很好的處理,大體緣由和上面那個案例緣由差很少。

而後我每次想要使用的時候須要作如下操做,這充分鍛鍊了我 Ctrl + CCtrl + 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

前端公衆號:「合格前端」按期推送高質量博文,不按期進行免費技術直播分享,你想要的都在這了

參考文章:Vue Function-based API RFC

相關文章
相關標籤/搜索