背景
第一次晓得 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.js
import {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.js
import {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.vue
import {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.js
import 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 是两个 ref
const {x, y} = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中失落与组合式函数内状态的响应性连贯。与之相同,ref 则能够维持这一响应性连贯。
如果你更心愿以对象 property 的模式从组合式函数中返回状态,你能够将要返回的对象用 reactive()
包装,这样其中的 ref 会被主动解包,例如:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.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.js
import {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.js
function 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 得以确定以后沉闷的组件实例的条件。有能力对沉闷的组件实例进行拜访是必要的,以便:
- 能够在组合式函数中注册生命周期钩子
- 计算属性和监听器能够连贯到以后组件实例,以便在组件卸载时解决掉。
<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 公布!