Feel the magic of Vue3

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:

setup syntactic sugar < script setup lang = "ts" > gets rid of writing declarative code, it is very smooth to use and improves a lot of efficiency
• The reusable logic can be encapsulated through the Composition API (combined API), which separates the UI from the logic, improves reusability, and makes the view layer code display clearer
Pinia , a state management library that is more compatible with Vue3, saves a lot of configuration and is more convenient to use
• The build tool Vite , based on ESM and Rollup, saves the compilation step during local development, but it will still be compiled when the build is packaged (considering compatibility)
The necessary VSCode plug-in Volar , which supports the TS type inference of Vue3's built-in API, but is not compatible with Vue2. If you need to switch between Vue2 and Vue3 projects, it will be troublesome

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:

1. In Vue3, the state is the default deep response type (scenario 7), and the nested reference type must return the Proxy response type object when getting the value (get).
2. When the watch data source is a responsive object (scenario 4, 7, 9), a deep listener will be implicitly created , and there is no need to explicitly set deep: true
3. In both scenarios 3 and 8, you must explicitly set deep: true to force conversion to a deep listener
4. Comparing Scenario 5 and Scenario 7, although the writing is exactly the same, if the attribute value is a basic type, it cannot be monitored, especially when the ts type is declared as any, the ide will not prompt an alarm, which makes troubleshooting more laborious
5. Therefore , the precise ts type declaration is very important, otherwise there will often be inexplicable problems that the watch does not take effect
6. When the ref value is a basic type, the response type is realized by get\set interception; when the ref value is a reference type, it is realized by converting the .value attribute into a reactive response object;
7. Deep will affect performance, and reactive will implicitly set deep: true, so only use reactive when the clear state data structure is relatively simple and the amount of data is not large, and use ref for everything else

Props

set default

type Props = {
  placeholder?: string
  modelValue: string
  multiple?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  placeholder: '请选择',
  multiple: false,
})

Two-way binding (multiple values)

Custom components
//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)
}
• Working with components
<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

1. In most cases, the principle of [one-way data flow] should be followed, and subcomponents are prohibited from modifying props directly, otherwise the data flow in complex applications will become chaotic, prone to bugs and difficult to troubleshoot
2. There will be a warning if the props are directly modified, but if the props is a reference type, there will be no warning prompt for modifying the internal value of the props, so there should be a team agreement (except for Article 5)
3. If props is a reference type, it needs to be dereferenced when assigning to the subcomponent state (except for item 5)
4. For complex logic, the state and the method of modifying the state can be encapsulated into custom hooks or promoted to the inside of the store to avoid layer-by-layer transfer and modification of props
5. In some scenarios where parent-child components are already tightly coupled, it is allowed to modify the internal value of props, which can reduce a lot of complexity and workload (requires the team to agree on a fixed scenario)

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.





 

Code for the hooks section: 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,
  }
}

Used in different components (UI1/UI2 components are recursive components, and the internal implementation is no longer expanded)
//组件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

Define a store (non-declarative): 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
  }
})

Used in components
<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>
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/6821269