背景

第一次晓得composition api是从vue3的RFC提案中据说的,印象最深的是options api和composition api的比照图:

这种图片很清晰的形容出composition api的劣势:可能把雷同逻辑关注点都归为一组,不必在不同的选项中来回滚动切换,这样能达到更好的逻辑复用的成果。这点对于我有很大的吸引力,很多简单业务中,template的内容很少,大部分代码都是js逻辑,性能比较复杂,为了更好的代码复用,采纳了mixins封装了很多专用逻辑,但mixins的缺点很显著,mixins外面应用的变量或办法在vue组件中复用须要重复横跳,但过后受限于vue2,只能达到这种水平,如果当初从新做,应用composition api能达到更好的逻辑复用作用。

composition api

首先先相熟一下composition api,有哪些api,目前只须要把握加粗的局部,就能够体验组合式api的魅力了,其余的api等须要用到了再通过vue3的官网文档深刻学习。

reactive 用来将对象转变为响应式的,与vue2的observable相似,ref用来取得独自或者为根底数据类型取得响应性。为什会会有两个取得响应性的api呢,稍后咱们将具体阐明。computed、watch,provide、inject不必狐疑和vue2中做的是一样的事件。你肯定留神到上面这些加了on结尾的生命周期钩子函数,没错在组合式api中,这就是他们注册的形式。然而为什么不见了beforeCreate和created呢?因为setup就是在这个阶段执行的,而setup就是关上组合式api世界的大门。你能够把setup了解为class的constructor,在vue组件的创立阶段,把咱们的相干逻辑执行,并且注册相干的副作用函数。

当初咱们说回ref和reactive。

  • reactive在官网中的阐明,承受一个对象,返回对象的响应式正本。
  • ref在官网中的形容"承受一个外部值并返回一个响应式且可变的 ref 对象。ref 对象具备指向外部值的单个 property.value"。

听着很绕口,简略来讲就是reactive能够为对象创立响应式;而ref除了对象,还能够接管根底数据类型,比方string、boolean等。   那为什么会有这种差别呢?在vue3当中响应式是基于proxy实现的,而proxy的target必须是简单数据类型,也就是寄存在堆内存中,通过指针援用的对象。其实也很好了解,因为根底数据类型,每一次赋值都是全新的对象,所以根本无法代理。 那么如果咱们想获得简略类型的响应式怎么办呢?这时候就须要用到ref。

class RefImpl<T> {  private _value: T  public readonly __v_isRef = true  constructor(private _rawValue: T, public readonly _shallow = false) {    this._value = _shallow ? _rawValue : convert(_rawValue)  }  get value() {    track(toRaw(this), TrackOpTypes.GET, 'value')    return this._value  }  set value(newVal) {    if (hasChanged(toRaw(newVal), this._rawValue)) {      this._rawValue = newVal      this._value = this._shallow ? newVal : convert(newVal)      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)    }  }}...const convert = <T extends unknown>(val: T): T =>  isObject(val) ? reactive(val) : val

ref通过创立外部状态,将值挂在value上,所以ref生成的对象,要通过value应用。重写get/set取得的监听,同时对对象的解决,也依赖了reactive的实现。 由此,ref并不只是具备对根本数据类型的响应式解决能力,他也是能够解决对象的。
所以我认为ref和reactive的辨别并不应该只是简略/简单对象的辨别,而是应该用编程思维辨别的。咱们应该防止,把reactive 当作data在顶部将所有变量申明的想法,而是应该联合具体的逻辑性能,比方一个管制灰度的Flag那他就因该是一个ref,而分页当中的页码,pageSize,total等就应该是一个reactive申明的对象。也就是说一个setup当中能够有多出响应变量的申明,而且他们该当是与逻辑紧密结合的。

接下来我先用一个分页的性能,用选项式和组合式api给大家比照一下:

