Vue3项目实践总结

Tech 导读
本文主要介绍了Vue3中的一些新特性:如Composition API等,借助实际项目案例和场景,读者可以从本文中学到响应式数据、组件间Props、逻辑/UI解耦复用、Pinia状态管理等内容。
导读
近半年有幸参与了一个创新项目,由于没有任何历史包袱,所以选择了Vue3技术栈,总体来说感受如下:
•setup语法糖<script setup lang="ts">摆脱了书写声明式的代码,用起来很流畅,提升不少效率
•可以通过Composition API(组合式API)封装可复用逻辑,将UI和逻辑分离,提高复用性,View层代码展示更清晰
•和Vue3更搭配的状态管理库Pinia,少去了很多配置,使用起来更便捷
•构建工具Vite,基于ESM和Rollup,省去本地开发时的编译步骤,但是build打包时还是会编译(考虑到兼容性)
•必备VSCode插件Volar,支持Vue3内置API的TS类型推断,但是不兼容Vue2,如果需要在Vue2和Vue3项目中切换,比较麻烦。
当然也遇到一些问题,最典型的就是响应式相关的问题。
import{ ref, reactive, watch, nextTick }from'vue'//定义4种响应式数据/状态//1、ref值为基本类型const simplePerson =ref('张三')//2、ref值为引用类型,等价于:person.value = reactive({ name: '张三' })const person =ref({name:'张三'})//3、ref值包含嵌套的引用类型,等价于:complexPerson.value = reactive({ name: '张三', info: { age: 18 } })const complexPerson =ref({ name:'张三', info:{ age:18}})//4、reactiveconst reactivePerson =reactive({ name:'张三', info:{ age:18}})//改变属性,观察以下不同情景下的监听结果nextTick(()=>{simplePerson.value ='李四'person.value.name ='李四'complexPerson.value.info.age =20reactivePerson.info.age =22})//情景一:数据源为RefImplwatch(simplePerson,(newVal)=>{console.log(newVal)//输出:李四})//情景二:数据源为'张三'watch(simplePerson.value,(newVal)=>{console.log(newVal)//非法数据源,监听不到且控制台告警})//情景三:数据源为RefImpl,但是.value才是响应式对象,所以要加deepwatch(person,(newVal)=>{console.log(newVal)//输出:{name: '李四'}},{deep:true//必须设置,否则监听不到内部变化})//情景四:数据源为响应式对象watch(person.value,(newVal)=>{console.log(newVal)//输出:{name: '李四'}})//情景五:数据源为'张三'watch(person.value.name,(newVal)=>{console.log(newVal)//非法数据源,监听不到且控制台告警})//情景六:数据源为getter函数,返回基本类型watch(()=> person.value.name,(newVal)=>{console.log(newVal)//输出:李四})//情景七:数据源为响应式对象(在Vue3中状态都是默认深层响应式的)watch(complexPerson.value.info,(newVal, oldVal)=>{console.log(newVal)//输出:Proxy {age: 20}console.log(newVal === oldVal)//输出:true})//情景八:数据源为getter函数,返回响应式对象watch(()=> complexPerson.value.info,(newVal)=>{console.log(newVal)//除非设置deep: true或info属性被整体替换,否则监听不到})//情景九:数据源为响应式对象watch(reactivePerson,(newVal)=>{console.log(newVal)//不设置deep: true也可以监听到})
7.deep会影响性能,而reactive会隐式的设置deep: true,所以只有明确状态数据结构比较简单且数据量不大时使用reactive,其他一律使用ref。
设置默认值:
type Props = {placeholder?: stringmodelValue: stringmultiple?: boolean}const props = withDefaults(defineProps<Props>(), {placeholder: '请选择',multiple: false,})
双向绑定(多个值)
•自定义组件
//FieldSelector.vuetype Props = {businessTableUuid: stringbusinessTableFieldUuid?: string}const props = defineProps<Props>()const emits = defineEmits(['update:businessTableUuid','update:businessTableFieldUuid',])const businessTableUuid = ref('')const businessTableFieldUuid = ref('')// props.businessTableUuid、props.businessTableFieldUuid转为本地状态,此处省略//表切换const tableChange = (businessTableUuid: string) => {emits('update:businessTableUuid', businessTableUuid)emits('update:businessTableFieldUuid', '')businessTableFieldUuid.value = ''}//字段切换const fieldChange = (businessTableFieldUuid: string) => {emits('update:businessTableFieldUuid', businessTableFieldUuid)}}
•使用组件
<template><FieldSelectorv-model:business-table-uuid="stringFilter.businessTableUuid"v-model:business-table-field-uuid="stringFilter.businessTableFieldUuid"/></template><script setup lang="ts">import { reactive } from 'vue'const stringFilter = reactive({businessTableUuid: '',businessTableFieldUuid: ''})</script>
单向数据流
1. 大部分情况下应该遵循【单向数据流】原则,禁止子组件直接修改props,否则复杂应用下的数据流将变得混乱,极易出现bug且难排查;
2. 直接修改props会有告警,但是如果props是引用类型,修改props内部值将不会有告警提示,因此应该有团队约定(第5条除外);
3. 如果props为引用类型,赋值到子组件状态时,需要解除引用(第5条除外);
4. 复杂的逻辑,可以将状态以及修改状态的方法,封装成自定义hooks或者提升到store内部,避免props的层层传递与修改;
利用Vue3的Composition/组合式API,将某种逻辑涉及到的状态,以及修改状态的方法封装成一个自定义hook,将组件中的逻辑解耦,这样即使UI有不同的形态或者调整,只要逻辑不变,就可以复用逻辑。下面是本项目中涉及的一个真实案例-逻辑树组件,UI有2种形态且可以相互转化。
图1 可以相互转化的两种UI形态
•hooks部分的代码:useDynamicTree.ts
import { ref } from 'vue'import { nanoid } from 'nanoid'export type TreeNode = {id?: stringpid: stringnodeUuid?: stringpartentUuid?: stringnodeType: stringnodeValue?: anylogicValue?: anychildren: TreeNode[]level?: number}export const useDynamicTree = (root?: TreeNode) => {const tree = ref<TreeNode[]>(root ? [root] : [])const level = ref(0)//添加节点const add = (node: TreeNode, pid: string = 'root'): boolean => {//添加根节点if (pid === '') {tree.value = [node]return true}level.value = 0const pNode = find(tree.value, pid)if (!pNode) return false//嵌套关系不能超过3层if (pNode.level && pNode.level > 2) return falseif (!node.id) {node.id = nanoid()}if (pNode.nodeType === 'operator') {pNode.children.push(node)} else {//如果父节点不是关系节点,则构建新的关系节点const current = JSON.parse(JSON.stringify(pNode))current.pid = pidcurrent.id = nanoid()Object.assign(pNode, {nodeType: 'operator',nodeValue: 'and',// 重置回显信息logicValue: undefined,nodeUuid: undefined,parentUuid: undefined,children: [current, node],})}return true}//删除节点const remove = (id: string) => {const node = find(tree.value, id)if (!node) return//根节点处理if (node.pid === '') {tree.value = []return}const pNode = find(tree.value, node.pid)if (!pNode) returnconst index = pNode.children.findIndex((item) => item.id === id)if (index === -1) returnpNode.children.splice(index, 1)if (pNode.children.length === 1) {//如果只剩下一个节点,则替换父节点(关系节点)const [one] = pNode.childrenObject.assign(pNode,{...one,},{pid: pNode.pid,},)if (pNode.pid === '') {pNode.id = 'root'}}}//切换逻辑关系:且/或const toggleOperator = (id: string) => {const node = find(tree.value, id)if (!node) returnif (node.nodeType !== 'operator') returnnode.nodeValue = node.nodeValue === 'and' ? 'or' : 'and'}//查找节点const find = (node: TreeNode[], id: string): TreeNode | undefined => {// console.log(node, id)for (let i = 0; i < node.length; i++) {if (node[i].id === id) {Object.assign(node[i], {level: level.value,})return node[i]}if (node[i].children?.length > 0) {level.value += 1const result = find(node[i].children, id)if (result) {return result }level.value -= 1}}return undefined}//提供遍历节点方法,支持回调const dfs = (node: TreeNode[], callback: (node: TreeNode) => void) => {for (let i = 0; i < node.length; i++) {callback(node[i])if (node[i].children?.length > 0) {dfs(node[i].children, callback)}}}return {tree,add,remove,toggleOperator,dfs,}}
//组件1<template><UI1:logic="logic":on-add="handleAdd":on-remove="handleRemove":toggle-operator="toggleOperator"</UI1></template><script setup lang="ts">import { useDynamicTree } from '@/hooks/useDynamicTree'const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()const handleAdd = () => {//添加条件}const handleRemove = () => {//删除条件}const toggleOperator = () => {//切换逻辑关系:且、或}</script>
//组件2<template><UI2 :logic="logic":on-add="handleAdd":on-remove="handleRemove":toggle-operator="toggleOperator"</UI2></template><script setup lang="ts">import { useDynamicTree } from '@/hooks/useDynamicTree'const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()const handleAdd = () => { //添加条件 }const handleRemove = () => { //删除条件 }const toggleOperator = () => { //切换逻辑关系:且、或 }</script>
Pinia状态管理篇
将复杂逻辑的状态以及修改状态的方法提升到store内部管理,可以避免props的层层传递,减少props复杂度,状态管理更清晰。
import { computed, reactive } from 'vue'import { defineStore } from 'pinia'type UserInfo = {userName: stringrealName: stringheadImg: stringorganizationFullName: string}export const useUserStore = defineStore('user', () => {const userInfo = reactive<UserInfo>({userName: '',realName: '',headImg: '',organizationFullName: ''})const fullName = computed(() => {return `${userInfo.userName}[${userInfo.realName}]`})const setUserInfo = (info: UserInfo) => {Object.assgin(userInfo, {...info})}return {userInfo,fullName,setUserInfo}})
<template><div class="welcome" font-JDLangZheng><el-space><el-avatar :size="60" :src="userInfo.headImg ? userInfo.headImg : avatar"> </el-avatar><div><p>你好,{{ userInfo.realName }},欢迎回来</p><p style="font-size: 14px">{{ userInfo.organizationFullName }}</p></div></el-space></div></template><script setup lang="ts">import { useUserStore } from '@/stores/user'import avatar from '@/assets/avatar.png'const { userInfo } = useUserStore()</script>
读者可以思考讨论以下问题:
1. Vue2中可以做到逻辑与UI分离吗?如果有,有哪些缺点?
2. 什么场景下可以约定允许修改props内部的值?
求分享
求点赞
求在看