Skip to content

vue3

  • 在vue3中,推荐setup写法,不推荐option写法,setup 写法更简洁,更易读,没有this指向问题,hooks提高代码逻辑复用性,也更易维护。
  • 在vue3中,重复性代码,多封装,在后续修改时,只需要修改封装的那一部分代码即可更易维护。
  • 不要一个文件几千行,读着累,也就更容易堆成 💩山。
  • 写业务不建议jsx,vue针对jsx的diff优化没有template好。
  • propsstore不要用toReftoRefs解构使用,解构会影响可读性。

不要使用prototype

ts
// eslint-disable-next-line no-extend-native
Date.prototype.format = function () { 
  // xxxxx
} 

const date = formatDate(new Date()) 

function formatDate(date: Date) { 
  // 时间处理逻辑
} 

// vue3
const app = createApp() 
app.config.globalProperties.$formatDate = formatDate 
  • 不要在对象和方法的原型上定义属性和方法,因为所有实例都共享同一个原型对象,保不住被人也在项目里面某个角落也修改了prototype上同名的属性和方法,建议用纯函数
  • vue3中,app.config.globalProperties,在vue2中是Vue.prototype,也不建议使用,不容易追踪调用来源,同样用函数直接调用的方式代替。

hooks(composables)

// hooks/useQuery.ts

ts
export type Query = Record<string, any>

/**
 * 获取query参数
 * @returns Ref<T>
 *
 * @example
 * ```ts
 * const query = useQuery()
 * const query = useQuery<{ id: string }>()
 * ```
 */
export function useQuery<T extends Query>() {
  const query = shallowRef({} as T)

  onLoad((data) => {
    if (typeof data === 'object') {
      for (const key in data) {
        if (typeof data[key] === 'string') {
          data[key] = decodeURIComponent(data[key])
        }
      }
    }
    query.value = data as T
  })

  return readonly(query)
}
  • 文件放在hooks目录下
  • 文件名和函数名一致,以use开头
  • vue里面又叫composables,意思是可复用的组合函数,react里面叫hookscomposables单词太复杂了,所以大家还是叫hooks顺口

store(pinia)

// store/count.ts

ts
import { defineStore } from 'pinia'

export const useCountStore = defineStore(
  'count',
  () => {
    const count = ref(1)

    function add() {
      count.value++
    }

    return { count, add }
  },
)
  • 推荐使用piniavuex不好用,pinia有更好的类型提示
  • 文件放在store目录下
  • 文件名用 count,store名用 useCountStore, 增加可读性
  • defineStore第一个参数传id,第二个参数传一个函数,返回一个对象,在这函数中写法和setup类似,且在函数内定义的方法,变量在同一作用域下,减少开发者心智负担

computed

js
import { computed, ref } from 'vue'

const num = ref(1) 
const doubleNum = ref(0) 

function add() { 
  num.value++
  doubleNum.value = num.value * 2
} 

const count = ref(1) 
const double = computed(() => count.value * 2) 
  • 上面错误的写法看起来很傻,也很啰嗦,但确实有人在项目里面真的这样写
vue
<script lang="ts" setup>
import { computed, ref } from 'vue'

const list = ref([
  { key: 1, name: 'a' },
  { key: 2, name: 'b' },
])

const list2 = [
  { key: 1, name: 'list2-a' },
  { key: 2, name: 'list2-b' },
]

const getList2Name = computed(() => (key: string) => { 
  return list.value.find(item => item.key === key)?.name 
}) 

function getList2Name(key: string) { 
  return list.value.find(item => item.key === key)?.name 
} 
</script>

<template>
  <div v-for="item in list" :key="item.key">
    name: {{ item.name }}
    list2-name: {{ getList2Name(item.key) }}
  </div>
</template>
  • 上面这样的computed里面返回一个函数,并不会有缓存效果,相当于直接写函数
  • vuetemplate里面,响应式数据改变时,其响应式数据所在的函数也会重新执行
  • 上面错误的写法看起来很傻,但确实有人在项目里面真的这样写

