Author: JD Technology Niu Zhiwei
In the past six months, I was fortunate to participate in an innovative project. Since there is no historical burden, I chose the Vue3 technology stack. The overall feeling is as follows:
Of course, there are also some problems, the most typical ones are responsive related problems
Responsive articles
This article mainly uses the watch function to understand responsive data/states such as ref and reactive. Interested students can view the source code of Vue3 to deepen their understanding.
The watch data source can be a ref (including computed properties), a responsive object, a getter function, or an array of multiple data sources
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也可以监听到
})
Summarize:
Props
set default
type Props = {
placeholder?: string
modelValue: string
multiple?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择',
multiple: false,
})
Two-way binding (multiple values)
//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>
one-way data flow
Logic/UI Decoupling
Use Vue3's Composition/combined API to encapsulate the state involved in a certain logic and the method of modifying the state into a custom hook to decouple the logic in the component, so that even if the UI has different forms or adjustments, as long as the logic If it remains unchanged, the logic can be reused. The following is a real case involved in this project - the logic tree component, the UI has two forms and can be transformed into each other.
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,
}
}
//组件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 Status Management
Promote the state of complex logic and the method of modifying the state to the internal management of the store, which can avoid the layer-by-layer transmission of props, reduce the complexity of props, and make state management clearer
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>