优秀的编程知识分享平台

网站首页 > 技术文章 正文

Vue2转Vue3官方变更学习记录V3.0(vue2转vue3工具)

nanyue 2024-09-10 16:05:57 技术文章 5 ℃

本文适合有一定 Vue2 基础且打算快速转型并深入 Vue3 的前端小伙伴。

本次更新:

去掉图片,使用代码优化显示;

梳理了目录,参考官方文档结构,剔除不涉及章节。

添加了个人实际开发的理解;

标记了没用过但觉得有实际场景的情况。


注意,完全不考虑选项式与 reactive 对象。

一、基础

1.1 模板语法

1.1.1 同名简写,3.4 新功能

<div :id></div>
<!-- 等于 -->
<div :id="id"></div>

1.1.2 动态绑定多个值

const obj = {a:1, b:2}
<div v-bind="obj"></div>
<!-- 等于 -->
<div :a="obj.a" :b="obj.b"></div>

1.1.3 setup 模块内支持的全局变量是有限的

Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error

1.1.4 动态参数

(没用过)

<div :[变量名]="变量"></div>

<div @[函数名]="函数"></div>

1.2 计算属性

1.2.1 可写的计算属性

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

1.3 表单输入绑定

应用在 v-model 上,如 v-model.lazy

  • .lazy 将 input 原本的 input 响应改为 change 响应,避免频繁触发
  • .number 将 input 数字经过 parseFloat 处理,若非数字(NaN)则返还原始值
  • .trim 将输入内容掐头去尾空白,常用

1.3 watch

1.3.1 侦听数据源类型

注意,无法监听被监听对象的值的变化,因为引用未变,监听往往是变化(替换),除非强制深层监听

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

// 由于不能监听 ref 对象的值,会被当做普通数据(除非 ref 对象的值本身也是 ref
// 所以 getter 可以简单理解成监听多个数据与 ref 对象的值
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

// 另一个坑是强行监听一个 ref 对象,且 ref 的源也是个对象
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

// 如果监听的是 ref 对象的属性且这个属性本身也是个普通对象
watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

// 可以手动强制深层监听,慎用,想不到使用场景,宁可用 getter 多一些或下面的 watchEffect
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

1.3.2 即时回调的侦听器

使用场景:分页场景下,通过侦听 page 的变化自动更新数组时候,第一次 page 赋值用,还是很实用的。

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
)

1.3.3 watchEffect

watchEffect 简单理解成逻辑处理型的 computed 即可,同时watchEffect 支持异步,不过异步只追踪 await 之前的变量或属性。

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

1.3.4 watchPostEffect

(没用过,使用场景想得到的是组件实例的 DOM 控制)如果你有蛋疼的需求要在 watch 中进行更新后的 DOM 操作,则使用:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

watchPostEffect 是 flush 为 post 的 watchEffect 的简写。

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

1.3.5 停止监听器

(没用过,想破头想不到什么时候用)

import { watchEffect } from 'vue'

// 它会自动停止
let unwatch = watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  unwatch = watchEffect(() => {})
  unwatch() // 手动停止
}, 100)

1.3.6 effectScope

(没用过)

用来批量组合computed watch和watchEffect,获取统一返回值并停止。

