近半年有幸参与了一个立异项目,由于没有任何前史包袱,所以选择了Vue3技能栈,总体来说感触如下:
• setup语法糖
• 能够经过Composition API(组合式API)封装可复用逻辑,将UI和逻辑分离,进步复用性,view层代码展现更明晰
• 和Vue3更调配的状况管理库Pinia,少去了许多配置,运用起来更便捷
• 构建东西Vite,基于ESM和Rollup,省去本地开发时的编译步骤,可是build打包时仍是会编译(考虑到兼容性)
• 必备VSCode插件Volar,支撑Vue3内置API的TS类型揣度,可是不兼容Vue2,假如需要在Vue2和Vue3项目中切换,比较费事
当然也遇到一些问题,最典型的就是呼应式相关的问题
呼应式篇
本篇首要凭借watch函数,理解ref、reactive等呼应式数据/状况,有爱好的同学能够检查Vue3源代码部分加深理解,
watch数据源能够是ref (包括核算特点)、呼应式目标、getter 函数、或多个数据源组成的数组
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、reactive
const reactivePerson = reactive({ name: '张三', info: { age: 18 } })
//改动特点,调查以下不同情形下的监听成果
nextTick(() => {
simplePerson.value = '李四'
person.value.name = '李四'
complexPerson.value.info.age = 20
reactivePerson.info.age = 22
})
//情形一:数据源为RefImpl
watch(simplePerson, (newVal) => {
console.log(newVal) //输出:李四
})
//情形二:数据源为'张三'
watch(simplePerson.value, (newVal) => {
console.log(newVal) //不合法数据源,监听不到且控制台告警
})
//情形三:数据源为RefImpl,可是.value才是呼应式目标,所以要加deep
watch(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也能够监听到
})
总结:
-
在Vue3中状况都是默认深层呼应式的(情形七),嵌套的引证类型在取值(get)时一定是回来Proxy呼应式目标
-
watch数据源为呼应式目标时(情形四、七、九),会隐式的创立一个深层侦听器,不需要再显示设置deep: true
-
情形三和情形八两种情况下,有必要显示设置deep: true,强制转换为深层侦听器
-
情形五和情形七对比下,虽然写法完全相同,可是假如特点值为根本类型时是监听不到的,尤其是ts类型声明为any时,ide也不会提示告警,导致排查问题比较吃力
-
所以准确的ts类型声明很重要,不然经常会出现莫名其妙的watch不收效的问题
-
ref值为根本类型时经过get\set拦截完成呼应式;ref值为引证类型时经过将.value特点转换为reactive呼应式目标完成;
-
deep会影响性能,而reactive会隐式的设置deep: true,所以只有明晰状况数据结构比较简单且数据量不大时运用reactive,其他一概运用ref
Props篇
设置默认值
type Props = {
placeholder?: string
modelValue: string
multiple?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择',
multiple: false,
})
双向绑定(多个值)
• 自定义组件
//FieldSelector.vue
type Props = {
businessTableUuid: string
businessTableFieldUuid?: 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>
<FieldSelector
v-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>
单向数据流
-
大部分情况下应该遵从【单向数据流】准则,制止子组件直接修正props,不然杂乱应用下的数据流将变得混乱,极易出现bug且难排查
-
直接修正props会有告警,可是假如props是引证类型,修正props内部值将不会有告警提示,因此应该有团队约定(第5条在外)
-
假如props为引证类型,赋值到子组件状况时,需要免除引证(第5条在外)
-
杂乱的逻辑,能够将状况以及修正状况的办法,封装成自定义hooks或许提升到store内部,避免props的层层传递与修正
-
一些父子组件本就严密耦合的场景下,能够答应修正props内部的值,能够削减许多杂乱度和工作量(需要团队约定固定场景)
逻辑/UI解耦篇
使用Vue3的Composition/组合式API,将某种逻辑涉及到的状况,以及修正状况的办法封装成一个自定义hook,将组件中的逻辑解耦,这样即便UI有不同的形状或许调整,只要逻辑不变,就能够复用逻辑。下面是本项目中涉及的一个实在案例-逻辑树组件,UI有2种形状且能够彼此转化。
• hooks部分的代码:useDynamicTree.ts
import { ref } from 'vue'
import { nanoid } from 'nanoid'
export type TreeNode = {
id?: string
pid: string
nodeUuid?: string
partentUuid?: string
nodeType: string
nodeValue?: any
logicValue?: any
children: 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 = 0
const pNode = find(tree.value, pid)
if (!pNode) return false
//嵌套联系不能超过3层
if (pNode.level && pNode.level > 2) return false
if (!node.id) {
node.id = nanoid()
}
if (pNode.nodeType === 'operator') {
pNode.children.push(node)
} else {
//假如父节点不是联系节点,则构建新的联系节点
const current = JSON.parse(JSON.stringify(pNode))
current.pid = pid
current.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) return
const index = pNode.children.findIndex((item) => item.id === id)
if (index === -1) return
pNode.children.splice(index, 1)
if (pNode.children.length === 1) {
//假如只剩下一个节点,则替换父节点(联系节点)
const [one] = pNode.children
Object.assign(
pNode,
{
...one,
},
{
pid: pNode.pid,
},
)
if (pNode.pid === '') {
pNode.id = 'root'
}
}
}
//切换逻辑联系:且/或
const toggleOperator = (id: string) => {
const node = find(tree.value, id)
if (!node) return
if (node.nodeType !== 'operator') return
node.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 += 1
const 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,
}
}
• 在不同组件中运用(UI1/UI2组件为递归组件,内部完成不再展开)
//组件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杂乱度,状况管理更明晰
• 定义一个store(非声明式):User.ts
import { computed, reactive } from 'vue'
import { defineStore } from 'pinia'
type UserInfo = {
userName: string
realName: string
headImg: string
organizationFullName: 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>