props

vue
<script setup lang="ts">
const props = defineProps<{
  title: string
  likes?: number
  visible?: boolean
}>()

const modelValue = defineModel<boolean>()

console.log(props.visible) // false
</script>

<template>
  <!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
  <BlogPost is-published />
</template>
  • 都使用typescript了,声明propsemitdefineModel用其typescript的声明方式
  • 父组件引用子组件时,visible带有?可选符,如果不给子组件传递visible,因为visible的类型是boolean,vue在底层做了转换,那么props.visible的值就是false
  • 尽量使用props传递数据,不要直接修改props, vue3.4之后可以使用defineModel,它的底层也是props结合 emit实现的,本质上算是v-model的语法糖,详见defineModel

defineModel

ts
const visible = defineModel<boolean>('visible')

// 等价于
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits<{ (e: 'update:visible', value: boolean): void }>()

const visible = computed({
  get: () => props.visible,
  set: value => emit('update:visible', value),
})
  • defineModel可以简化代码,一行相当于写了上面那一坨
vue
<script lang="ts" setup>
const props = withDefaults(defineProps<{
  list?: string[]
}>(), {
  list: () => []
})
</script>
  • 上面代码中,list是可选的,如果不传值,则使用默认值[],值得注意的是list的默认值需要是函数() => []
  • 如果希望默认值是空对象{},那么同理默认值应该是函数() => ({})
  • 详见withDefaults

emit

ts
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()

ref

vue
<script lang="ts" setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref<InstanceType<typeof Child>>()

function add() {
  childRef.value.count++
  childRef.value?.add() 
}
</script>

<template>
  <Child ref="childRef" />
</template>
  • ref获取一个组件实例,需要修改组件实例里面的数据时,为了不破坏数据流向,应该在组件内部defineExpose暴露一个修改数据的函数,然后在父组件中调用该方法
  • 使用InstanceTypetypeof组合来获取子组件的实例类型,可以获得相应的代码提示,包括子组件defineExpose暴露的方法和属性
  • vue3.5+之后可以用户useTemplateRef获取组件实例,结合volar可以自动推导组件类型,详见useTemplateRef

泛型

vue
<script lang="ts" setup generic="T extends Item">
export interface Item {
  id: string
  name: string
}

const props = defineProps<{
  data: T[]
}>()

function add(item: T) {
  console.log(item)
}
</script>
  • 上面代码中,T是泛型,generic="T extends Item"表示T继承于Item, 父组件的传值的数据类型必须是{id: string; name: string}[],或者它的扩展。
  • 顺便一提,vue文件里面的类型是可以export的,在其他地方可以使用的。如果类型定义很多,很混乱的话,建议在types.ts里面定义好,然后在vue文件里面import使用。
  • 详见泛型

devtools

  • 这个插件很好用,但是不知道为什么还是有很多人不用
  • 有vite插件版本和chrome插件版本
  • 亮点
    1. 可以很清晰的看到组件结构
    2. 2直接从dom跳转到编辑器的代码行位置
    3. 可以很清晰的看到组件的propsdatacomputedwatch等,调试的时候减少console.log的次数
    4. 可以看到pinia的数据
    5. 在vite版本下还有很多新功能值得探索,chrome版本后续会跟进

组件的设计

组件架构

  • 符合这样的设计,那么在vue3里面,父组件引用子组件时,可以这样写
vue
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
import Child2 from './Child2.vue'

const data = ref()
</script>

<template>
  <Child :data="data" />
  <Child2 :data="data" />
</template>

数据流向

  • 父组件data传递给子组件,层级不深的用props传递,层级深的可以用用provide/inject传递,也可以使用store传递
  • 尽量不要在父组件里面通过ref直接调用子组件方法,组件比较简单的时候能用,当组件复杂度上来后,会变得难以维护,数据流向混乱。
  • 父组件data传递给子组件,子组件应该有自己的组件方法和状态,子组件接受到data这个响应式数据后,子组件自己内部使用watch等方式触发相应的操作。

Released under the MIT License.