const scope = effectScope()
scope.run(() => {
  const doubled = computed(() => counter.value * 2)
  watch(doubled, () => console.log(doubled.value))
  watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 
effectscope.stop()

1.3.6.1 getCurrentScope

与 effectScope 有关,这是测试效果,没有实际用过。

const allScope = getCurrentScope()

1.3.6.2 onScopeDispose

与 effectScope 有关,这是测试效果,没有实际用过。

// 只有类似下面所有 effect 作用域被取消才会触发,所以在 use 里面使用当做 unMounted 用挺好的。
onScopeDispose(() => {
  console.log('onScopeDispose')
  debugger
})

const allScope = getCurrentScope() // 注意这里是当前组件的作用域
setTimeout(() => {
  console.log(allScope)
  allScope.stop()
}, 3000)

1.3.7 watch全家桶示例

<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>
<script setup>
import { ref, watch } from "vue"
const count = ref(0)
/**
 * 挑战 1: Watch 一次
 * 确保副作用函数只执行一次
*/
const unWatch = watch(count, () => {
  console.log("Only triggered once")
  unWatch()
})
count.value = 1
setTimeout(() => count.value = 2)
/**
 * 挑战 2: Watch 对象
 * 确保副作用函数被正确触发
*/
const state = ref({
  count: 0,
})
watch(state, () => {
  console.log("The state.count updated")
}, {
  deep: true // 深层监听
})
state.value.count = 2
/**
 * 挑战 3: 副作用函数刷新时机
 * 确保正确访问到更新后的`eleRef`值
*/
const eleRef = ref()
const age = ref(2)
watch(age, () => {
  console.log(eleRef.value)
}, {
  flush: 'post' // 
})
age.value = 18 
</script>
<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>

1.4 模板引用

1.4.1 组件上的 ref

父组件

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const child = ref(null)

onMounted(() => {
  // child.value 是 <Child /> 组件的实例
  console.log(child.value) // Proxy<{ a: number, b: number }> (ref 都会自动解包,和一般的实例一样)
})
</script>

<template>
  <Child ref="child" />
</template>

子组件

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

二、组件

2.1 Props

2.1.1 组件类型标注

export interface Props {
  msg?: string as CustomString // 如果需要引入自定义类型
  labels?: string[]
}

// 默认值处理
const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

2.1.2 Prop 校验

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

2.2 事件

2.2.1 触发与监听事件(单次)

<MyComponent @some-event.once="callback" />

2.2.2 事件校验

const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}

2.3 组件 v-model

2.3.1 示例

父组件

<Child v-model="count" />

子组件

<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>

子组件相当于:

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

2.3.2 必填 v-model 与默认值

// 使 v-model 必填
const model = defineModel({ required: true })

// 提供一个默认值
const model = defineModel({ default: 0 })

2.3.3 v-model 的参数

<MyComponent v-model:title="bookTitle" />

子组件

<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

子组件相当于:

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

2.3.4 处理 v-model 修饰符

父组件

<MyComponent v-model.capitalize="myText" />

子组件

<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

子组件相当于

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

2.3.5 带参数的 v-model 修饰符

父组件

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.capitalize="last"
/>

子组件

<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { capitalize: true}
</script>

子组件相当于

<script setup>
const props = defineProps({
  firstName: String,
  firstNameModifiers: { default: () => ('') },
  lastName: String,
  lastNameModifiers: { default: () => ('') },
})

const emit = defineEmits(['update:firstName'])

function emitValue1(e) {
  let value = e.target.value
  if (props.firstNameModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:firstName', value)
}

function emitValue2(e) {
  let value = e.target.value
  if (props.lastNameModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:firstName', value)
}
</script>

<template>
  <input type="text" :value="firstName" @input="emitValue1" />
  <input type="text" :value="lastName" @input="emitValue2" />
</template>

2.4 透传 Attributes

2.4.1 透传消费

一般来说属性和事件都会传递给组件的根元素,如果是多层嵌套会继续透传(假设组件的根元素就是另一个组件),除非被传递组件声明了 defineProps 和 defineEmits,被声明的不再透传,即被“消费”。

2.4.2 透传使用

类似 Vue2 的 $attrs 使用方式,结合 v-bind 语法部分场景可精简代码。

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>

2.4.3 多根节点的 Attributes 继承

首先,Vue3 支持多根节点,即如下合法:

<template>
  <Child1 />
  <Child2 />
</template>

在这种场景下,需要显式的挂载 $attrs,否则会警告,因为不知道给谁。

2.4.4 在 JavaScript 中访问透传 Attributes

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

2.5 插槽

2.5.1 具名插槽语法

子组件

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

可简写为

<BaseLayout>
  <template #header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

2.5.2 动态插槽名

(没用过)

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

2.5.3 作用域插槽

(没用过)

一般来说,插槽的作用域是父组件,但有时需要用到子组件的数据。

子组件模板

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

父组件,注意 v-solt 的用法,与具名插槽不同。

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

2.5.4 具名作用域插槽

(没用过)

父组件

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

这里有需要注意的点,一个是 slot 的那么属性是保留字,所以给name设置的值,不会带入到 props 中。另一个是同时使用了默认插槽与具名插槽的话,默认插槽需要使用 template 标注为 default。

<template>
  <MyComponent>
    <!-- 使用显式的默认插槽 -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Here's some contact info</p>
    </template>
  </MyComponent>
</template>

2.5.5 无渲染组件

一般用来做 UI 框架,主要是封装逻辑与视图时候有用,比如将列表内容交给父组件,自己内部只做逻辑。

常规情况建议使用 use 组合式函数代替。

2.6 依赖注入

用起来很舒服,不用那么积极的用状态管理,一般这个就够了。恶心点就是 Typescript 时候的类型标注。

2.6.1 使用 Symbol 做注入名

作轻量的数据状态管理库用。

// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要提供的数据
*/ });
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

2.6.2 为 Provide / Inject 标注类型

import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // 若提供的是非字符串值会导致错误

const foo = inject(key) // foo 的类型:string | undefined

// 下面是两种消掉 undefined 的方法
const foo = inject<string>('foo', 'bar') // 类型:string
const foo = inject('foo') as string // 需要确定有值才这样用

2.7 异步组件

2.7.1 加载与错误状态

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 下面是可选的
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

2.7.2 搭配 Suspense 使用

(没用过)

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

由于 Suspense 还是实验性,所以我没用过,这里理解。

理解:主要是解决假设同时加载多个异步组件,这时候有可能出现多个加载器在转,为了忽略多个并统一管理,套在这个里面就好了。

2.8 组件递归调用

// FooBar.vue 内
import { FooBar as FooBarChild } from './components'

三、逻辑复用

3.1 插件

i18n 插件示例

// plugins/i18n.js
export default {
  // options 为配置插件时候的参数
  install: (app, options) => {
    // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      // 获取 `options` 对象的深层属性
      // 使用 `key` 作为索引
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}
import i18nPlugin from './plugins/i18n'

app.use(i18nPlugin, {
  greetings: {
    hello: 'Bonjour!'
  }
})

3.2 插件中的 Provide / Inject

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.provide('i18n', options)
  }
}
import { inject } from 'vue'

