最近公司新项目启用了vue3+element-plus+webpack(暂未使用vite
),最大的改变是CompositionAPI,这里记录一下使用的一点经验。
vue从2到3,最基本的方面是平时一些语法改变了,官方文档和网上一下文章都很详细,本文不再累述,这部分可以参考以下文章作为前置学习
Vue3中文文档
万字长文带你全面掌握Vue3
【Vue3官方教程】万字笔记 | 同步导学视频
这里我是用公司之前的一个vue2的OA系统模板作为基础来进行开发,GitHub上这种模板也有很多,比如很好用的
vue-element-admin基础模板
从vue2转为vue3进行开发,我是分了三步:
单纯把vue2的语法全部改成vue3的语法
使用CompositionAPI进行代码抽离与复用。因为一开始都会有个不熟悉到熟悉的过程,如果语法都还没有写熟练的话就去抽离代码也不会顺利哪去,同时还会给自己一个心理暗示:vue3这是什么破东西,还是vue2香。第一印象对任何事物都很重要
项目全面拥抱TypeScript。这个为啥放在最后了。因为我这个菜鸡对TypeScript还不熟练,如果一开始就加进去的话可能这个项目进度基本为0了。
先来说第一步:修改为vue3的语法多看些文档就行,如果嫌麻烦也可以看下上面推荐的两篇文章,这里面语法大概都了解之后就可以动工了。
这部分要注意的是vue3虽然兼容vue2的语法,但是有一些是非兼容更新
V3迁移指南
还有如果之前的项目有使用vuex
vue-router
的话记得看最新的文档
vue-router
vuex
vuex和vue-router文档是英文的,大佬们英语八级水平肯定没啥问题。但是考虑到可能会有像我这种懒癌患者,在这里提一下比较常用的;
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
setup() {
// store就可以当做vue2中的this.$store。然后之前怎么取值调用方法现在就继续
const store = useStore()
// 例如:
store.dispatch('xxx')
store.commit('xxx')
const gettersData = store.getters.xxx
// route相当于vue2的this.$route,如果想获取地址栏参数之类的可以用这个
const route = useRoute()
const xxx = route.query.xxx
// router相当于vue2的this.$router,跳转路由的时候调用这个
const router = useRouter()
router.push()
}
如果对页面逻辑等不做修改只是修改语法的话,这部分大概几天时间就熟悉了
当我按照上面步骤把页面逻辑写完之后,嚯,提神醒脑。
某个逻辑不太多的页面一个setup函数就给整了3 4百行,数据,方法,监听等等混合在一起,你中有我,我中有你,代码是甜蜜了,给我整的头大了好几圈。而且这还是逻辑不太多,要是复杂一点的页面岂不是上千行代码的setup等待我去宠幸?亚历山大啊
从这里就可以看出vue2的一个好处:下限高。就算我的基础不太好,也能把methods写在methods里面,watch写在watch里面,这些vue2的选项一定程度上让代码变得清晰,如果仅仅是把vue2的语法改成了vue3,没有使用CompositionAPI抽取功能逻辑的话,在我看来是真的还不如用2,因为确实太乱了。
这里根据我的项目从全局逻辑复用和组件内的逻辑抽取两方面举两个示例来记录下我的做法。这里代码和逻辑会展示的尽可能简单,具体写的思路和想法会描述比较多。
写的过程中可能会有点疑惑:vue3里面有CompositionAPI可以进行复用,而之前我也有的地方是通过组件进行的复用,那么这个东西我是要抽成组件呢还是抽成CompositionAPI呢?
简单区分的话,可以看你要处理的这部分内容是否有UI结构需要复用。如果有的话优先考虑抽离组件。没有的话就可以考虑CompositionAPI了。(大多数是这样考虑,实际项目中还是要根据实际情况进行取舍判断);其实CompositionAPI是这个意思:某些逻辑需要进行复用,比如奥特曼们都需要打怪兽这个行为是共同的,这样就可以抽离CompositionAPI
上面说的是根据是否公用来进行抽离CompositionAPI,这个也是我们使用它的初衷,但是实际项目中还是会有问题:如果我一个页面的逻辑有很多,同时他们中还真的是没有要复用的逻辑,用CompositionAPI好像没有必要,但是不做任何操作的话这个setup确实又臭又长。如果是一气呵成的写还好,因为之前写了什么还有点记忆,但凡时间稍长一点,回过头重新看真的是折磨。首先不说逻辑复不复杂,就是这么长的一个函数就看不下去。所以从这方面来说进行抽离也是有必要的。
所以总结来说要抽取CompositionAPI主要考虑两方面:逻辑复用和方便阅读维护
抽离逻辑代码一般遵循单一功能原则,一个逻辑js只负责一个功能,同时自己管理自己的数据,也就是更改数据的行为也定义在这里,引用的时候调用这个改变数据的行为,而不是直接暴力修改内部数据。
项目中有一部分功能逻辑是贯穿全局,有很多地方使用,这样的逻辑就可以抽离成全局的CompositionAPI。这些逻辑文件我会在src/文件夹下建一个hooks文件夹和全局组件components放在一起。文件夹为什么叫hooks
也是去年写react遗留下的习惯。另一方面是CompositionAPI
的话太长了,如果是use
感觉不太明确,命名问题看自己习惯就好。
我这里用我项目中的一个的全局逻辑作为例子方便大家理解。
全局的公共逻辑方面,作为OA系统,项目里面有很多功能模块都附带新增和编辑等。而我司项目都是用弹框来承载这部分form表单。
这里就可以提取公共的逻辑:1. 打开弹框 2. 关闭弹框 3. 关闭弹框前吧form数据清空 4. 打开编辑弹框时候赋初始值
这些功能代码可能不多,但是因为公用,所以可以抽取,因为以后维护的话会很方便,而且虽然代码少,但是如果要每次都写这些也确实挺烦的。
这里额外提一句:为什么这种弹框不抽离成组件?因为之前说有UI结构相同的话就抽离成组件。
我这里是这么考虑的:如果抽成一个组件,那么可以说这个弹框的UI元素们都是需要在使用的时候确定——比如标题、宽高、按钮位置与文字、弹框内部样式等等。这样考虑下来就会发现如果我抽成组件好像和直接用element-plus的组件没啥区别,所以这里就考虑只抽取这些逻辑来复用
这里详细解释一下
// vue3中很方便的就在这里,不需要在vue文件我也可以非常方便的引入我想用的api。
import { ref, watchEffect } from 'vue'
import _ from 'lodash'
// 整体导出是一个函数,这个函数一般要做到功能单一同时自己维护自己内部的数据
// formData为当前form绑定的数据源,editFormData为编辑的时候获取的form初始值,formRef为dialog内的form的ref对象,可以用来清空form和移除校验结果。
export default function useDialogForm({ formData, formRef, editFormData } = {}) {
// 这里我定义的变量是在函数内部定义,也有在函数外部定义的,从语法角度来讲都可以,但是要看自己的需求然后来决定具体定义在哪里。
// 如果定义在外部的话我使用这个函数会做到数据共享。也就是如果我这个页面有两个弹框的话,他们共享是否展示这个变量是不是就不对劲了。
const dialogFlag = ref(false)
function openDialog() {
dialogFlag.value = true
}
function closeDialog() {
formRef.value.resetFields()
dialogFlag.value = false
}
function beforeClose(done) {
done()
formRef.value.resetFields()
}
function openEditDialog() {
dialogFlag.value = true
nextTick(() => {
formData.value = _.cloneDeep(editFormData.value)
})
}
return {
dialogFlag,
openDialog,
closeDialog,
beforeClose,
openEditDialog
}
}
这样这部分逻辑就抽离完成了。如果某个页面需要一个弹框,可以这样使用:
<template>
<el-button type="primary" @click="openDialog">新增</el-button>
<el-button type="text" @click="edit(当前行数据)">编辑</el-button>
<el-dialog v-model="dialogFlag" title="xxx" width="60%" :before-close="beforeClose">
<el-form ref="addCityRef" :model="form" label-width="140px">
....
</el-form>
</el-dialog>
</template>
<script>
import useDialogForm from '@/hooks/useDialogForm'
setup() {
const {dialogFlag, openDialog, closeDialog, openEditDialog} = openEditDialog
const editData = ref({})
function edit(data) {
editData.value = data
}
return {
dialogFlag,
openDialog,
closeDialog,
beforeClose,
openEditDialog,
edit
}
}
</script>
组件内的hook大多数都是为了抽取不同的逻辑进行分别管理,虽然可以复用,但是实际上不一定会复用。这里以下面这个非常常见的页面为例,前提是里面所有内容都没有抽离组件
普通的页面逻辑我都会在当前文件夹下直接新建文件然后引入,这里如果嫌不清晰的话可以在当前文件夹下再建一个hook文件夹。
这里是为了说明我们组件内的hook应该进行怎样的一种考虑来抽取不同逻辑。所以为了下面内容好接受一些还望大家记得前提:这里所有内容没有被抽离成组件。
看到红框的内容一开始的印象肯定是这不就是一体的嘛——展示数据。然后没了。
看起来好像是没得分解,其实认真思考一下还是可以发现他二者是有区别的:表格获取和展示数据,分页描述数据范围。
平时我们思考的一种比较简单的方式是:如果我去掉其中一部分,剩下的是否还能正常使用。按照上面的分析来看,这二者确实是可以分离的。没有分页功能表格自己也可以展示获取数据;获取某个范围内的数据也不一定非要展示在表格。
下面还是根据代码来看下具体怎么进行抽取操作,伪代码展示。
表格部分useGetTableList.js:
export default function useGetTableList() {
const loading = ref(false)
// tableData是表格数据源,这里作为复杂类型为什么我还是用了ref,可以看下面
const tableData = ref([])
// pageparams是分页参数,searchData是调用接口需要的其他参数,比如是表格上方搜索项你填入的内容
async function getList({pageParams = {}, searchData = {}} = {}) {
// data是调用接口实际需要的参数
const data = {
...pageParams,
...searchData
}
const res = await xxx(data)
loading.value = false
if(!res || res.data) return
// 这里就可以看到我为了上面用的ref,用reactive定义的话返回的是一个响应式副本,下面用tableData = xx的话变量重新指向,就没有任何响应式了。
// 如果是上面通过reactive定义,但是用tableData.name = xx,tableData.age = xx这种方式是没有问题的,但是这样对比下面的方式就显得太繁琐了。
tableData.value = res.data
...
}
return {
loading,
getList,
tableData
}
}
// 这里留个思考,上面的代码是否还有可以进行抽离的内容
分页usePageParams.js
export default function usePageParams() {
const currentPage = ref(1)
const currentSize = ref(10)
const pageParams = computed(() => ({
page: current.value,
size: current.value
}))
// 当前条数发生改变
function sizeChange(val) {
currentSize.value = val
//这里可能会疑惑:我分页改变了一般还需要调用接口,这里怎么调用,不要着急,调用接口不是写在这里,继续往下看。
}
// 当前页码发生改变
function currentChange(val) {
currentPage.value = val
}
return {
sizeChange,
currentChange,
currentPage,
currentSize
}
}
具体功能页面index.vue
<template>
<el-table
v-loading="loading"
:data="tableData">
...
</el-table>
<el-pagination
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="currentSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script>
import useGetTableList from './useGetTableList'
import usePageParams from './usePageParams'
export default {
setup() {
// 获取loading,getList,tableData
const {loading,getList,tableData} = useGetTableList()
// 获取sizeChange, currentChange, currentSize, currentPage, pageParams
const {
sizeChange,
currentChange,
currentSize,
currentPage,
pageParams
} = usePageParams()
// 当前条数发生改变
function handleSizeChange(val) {
// 这里调用usePageParams中的sizeChange改变currentSize
sizeChange(val)
//这里调用useGetTableList中的调用接口方法,同时吧分页改变之后的页码参数传入
getList({ pageParams: pageParams.value })
}
// 当前页码发生改变
function handleCurrentChange(val) {
currentChange(val)
getList({ pageParams: pageParams.value })
}
return {
....太多了这里就不写了。
}
}
}
</script>
这部分很惭愧,暂时还没有加入进去,所以如果各位大佬有关于vue3+typescript好的学习资源还望留言
到这里也就结束了,vue3的 CompositionAPI我觉得还是很好用很有潜力的,希望大家能有一些收获。分享这点使用总结也是希望能抛砖引玉,如果各位有好的想法与建议还望不吝赐教