// options api<template>    <div>        <ul class="article-list">            <li v-for="item in articleList" :key="item.id">                <div>                    <div class="title">{{ item.title }}</div>                    <div class="content">{{ item.content }}</div>                </div>            </li>        </ul>        <el-pagination            @size-change="handleSizeChange"            @current-change="handleCurrentChange"            :current-page="currentPage"            :page-sizes="pageSizes"            :page-size="pageSize"            layout="total, sizes, prev, pager, next, jumper"            :total="total"        >        </el-pagination>    </div></template><script>import { getArticleList } from '@/mock/index';export default {    data() {        return {            articleList: [],            currentPage: 1,            pageSizes: [5, 10, 20],            pageSize: 5,            total: 0,        };    },    created() {        this.getList();    },    methods: {        getList() {            const param = {                currentPage: this.currentPage,                pageSizes: this.pageSizes,                pageSize: this.pageSize,            };            getArticleList(param).then((res) => {                this.articleList = res.data;                this.total = res.total;            });        },        handleSizeChange(val) {            this.pageSize = val;            this.getList();        },        handleCurrentChange(val) {            this.currentPage = val;            this.getList();        },    },};</script>

下面就是咱们相熟到不能在相熟的分页流程,在data中申明数据,在method中提供修分页的办法。当咱们用composition-api实现的时候他面成了上面的样子:

<script>import { defineComponent, reactive, ref, toRefs } from "@vue/composition-api";import { getArticleList } from "@/mock/index";export default defineComponent({  setup() {    const page = reactive({      currentPage: 1,      pageSizes: [5, 10, 20],      pageSize: 5,      total: 0,    });    function handleSizeChange(val) {      page.pageSize = val;      getList();    }    function handleCurrentChange(val) {      page.currentPage = val;      getList();    }    const articleList = ref([]);    function getList() {      getArticleList(page).then((res) => {        articleList.value = res.data;        page.total = res.total;      });    }    getList();    return {      ...toRefs(page),      articleList,      getList,      handleSizeChange,      handleCurrentChange,    };  },});</script>

这是以composition-api的形式实现的分页,你会发现本来的data,method,还有申明周期等选项都不见了,所有的逻辑都放到了setup当中。通过这一个简略的例子,咱们能够发现本来扩散在各个选项中的逻辑,在这里失去了聚合。这种变动在简单场景下更为显著。在简单组件中,这种状况更加显著。 而且当逻辑齐全汇集在一起,这时候,将他们抽离进去,而且抽离逻辑的能够在别处复用,至此hook就造成了。

hook状态的分页组件:

// hooks/useArticleList.jsimport { ref } from "@vue/composition-api";import { getArticleList } from "@/mock/index"; // mock ajax申请function useArticleList() {  const articleList = ref([]);  function getList(page) {    getArticleList(page).then((res) => {      articleList.value = res.data;      page.total = res.total;    });  }  return {    articleList,    getList,  };}export default useArticleList;// hooks/usePage.jsimport { reactive } from "@vue/composition-api";function usePage(changeFn) {  const page = reactive({    currentPage: 1,    pageSizes: [5, 10, 20],    pageSize: 5,    total: 0,  });  function handleSizeChange(val) {    page.pageSize = val;    changeFn(page);  }  function handleCurrentChange(val) {    page.currentPage = val;    changeFn(page);  }  return {    page,    handleSizeChange,    handleCurrentChange,  };}export default usePage;// views/List.vueimport { defineComponent, toRefs } from "@vue/composition-api";import usePage from "@/hooks/usePage";import useArticleList from "@/hooks/useArticleList";export default defineComponent({  setup() {    const { articleList, getList } = useArticleList();    const { page, handleSizeChange, handleCurrentChange } = usePage(getList);    getList(page);    return {      ...toRefs(page),      articleList,      getList,      handleSizeChange,      handleCurrentChange,    };  },});

在我的项目中的应用