const i18n = inject('i18n')

console.log(i18n.greetings.hello)

四、内置组件

4.1 Teleport

将内部元素传递给目标元素内,一般是传给 body 居多,作全局样式。

比如全局弹框之类的,一般一个应用就一个,写在组件里面会有问题,所以用统一的组件并且放在 body 下面方便配置 z-index 之类的东西。

可参考 element plus 的 image 的 preview 功能源码。

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

4.2 禁用 Teleport

特殊场景,如响应式场景下。

<Teleport :disabled="isMobile">
  ...
</Teleport>

五、应用规模化

5.1 测试

不用纠结,用 Vitest 即可。

5.1.1 单元测试

跟 Vue3 没啥关系,就是正常的功能库测试,或纯函数测试。

// helpers.js
export function increment (current, max = 10) {
  if (current < max) {
    return current + 1
  }
  return current
}
// helpers.spec.js
import { increment } from './helpers'

describe('increment', () => {
  test('increments the current number by 1', () => {
    expect(increment(0, 10)).toBe(1)
  })

  test('does not increment the current number over the max', () => {
    expect(increment(10, 10)).toBe(10)
  })

  test('has a default max of 10', () => {
    expect(increment(10)).toBe(10)
  })
})

5.1.2 组件测试

(没用过,等用到再说)

5.1.3 端到端(E2E)测试

(没用过,也不打算用,还是让测试人员来吧)

5.1.4 测试组合式函数

没有生命周期勾子和 Provide / Inject 的情况跟单元测试没区别,就是多导入一个 vue 而已。

// counter.js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++

  return {
    count,
    increment
  }
}
// counter.test.js
import { useCounter } from './counter.js'

test('useCounter', () => {
  const { count, increment } = useCounter()
  expect(count.value).toBe(0)

  increment()
  expect(count.value).toBe(1)
})

有生命周期勾子和 Provide / Inject 的情况需要一个父组件(应用)来包裹模拟。