在vue2中也能应用composition api,只须要引入@vue/composition-api这个包,而且通过测试,对于IE11的兼容性没有问题,那就能够释怀的在我的项目中应用了,所以在金融综合安防V1.7.0的二期中,咱们引入了composition api。

// main.jsimport VueCompositionAPI from '@vue/composition-api'Vue.use(VueCompositionAPI)

首先想到的是典型列表页面的增删改查能够封装成hooks来达到复用成果,之前每次写这种curd业务,一些逻辑反复写,比方删除、分页跳转等,尽管逻辑简略,但还是须要耗一些工夫的,这次罗唆好好封装一下,当前再也不必写这种没技术含量的代码了。

上面举一些我的项目中封装的curd的逻辑:

1)列表分页hook

参考下面一节

2)增加hook

在增加/编辑页面,保留前进行表单校验,调用保留接口,勾销返回上一级页面,这些都是通用逻辑,波及数据:

const loading = ref(false) // 保留的loading标识

波及的性能函数:

// 保留函数,先进行表单校验,通过hook传入保留的接口、传参const handleSave = () => {    form.value.validate(async valid => {        if (valid) {            loading.value = true            try {                await api.save(params && params.formModel)                root.$message.success('保留胜利')            } finally {                loading.value = false            }        }    })}// 勾销const handleCancel = () => {    root.$router.go(-1)}

在应用的时候只须要调用hook函数,传入列表保留接口、保留传参,在模板中应用同名性能函数。

3)列表hook

列表中个别存在操作项(增加、编辑、详情、删除),下面筛选项反对查问和重置,能够集成后面的列表分页hook,实现残缺的列表通用性能,波及的性能函数:

// 重置筛选项const handleReset = () => {    search.value.getForm().resetFields()    handleSearch()}// 删除,反对单个删除和批量删除,反对不同的惟一标识字段,是否须要二次提醒const handleDeleteData = async (row, id = 'id', hintFlag = true) => {    let data = []    if (row) {        data = [row]    } else {        data = checkedList.value    }    const deleteAndUpdateData = async () => {        const ids = data.map(item => item[id])        await api.delete(ids)        root.$message.success('删除胜利!')        handleSearch()    }    if (hintFlag) {        root.$confirm('是否确定删除?', {            confirmButtonText: '确定',            cancelButtonText: '勾销',            onConfirm: () => {                deleteAndUpdateData()            },            onCancel: () => {                console.log('勾销删除')            }        })    } else {        deleteAndUpdateData()    }}// 跳转到增加页面,反对不同的增加页路由const handleAdd = () => {    root.$router.push({        path: params.addPath    })}// 跳转到编辑页面,反对不同的惟一标识,路由与增加统一const handleEdit = (row, id = 'id') => {    root.$router.push({      path: params.addPath,      query: {        [id]: row[id]      }    })}// 跳转到详情页面,反对不同的路由和惟一标识const handleDetail = (row, id = 'id') => {    root.$router.push({      path: params.detailPath,      query: {        [id]: row[id]      }    })}// 导出const handleExport = () => {    window.location.href = api.exportUrl + `?${qs.stringify(params.searchParam)}`}

在应用的时候只须要调用hook函数,传入删除接口、惟一字段标识、是否须要二次确认,在模板中应用同名性能函数,目前接口调用只依照大部分接口传参习惯,如果有对应后端接口传参不统一,能够去沟通让其保持一致,很多这种通用的性能须要去推动接口标准,等整套hooks稳固之后会去推动后端恪守这套标准。

4)详情hook

编辑和详情页都存在从路由参数中获取惟一标识,调接口获取详情数据,能够封装成详情hook:

const identifier = root.$route.query[id] // 惟一标识const getDetail = async () => {    const { data } = await api.detail(identifier)    return data}

在应用的时候只须要调用hook函数,传入惟一字段标识、详情接口。

vue的官网也基于composition api提取了函数汇合VueUse。

当然composition api 不仅仅是用于复用公共逻辑,还能用于更好的代码组织抽取组合式函数:

抽取组合式函数不仅是为了复用,也是为了代码组织。随着组件复杂度的增高,你可能会最终发现组件多得难以查问和了解。组合式 API 会给予你足够的灵活性,让你能够基于逻辑问题将组件代码拆分成更小的函数:

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

在某种程度上,你能够将这些提取出的组合式函数看作是能够互相通信的组件范畴内的服务。

最佳实际

命名

组合式函数约定用驼峰命名法命名,并以“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 对象,这样该函数在组件中解构之后仍能够放弃响应性:

// x 和 y 是两个 refconst { x, y } = useMouse()

从组合式函数返回一个响应式对象会导致在对象解构过程中失落与组合式函数内状态的响应性连贯。与之相同,ref 则能够维持这一响应性连贯。

如果你更心愿以对象 property 的模式从组合式函数中返回状态,你能够将要返回的对象用 reactive() 包装,这样其中的 ref 会被主动解包,例如:

const mouse = reactive(useMouse())// mouse.x 链接到了原来的 x refconsole.log(mouse.x)Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

hook封装性能准则

hook封装的性能尽量繁多,在组件中应用通过组合的形式来应用,如果hook外面的性能逻辑简单,那就失去了拆分成hook的意义了,通过组合的形式应用能更清晰展现应用过程,更好查看代码和定位问题了,通过解构间接裸露给template的变量和办法只能通过搜寻的形式去查看了。

副作用

在组合式函数中确实能够执行副作用 (例如:增加 DOM 事件监听器或者申请数据),但请留神以下规定:

  • 如果你在一个利用中应用了服务器端渲染 (SSR),请确保在后置加载的申明钩子上执行 DOM 相干的副作用,例如:onMounted()。这些钩子仅会在浏览器中应用,因而能够确保能拜访到 DOM。
  • 确保在 onUnmounted() 时清理副作用。举个例子,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像咱们在 useMouse() 示例中看到的一样)。当然也能够应用一个组合式函数来主动帮你做这些事。

限度

hook中的异步问题

因为hook实质上就是函数,所以灵便度十分高,尤其是在波及异步的逻辑中,思考不全面就很有可能造成很多问题。 hook是能够笼罩异步状况的,然而必须在setup当中执行时返回无效对象不能被阻塞。咱们总结了两种异步的格调,通过一个简略的hook为例:

  • 内部没有其余依赖,只是交付渲染的响应变量

对于这种状况,能够通过申明、对外裸露响应变量,在hook中异步批改的形式

// hooks/useWarehouse.jsimport { reactive,toRefs } from '@vue/composition-api';import { queryWarehouse } from '@/mock/index';  // 查问仓库的申请import getParam from '@/utils/getParam'; // 取得一些参数的办法function useWarehouse(admin) {    const warehouse = reactive({ warehouseList: [] });    const param = { id: admin.id, ...getParam() };    const queryList = async () => {        const { list } = await queryWarehouse(param);        list.forEach(goods=>{        // 一些逻辑...          return goods        })        warehouse.warehouseList = list;    };    return { ...toRefs(warehouse), queryList };}export default useWarehouse;// components/Warehouse.vue<template>    <div>        <button @click="queryList">queryList</button>        <ul>            <li v-for="goods in warehouseList" :key="goods.id">                {{goods}}            </li>        </ul>    </div></template><script>import { defineComponent } from '@vue/composition-api';import useWarehouse from '@/hooks/useWarehouse';export default defineComponent({    setup() {        // 仓库保管员        const admin = {            id: '1234',            name: '张三',            age: 28,            sex: 'men',        };        const { warehouseList, queryList } = useWarehouse(admin);        return { warehouseList, queryList };    },});</script>
  • 内部具备依赖,须要在应用侧进行加工的

能够通过对外裸露Promise的形式,使内部取得同步操作的能力
在原有例子上拓展,减少一个须要解决的更新工夫属性