(没用过)

// test-utils.js
import { createApp } from 'vue'

export function withSetup(composable) {
  let result
  const app = createApp({
    setup() {
      result = composable()
      // 忽略模板警告
      return () => {}
    }
  })
  app.mount(document.createElement('div'))
  // 返回结果与应用实例
  // 用来测试供给和组件卸载
  return [result, app]
}
import { withSetup } from './test-utils'
import { useFoo } from './foo'

test('useFoo', () => {
  const [result, app] = withSetup(() => useFoo(123))
  // 为注入的测试模拟一方供给
  app.provide(...)
  // 执行断言
  expect(result.foo.value).toBe(1)
  // 如果需要的话可以这样触发
  app.unmount()
})

六、进阶主题

6.1 深入响应式系统

6.1.1 Vue 中的响应性是如何工作的

首先,getter / setter 用在了 ref 上,不是不用。

其次,reactive 使用的是 proxy。

伪代码大致这样:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

6.1.2 组件调试勾子

import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})

6.1.3 计算属性调试

const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // 当 count.value 被追踪为依赖时触发
    debugger
  },
  onTrigger(e) {
    // 当 count.value 被更改时触发
    debugger
  }
})

// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)

// 更改 count.value,应该会触发 onTrigger
count.value++

6.1.4 侦听器调试

watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

6.2 渲染函数 & JSX

(没用过)只要强依赖 JavaScript 原生能力才用得到。

目前没有开放给 setup 语法糖的函数,故得使用 setup 函数。

6.2.1 渲染一个列表

function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

6.2.2 JSX

非 JSX

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
    return ok.value ? h(Foo) : h(Bar)
}

JSX

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return ok.value ? <Foo /> : <Bar />
}

6.2.3 内置组件

import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  setup () {
    return () => h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

6.2.4 函数式组件 / 为函数式组件标注类型

具名函数式组件

import type { SetupContext } from 'vue'
type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

function FComponent(
  props: FComponentProps,
  context: SetupContext<Events>
) {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value: unknown) => typeof value === 'string'
}

匿名函数式组件

import type { FunctionalComponent } from 'vue'

type FComponentProps = {
  message: string
}

type Events = {
  sendMessage(message: string): void
}

const FComponent: FunctionalComponent<FComponentProps, Events> = (
  props,
  context
) => {
  return (
    <button onClick={() => context.emit('sendMessage', props.message)}>
        {props.message} {' '}
    </button>
  )
}

FComponent.props = {
  message: {
    type: String,
    required: true
  }
}

FComponent.emits = {
  sendMessage: (value) => typeof value === 'string'
}

6.3 Vue 与 Web Components

这是个好东西,面向未来,有了这个就不用担心 Vue3 过时。

6.3.1 定义与使用

import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // 这里是同平常一样的 Vue 组件选项
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`]
})

// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签
// 都会被升级
customElements.define('my-vue-element', MyVueElement)

// 你也可以编程式地实例化元素:
// (必须在注册之后)
document.body.appendChild(
  new MyVueElement({
    // 初始化 props(可选)
  })
)
<my-vue-element></my-vue-element>

6.3.2 注意事项

  • Props 会被 Vue 转换成字符串
  • 事件会被定义为 CustomEvents
  • 插槽只支持 Web Components 的原生插槽,v-slot 不再支持
  • 依赖注入依然可用,当然是要在 Vue 的实例间共享,无法在 Vue 实例与 Vue 自定义元素之间共用

6.3.3 TS 上 CustomEvent 传递

document.addEventListener('confirm-edit', ((e: CustomEvent) => {
    // todo
  }) as EventListener)

七、全局 API

7.1 应用实例

7.1.1 app.funWithContext()

我是用来调试的……

import { inject } from 'vue'

app.provide('id', 1)

const injected = app.runWithContext(() => {
  return inject('id')
})

console.log(injected) // 1

7.1.2 app.config.errorHandler

app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

7.1.3 app.config.warnHandler

app.config.warnHandler = (msg, instance, trace) => {
  // `trace` 是组件层级结构的追踪
}

7.1.4 app.config.performance

(没用过)

性能工具。

7.1.5 app.config.globalProperties

定义

app.config.globalProperties.msg = 'hello'

调用有两种方式

import { ref, provide, nextTick, getCurrentInstance, onMounted } from 'vue'

const instance = getCurrentInstance() as ComponentInternalInstance
console.log(instance.proxy.$any)
console.log(instance.appContext.config.globalProperties.$any)

主要是 TS 扩展。

import axios from 'axios'

declare module 'vue' {
  interface ComponentCustomProperties {
    $http: typeof axios
    $translate: (key: string) => string
  }
}

八、组合式 API

8.1 响应式:核心

8.1.1 toRef

用来把对象的单一属性转为响应式。

<script setup>
import { toRef } from 'vue'

const props = defineProps(/* ... */)

// 将 `props.foo` 转换为 ref,然后传入
// 一个组合式函数
useSomeFeature(toRef(props, 'foo'))

// getter 语法——推荐在 3.3+ 版本使用
useSomeFeature(toRef(() => props.foo))
</script>

8.1.2 toValue

用来获取响应式的原始值。

import type { MaybeRefOrGetter } from 'vue'

function useFeature(id: MaybeRefOrGetter<number>) {
  watch(() => toValue(id), id => {
    // 处理 id 变更
  })
}

// 这个组合式函数支持以下的任意形式:
useFeature(1)
useFeature(ref(1))
useFeature(() => 1)

8.1.3 toRefs

主要用来提供解构的响应式方案。

如果单一响应式,用 toRef。

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ...基于状态的操作逻辑

  // 在返回时都转为 ref
  return toRefs(state)
}

// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX()

8.1.4 isProxy

判断对象是否被创建的代理对象。

注意,Proxy 本质是透明的,除非特殊处理,否则 JS 层面是无法获知该对象是否为 Proxy 实例,即 instanceof 无效。

判断是否为 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建。

8.2 响应式:进阶

8.2.1 shallowRef

浅层 ref。

const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

8.2.2 triggerRef

基于 shallowRef 时候,又需要对某个属性进行响应。

const shallow = shallowRef({
  greet: 'Hello, world'
})

// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)

8.2.3 customRef

自定义ref,处理类似防抖等。很常用的功能。

import { customRef } from 'vue'
export function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

使用。

<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>

<template>
  <input v-model="text" />
</template>

8.3 生命周期

8.3.1 onErrorCaptured

(没用过)

import { onErrorCaptured } from 'vue'

onErrorCaptured((error, instance, info) => {})

九、内置内容

9.1 指令

9.1.1 v-pre

原样输出,不做编译,用来做类似代码演示讲解。

<span v-pre>{{ this will not be compiled }}</span>

9.1.2 v-once

(没用过)

<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 带有子元素的元素 -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- 组件 -->
<MyComponent v-once :comment="msg" />
<!-- `v-for` 指令 -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

9.1.3 v-memo

(没用过)

告知编译器,那些场景下才进行 patch。注意这句:

v-model="[]" 与 v-once 效果一样。

<div v-memo="[valueA, valueB]">
  ...
</div>
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>

这里是说只有 item.id 为 selected 才更新,其他不做处理。

注意:v-for 与 v-memo 一起用的时候,只能作用在同一个元素上,不能把 v-memo 做为 v-for 内部的元素用。

9.1.4 v-cloak

这个只用于类似 CND 环境,即无构建步骤环境,因为有构建步骤的基本都已经构建好了,不会出现双括号的情况。

另外就是注意要给样式,否则无意义。

<style>
[v-cloak] {
  display: none;
}
</style>

<div v-cloak>
  {{ message }}
</div>

9.2 组件

9.2.1 KeepAlive

排除、缓存、最大值

<!-- 用逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view"></component>
</KeepAlive>

<!-- 正则表达式 (使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view"></component>
</KeepAlive>

<!-- 数组 (使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view"></component>
</KeepAlive>

<KeepAlive :max="10">
  <component :is="view"></component>
</KeepAlive>

十、单位件组件 SFC

10.1 <script setup>

10.1.1 命名空间组件

当一个文件中导出多个组件时候用。

比如有 form-component/input, form-component/label, form-component/index.js。

<script setup>
import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>

10.1.2 使用自定义指令

主要是满足命名规范,即 vAnyDirective。

<script setup>
const vMyDirective = {
  beforeMount: (el) => {
    // 在元素上做些操作
  }
}
</script>
<template>
  <h1 v-my-directive>This is a Heading</h1>
</template>

10.1.3 defineExpose

因为 <script setup> 默认是关闭的,所以需要特殊处理,将内部变量提供给父组件使用。

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

10.1.4 defineOptions

没有选项式的 options 可以写入配置,所以需要显式的定义。

<script setup>
defineOptions({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  }
})
</script>

注意:这是一个宏定义,选项将会被提升到模块作用域中,无法访问 <script setup> 中不是字面常数的局部变量。

10.1.5 defineSlots

Typescript 下定义 slots 的类型约束。

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { msg: string }): any
}>()
</script>

10.1.6 useSlots 和 useAttrs

在逻辑代码中使用 slots 与 attrs。

模板中自带 $slots 和 $attrs,无需单独处理。

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

10.1.7 顶层 await

首先,这个要与 Suspense 内置组件配合使用,否则会出问题。

个人不推荐使用。

<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

10.1.8 泛型

(没用过,想不到使用场景)

<script
  setup
  lang="ts"
  generic="T extends string | number, U extends Item"
>
import type { Item } from './types'
defineProps<{
  id: T
  list: U[]
}>()
</script>

10.2 CSS 功能

CSS 功能很多奇技淫巧,但是也带来了很多的坑,使用务必注意!

10.2.1 组件作用域 CSS

scoped 大家很熟悉了,这里提供一个点。

使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。

10.2.2 深度选择器

注意样式渗透,被深度包裹的选择器是没有 hash 值的,所以可以用来给 v-html 场景样式设置。

<style scoped>
.a :deep(.b) {
  /* ... */
}
</style>

<style>
.a[data-v-f3f3eg9] .b {
  /* ... */
}
</style>

10.2.3 插槽选择器

(没用过)

子组件对插槽内样式设置。

个人不建议使用,插槽样式应该归属于父组件,这样强行使用会很奇怪。

<style scoped>
:slotted(div) {
  color: red;
}
</style>

10.2.4 全局选择器

(没用过)

个人强烈不建议使用,全局样式就该在全局样式的地方设置,不容太容易污染。

<style scoped>
:global(.red) {
  color: red;
}
</style>

10.2.5 混合使用局部与全局样式

个人强烈不建议使用,虽然在使用第三方库时候经常遇到这种问题,但还是建议在全局样式该设置的地方设置。

<style>
/* 全局样式 */
</style>

<style scoped>
/* 局部样式 */
</style>

10.2.6 CSS 中的 v-bind

(用过,但没成功)

setup 中使用属性需要带上引号包起来。

<script setup>
const theme = {
  color: 'red'
}
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

十一、进阶 API

11.1 自定义渲染

11.1.1 createRenderer

基本就在测试时候用到。

import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement
  // ...
})

// `render` 是底层 API
// `createApp` 返回一个应用实例
export { render, createApp }

// 重新导出 Vue 的核心 API
export * from '@vue/runtime-core'

11.2 编译时标志

11.2.1 __VUE_OPTIONS_API__

编译时取消对选项式的支持,在不考虑第三方的情况下,建议关掉。

11.2.2 __VUE_PROD_DEVTOOLS__

生产环境对开发者工具支持,视情况开启吧,除非极致性能要求,个人觉得放开,方便排查问题。

11.2.3 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__

水合(激活)报错的警告,用于 SSR 场景,同样个人建议开放,理由如上。

11.2.4 Vite 配置

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    // 启用生产环境构建下激活不匹配的详细警告
    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
  }
})

十二、TypeScript 篇

12.1 importmap

解决引入文件路径和别名问题,注意,必须是标准JSON,不能是 JS 的 Object 即没有双引号或使用单引号那种。

<script type="importmap">
  {"imports":{"m1":"./m1.js","mm":"./mm.js","m2":"./m2.js"}}
</script>

Tags:

最近发表
标签列表