// hooks/useWarehouse.jsfunction useWarehouse(admin) {    const warehouse = reactive({ warehouseList: [] });    const param = { id: admin.id, ...getParam() };    const queryList = async () => {        const { list, updateTime } = await queryWarehouse(param);            list.forEach(goods=>{        // 一些逻辑...          return goods        })        warehouse.warehouseList = list;        return updateTime;    };    return { ...toRefs(warehouse), queryList };}export default useWarehouse;// components/Warehouse.vue<template>    <div>       ...        <span>nextUpdateTime:{{nextUpdateTime}}</span>    </div></template><script>...import dayjs from 'dayjs';export default defineComponent({    setup() {                ...       // 仓库保管员        const admin = {            id: '1234',            name: '张三',            age: 28,            sex: 'men',        };        const { warehouseList, queryList } = useWarehouse(admin);        const nextUpdateTime = ref('');        const interval = 7; // 假如更新仓库的工夫距离是7天        const queryHandler = async () => {            const updateTime = await queryList();            nextUpdateTime.value = dayjs(updateTime).add(interval, 'day');        };        return { warehouseList, nextUpdateTime, queryHandler };    },});</script>

this的问题

因为setup是beforecreate阶段,不能获取到this,尽管通过setup的第二个参数context能够取得一部分的能力,但如果咱们想要操作诸如路由,vuex这样的能力就收到了限度,最新的router@4、vuex@4都提供了组合式的api。因为vue2的底层限度咱们没有方法应用这些hook,尽管通过getCurrentInstance能够取得组件实例,下面挂载的对象,但因为composition-api中的响应式尽管底层原理与vue雷同都是通过object.defineproperty改写属性实现的,然而具体实现形式存在差别,所以载setup当中与vue原生的响应式并不互通。这也导致即便咱们拿到了相应的实例,也没有方法监听它们的响应式。如果有这方面的需要,只能在选项配置中应用。

组合式函数在 <script setup>setup() 钩子中,应始终被同步地调用。在某些场景下,你也能够在像 onMounted() 这样的生命周期钩子中应用他们。

这些是 Vue 得以确定以后沉闷的组件实例的条件。有能力对沉闷的组件实例进行拜访是必要的,以便:

  1. 能够在组合式函数中注册生命周期钩子
  2. 计算属性和监听器能够连贯到以后组件实例,以便在组件卸载时解决掉。

<script setup> 是惟一在调用 await 之后仍可调用组合式函数的中央。编译器会在异步操作之后主动为你复原以后沉闷的组件实例。

在选项式 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() 裸露的 property 能够在通过 `this` 拜访到    console.log(this.x)  }  // ...其余选项}

只能option api拜访composition api抛出的值,反过来就不行,所以不倡议composition api和options api混用。

不能共享一个实例

每一个调用 useMouse() 的组件实例会创立其独有的 x、y 状态拷贝,因而他们不会相互影响。如果你想要在组件之间共享状态,得应用状态治理(pinia)

template用到的变量或办法

如果应用...toRef(useData())这种写法间接解构裸露到template的变量和办法,查看无奈间接点击跳转,这个mixins也有一样的问题,都须要通过搜寻来查看,这边倡议hook在setup中进行组合应用,不要间接在return中解构应用,就算没有被其余hook或逻辑应用到,也倡议先解构一次,return再返回。

总结

通过vue3组合式、与hook的能力。咱们的代码格调有了很大的转变,逻辑更加聚合、纯正。复用性能力失去了晋升。我的项目整体的维护性有了显著的进步。这也是咱们即使在vue2的我的项目中,也要应用composition-api引入vue3新个性的起因。composition api对于开发的业务逻辑拆分能力有比拟高要求,如果拆分不好,很容易编写出面条型代码,也是反向推动前端人员对于业务须要更新相熟。

参考链接

  • vue3官网文档

    本文由博客一文多发平台 OpenWrite 公布!