Vue3 渐进式前端框架开发

11/1/2020 vue

# Vue3 简述

# Vue3 的一些新特性

必须要夸一夸 Vue3,它真实可谓“千呼万唤使出来”,听说是尤大神每天抱着孩子完成的。故事已经不能说明 Vue3 的伟大了,我们用下面一组数字来了解一下 Vue3。

Vue3:两年开发,99 位程序员成为贡献者,2600 次提交,628 次 PR

这些数字可以看出 Vue3 的工作量之大,我在一年前就开始关注 Vue3 的开发进展,虽然没成为 Vue3 的贡献者之一,但是我热爱的心不可否认。

官方网站地址: v3.vuejs.org

Vue3新特性

  • 首先是向下兼容,Vue3 支持大多数 Vue2 的特性。我们同事甚至开玩笑说,我就拿 Vue2 的语法开发 Vue3,也是没有任何问题的。
  • 性能的提升,每个人都希望使用的框架更快,更轻。Vue3 做到了,给开发者一个极致的体验。官方网站给出的数据是:打包大小减少 41%,初次渲染快 55%,更新快 133%,内存使用减少 54%。
  • 新推出的Composition API ,在 Vue2 中遇到的问题就是复杂组件的代码变的非常麻烦,甚至不可维护。说白了就是封装不好,重用不畅。这个Composition API一推出,立马解决了这个问题,本套课程中也会重点介绍这部分内容。它是一系列 API 的合集。
  • 其他新特性:Teleport(瞬移组件)、Suspense(解决异步加载组件问题)和全局 API 的修改和优化。
  • 更好TypeScript支持,我以前在开发 Vue2 的时候,是不适用TypeScript的,因为集成时很困难,疼点太多。但 Vue3 解决了这个问题,Vue3 的源代码就是使用TypeScript进行开发的。所以在新的版本上使用TS也更加顺畅无阻。

B 站 Vue3 发布会视频:https://www.bilibili.com/video/BV1iA411J7cA?from=search&seid=2979047174353974296

####### ###

# 全局 API 的重大更改

# 全局声明应用更换

Vue 2.x 有许多全局 API 和配置,这些 API 和配置可以全局改变 Vue 的行为。例如,Vue.use,Vue.component,Vue.directive等等。虽然这种声明方式很方便,但它也会导致一些问题。从技术上讲,Vue 2 没有“app”的概念,我们定义的应用只是通过 new Vue() 创建的根 Vue 实例。测试期间,全局配置很容易意外地污染其他测试用例。有些 API 像 Vue.use 以及 Vue.mixin 甚至连恢复效果的方法都没有,这使得涉及插件的测试特别棘手。实际上,vue-test-utils 必须实现一个特殊的 API createLocalVue 来处理此问题,了避免这些问题,在 Vue 3 中我们引入createApp的一个全新API

import { createApp } from 'vue'
const app = createApp({})

应用实例暴露当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是当前全局 API 及其相应实例 API 的表:

2.x 全局 API 3.x 实例 API (app)
Vue.config app.config
Vue.config.productionTip removed (见下方 (opens new window))
Vue.config.ignoredElements app.config.isCustomElement (见下方 (opens new window))
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use (见下方 (opens new window))

由于 use 全局 API 在 Vue 3 中不再使用,此方法将停止工作并停止调用 Vue.use() 现在将触发警告,于是,开发者必须在应用程序实例上显式指定使用此插件:

const app = createApp(MyApp)
app.use(VueRouter)

# next 函数的语法改变

在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如,我们之前的片段现在应该如下所示:

import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})

# 模板指令的重大更改

# 组件 v-model 的用法更改

在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model 绑定 prop 的情况)。为此,我们建议使用 update:myPropName 抛出事件。例如,对于在上一个示例中带有 title prop 的 ChildComponent,我们可以通过下面的方式将分配新 value 的意图传达给父级:

this.$emit('update:title', newValue)
<!-- 如果需要的话,父级可以监听该事件并更新本地 data property。例如: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
<!-- 为了方便起见,我们可以使用 .sync 修饰符来缩写,如下所示: -->
<ChildComponent :title.sync="pageTitle" />

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件:

<ChildComponent v-model="pageTitle" />
<!-- 简写: -->
<ChildComponent
  :modelValue="pageTitle"
  @update:modelValue="pageTitle = $event"
/>

若需要更改 model 名称,而不是更改组件内的 model 选项,那么现在我们可以将一个 argument 传递给 model

<ChildComponent v-model:title="pageTitle" />
<!-- 简写: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

v-bind anatomy

这也可以作为 .sync 修饰符的替代,而且允许我们在自定义组件上使用多个 v-model

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 简写: -->

<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>

# 模板与其他指令 key 值用法更改

在 Vue 2.x 中 <template> 标签不能拥有 key。不过你可以为其每个子节点分别设置 key

<!-- Vue 2.x -->
<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>

在 Vue 3.x 中 key 则应该被设置在 <template> 标签上。

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

类似地,当使用 <template v-for> 时存在使用 v-if 的子节点,key 应改为设置在 <template> 标签上。

<!-- Vue 2.x -->
<template v-for="item in list">
  <div v-if="item.isVisible" :key="item.id">...</div>
  <span v-else :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div v-if="item.isVisible">...</div>
  <span v-else>...</span>
</template>

# v-for 中的 Ref 不在返回数组

在 Vue 2 中,在 v-for 里使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。

在 Vue 3 中,这样的用法将不再在 $ref 中自动创建数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):

<template>
  <div v-for="item in list" :key="item" :ref="setItemRef"></div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  setup() {
    const itemRefs: Element[] = [];
    const setItemRef = (el: Element) => itemRefs.push(el);
    return {
      setItemRef
    };
  }
});
</script>

<style lang="scss"></style>

# 异步组件的重大更改

以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:

const asyncPage = () => import('./NextPage.vue')

或者,对于带有选项的更高阶的组件语法:

const asyncPage = {
  component: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

现在,在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包装在新的 defineAsyncComponent 助手方法中来显式地定义:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// 带选项的异步组件
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

# 移除 API

# 支持的库

所有的官方库和工具现在都支持 Vue 3,但大多数仍然处于 beta 状态,并在 NPM 的 next dist 标签下发布。我们正计划在 2020 年底前稳定所有项目,并将其转换为使用 latest 的 dist 标签

# Vue CLI

从 v4.5.0 开始,vue-cli 现在提供了内置选项,可在创建新项目时选择 Vue 3 预设。现在可以升级 vue-cli 并运行 vue create 来创建 Vue 3 项目。

# Vue Router

Vue Router 4.0 提供了 Vue 3 支持,并有许多突破性的变化,查看 README (opens new window) 中完整的细节,

# Vuex

Vuex 4.0 提供了 Vue 3 支持,其 API 与 3.x 基本相同。唯一的突破性变化是插件的安装方式 (opens new window)

# Devtools Extension

我们正在开发一个新版本的 Devtools,它有一个新的 UI 和经过重构的内部结构,以支持多个 Vue 版本。新版本目前处于测试阶段,目前只支持 Vue 3。Vuex 和路由器的集成也在进行中。

# IDE 支持

推荐使用 VSCode (opens new window) 和我们官方拓展 Vetur (opens new window),它为 Vue 3 提供了全面的 IDE 支持

# Vue 基础命令&对象

# Vue-html指令

  1. v-text:更新元素的textContent
  2. v-html:更新元素的innerHTML
  3. v-if:如果为true, 当前标签才会输出到页面
  4. v-else:如果为false, 当前标签才会输出到页面
  5. v-show:通过控制display 样式来控制显示/隐藏
  6. v-for:遍历数组/对象
  7. v-on:绑定事件监听, 一般简写为@
  8. v-bind:标签属性绑定解析表达式, 可以省略v-bind
  9. v-model:双向数据绑定
  10. ref:指定唯一标识, vue 对象通过$els 属性访问这个元素对象
  11. v-cloak:防止闪现, 与css 配合: [v-cloak] { display: none }
  12. setup: 发送ajax请求, 启动定时器等异步任务
  13. onBeforeUnmount: 做收尾工作, 如: 清除定时器

# Vue 切换显示动画

Vue transition 允许我们只设置隐藏或者显示的样式,这样切换的时候,也会有过渡的效果。

<transition name="name"><div v-show="fool">666</div></transition>
.[name]-enter-active, .[name]-leave-active{/*显示/隐藏的过渡样式*/}
.[name]-enter-from, .[name]-leave-to {/*隐藏的样式*/}
.[name]-enter-top, .[name]-leave-from {/*显示的样式*/}

# Vue 括号表达式

<view>
  <text class="title">{{title}}</text>
  <template v-for="(item, index) in 6">
    <div :key="index">{{item}}</div>
  </template>
</view>

# Vue 事件修饰符

<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div @scroll.passive="onScroll">...</div>

# Vue 按键修饰符

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
.enter	.tab	.delete (捕获“删除”和“退格”键)	.esc	.space	.up	.down	.left	.right

# Vue 定义响应式数据

# 定义数据(define)

import { defineComponent } from "vue";
import { reactive, ref } from "vue";

export default defineComponent({
  setup() {
    // 定义单个响应式数据
    const count = ref(0);
    // 通过改变count.value发生响应式变化
    count.value++;

    // 以对象形式定义响应式数据
    const state = reactive({ count: 0 });
    // 通过改变对象发生响应式变化
    state.count++;

    return {
      count,
      state
    }
  }
});

# 定义数据(setup)

import { reactive, ref } from "vue";
// 定义单个响应式数据
const count = ref(0);
// 通过改变count.value发生响应式变化
count.value++;

// 以对象形式定义响应式数据
const state = reactive({ count: 0 });
// 通过改变对象发生响应式变化
state.count++;

# 模板中使用

<template>
  <span>{{ count }}</span>
  <span> {{ state.count }}</span>
</template>

^注意: toRef 可以用来为一个 reactive 对象的属性创建一个 ref。这个 ref 可以被传递并且能够保持响应性。

# Vue 外置组合函数

# 计算属性值 (computed)

# computed 函数 计算属性
<template>
  姓:<input type="text" v-model="firstName" /><br />
  名:<input type="text" v-model="lastName" /><br />
  姓名(单向):<input type="text" v-model="Name" />
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
const firstName = ref("Aaa");
const lastName = ref("bbbB");

const name = computed(() => {
  return firstName.value + "  " + lastName.value;
});
</script>
# computed 对象 监视读写
<template>
  姓:<input type="text" v-model="firstName" /><br />
  名:<input type="text" v-model="lastName" /><br />
  姓名(单向):<input type="text" v-model="name" />
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
const firstName = ref("Aaa");
const lastName = ref("bbbB");

const name = computed({
  // 回调函数,当需要读取当前属性值时回调,根据相关的数据计算并返回当前属性的值
  get() {
    return firstName.value + "|" + lastName.value;
  },
  // 回调函数,当属性值发生改变时回调,更新相关的属性数据
  // value就是name的最新属性值
  set(value: string) {
    const names = value.split("|");
    firstName.value = names[0] || "";
    lastName.value = names[1] || "";
  }
});
</script>
# computed 替换过滤器

Filters 已从 Vue 3.0 中删除,不再受支持。在 3.x 中,filters 已删除,不再受支持。相反,官方建议用方法调用或计算属性替换它们。

// filters.ts
import { computed } from "vue";
export function (value: Ref) {
  return computed(() => "...")
}

# 收集依赖并监视(effect)

watchEffect会自动收集使用依赖并立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。

<script setup lang="ts">
import { ref, watchEffect } from "vue";
const firstName = ref("Aaa");
const lastName = ref("bbbB");
const name = ref("");
// watchEffect会自动收集使用依赖
// 当firstName与lastName改变时执行监视函数
watchEffect(() => {
  name.value = firstName.value + "|" + lastName.value;
});
</script>
# 停止监视

watchEffect 返回停止函数,可以调用该函数以显式停止观察程序:

import { ref, watchEffect } from "vue";
const count = ref(0);
// 接收停止函数
const stop = watchEffect(() => {
  console.log("count:", count);
});
count.value++;
// 停止监视
stop();
# 废弃钩子

watchEffect 第一个参数接收值为函数钩子,该钩子会在该 watchEffect 废弃时调用。

watchEffect(onInvalidate => {
  onInvalidate(() => {})
})
# 更新模式

在组件更新后需要重新运行观察者效果的情况下,我们可以通过选项传递一个附加options对象flush(默认为'pre'):

watchEffect(() => {
  /* ... */
}, { flush: 'post'})
# 看守调试

onTrackonTrigger选项可以用来调试观察者的行为。

  • onTrack 当将响应式属性或ref跟踪为依赖项时将被调用
  • onTrigger 当监视者回调由依赖项的变化触发时将被调用

这两个回调都将接收到一个调试器事件,该事件包含有关所依赖项的信息。

watchEffect(() => {
  /* ... */
}, { 
  onTrack(e) { },
  onTrigger(e) { },
})

# 指定数据源监视(watch)

watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。

import { ref, watch } from "vue";
const count = ref(0);
// watch(ref, fn, ?options) 对count进行监视, 值改变时触发
watch(count, (value, oldValue) => {
  console.log("count值发生改变");
  console.log("新值为: ", value);
  console.log("旧值为: ", oldValue);
});
count.value++

watch 函数第三个参数为配置参数,是可选项,可选项中带有以下参数

deep: boolean; // 是否深度监视, 及对象或数组内部(false)
immediate: boolean; // 是否初始化执行, 及以当前值执行

# Composition 组合式 API

# reactive 声明响应式对象

要为 JavaScript 对象创建响应式状态,可以使用 reactive 方法:

import { reactive } from 'vue'

// 响应式状态
const state = reactive({
  count: 0
})

# ref 声明响应式值

想象一下,我们有一个独立的原始值 (例如,一个字符串),我们想让它变成响应式的。当然,我们可以创建一个拥有相同字符串 property 的对象,并将其传递给 reactive。Vue 为我们提供了一个可以做相同事情的方法 ——ref

import { ref } from 'vue'

const count = ref(0)

ref 会返回一个可变的响应式对象,该对象作为它的内部值——一个响应式的引用,这就是名称的来源。此对象只包含一个名为 value 的 property :

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

# 响应式状态解构

当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构 (opens new window)来获取我们想要的 property:

import { reactive } from 'vue'

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = book

遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:

import { reactive, toRefs } from 'vue'

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = toRefs(book)

title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'

# ref | reactive 数据解包

传入响应式数据假如存在 ref | reactive | computed 会自动解包,并具有响应式

const name = ref('Mr.Mao')
const book = ref({
  name
})
/* Ref<{
  name: string;
}> */
const composition = reactive({
  name
})
/* {
  name: string;
} */

# Provide / Inject 声明注入

# Provide 提供源数据

为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref (opens new window)reactive (opens new window)

使用 MyMap 组件,我们的代码可以更新如下:

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref, readonly } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })
	// 确保通过 provide 传递的数据不会被 inject 的组件更改, 使用 readonly
    provide('location', readonly(location))
    provide('geolocation', readonly(geolocation))
    // 有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,建议 provide 一个方法来负责改变响应式 property。
    const updateLocation = () => {
      location.value = 'South Pole'
    }
    provide('updateLocation', updateLocation)
  }
}
</script>

# Inject 源数据注入

<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

# Mixin 混合合并

Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个 mixin 对象可以包含任意组件选项。当组件使用 mixin 对象时,所有 mixin 对象的选项将被“混合”进入该组件本身的选项。

例子:

// 定义 mixin 对象
const myMixin = {
  created() { this.hello() },
  methods: {
    hello() { console.log('hello from mixin!') }
  }
}
// 定义使用改 mixin 的应用
const app = Vue.createApp({ mixins: [myMixin] })
app.mount('#mixins-basic') // => "hello from mixin!"

# 选项合并

当组件和 mixin 对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

const myMixin = {
  data: ()=> ({
      message: 'hello',
      bar: 'abc'
  }),
}

const app = Vue.createApp({
  mixins: [myMixin],
  data: ()=> ({
      message: 'goodbye',
      bar: 'def'
  }),
  created() { console.log(this.$data) } // => { message: "goodbye", foo: "abc", bar: "def" }
})

同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用。

const myMixin = {
  created() { console.log('mixin 对象的钩子被调用') }
}

const app = Vue.createApp({
  mixins: [myMixin],
  created() { console.log('组件钩子被调用') }
})
// => "mixin 对象的钩子被调用"
// => "组件钩子被调用"

# Vue 实例对象生命周期

# 生命周期流程

Vue 实例生命周期

# 对比 Vue2.x 的声明周期

  • beforeCreate -> 使用setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

# Vue生命周期函数

一、初始化显示可调用函数

  • setup()
  • onBeforeMount()
  • onMounted()

二、更新状态可调用函数

  • onBeforeUpdate()
  • onUpdated()

三、销毁Vue实例可调用函数

  • onBeforeUnmount()
  • onUnmounted()

一般比较常用的生命周期方法

  • setup()/onMounted(): 发送ajax请求, 启动定时器等异步任务
  • onBeforeUnmount(): 做收尾工作, 如: 清除定时器

# Vue 自定义命令

# Vue所有实例注册

import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App)

// 元素内所有字符串改变为大写
app.directive("upper-text", (el, binding) => {
  const value: string = binding.value;
  el.innerText = value;
});

# Vue单个实例注册

export default defineComponent({
  directives: {
    "upper-text"(el, binding) {
      const value: string = binding.value;
      el.innerText = value;
    }
  }
});

# 使用自定义标签属性

<div id="test">
	<p v-upper-text='msg'>adsaSAsSasAS</p>
</div>

# Vue命令生命周期执行

const app = Vue.createApp({})

app.directive('highlight', {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // 新
  unmounted() {}
})

# Vue 源码分析

TODO

# Vue 面向组件编程

# 脚手架搭建环境

cnpm i @vue/cli -g

vue create  '项目名'  / vue ui

#↓↓↓↓↓↓↓#

? Please pick a preset: (Use arrow keys) ## 选择你的配置, 如果之前保存有配置, 会在此显示
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> Manually select features
  
#↓↓↓↓↓↓↓#

? Check the features needed for your project: ## 选择配置项, <空格>表示选择, <a>表示全选, <i>表示反转
>(*) Choose Vue version
 (*) Babel ## ES6转为ES5的解析器
 (*) TypeScript ## .ts的解析器
 ( ) Progressive Web App (PWA) Support ## 渐进式Web应用程序
 (*) Router  ## vue路由
 (*) Vuex    ## vue状态数据管理
 (*) CSS Pre-processors ## css预编译器
 (*) Linter / Formatter ## 代码风格检查和格式化
 ( ) Unit Testing  ## 单元测试(unit tests)
 ( ) E2E Testing   ## e2e测试(end to end)
 
#↓↓↓↓↓↓↓## 

? Choose a version of Vue.js that you want to start the project with ## 选择您要用来启动项目的Vue.js版本
  2.x
> 3.x (Preview)

#↓↓↓↓↓↓↓#

Use class-style component syntax? (y/N) ## 是否选用class类模式

? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) ## 将Babel与TypeScript一起使用(现代模式,自动检测的polyfill,转化JSX所需)? (是/否)

#↓↓↓↓↓↓↓#

? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter, Unit
? Use history mode for router? (Y/n) 	## router是否使用history模式, 否则使用hash默认(建议n)

#↓↓↓↓↓↓↓#

? Pick a CSS pre-processor: ## 选择css 预处理器
> Sass/SCSS (with dart-sass)
  Sass/SCSS (with node-sass)
  Less
  Stylus

#↓↓↓↓↓↓↓#

? Pick a linter / formatter config: ## 选择Eslint 代码验证规则 (通常Prettier用的比较多)
  ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
> ESLint + Prettier

#↓↓↓↓↓↓↓#

? Pick additional lint features: ## 选择什么时候检测
>(*) Lint on save		## 保存就检测
 ( ) Lint and fix on commit		## fix或commit的时候检测

#↓↓↓↓↓↓↓#

? Pick a unit testing solution: ## 选择单元测试方案
> Mocha + Chai		## Mocha测试库+Chai断言库
  Jest		## Jest测试库

#↓↓↓↓↓↓↓#

? Where do you prefer placing config for Babel, ESLint, etc.? ## 项目配置文件存放处
  In dedicated config files		## 独立文件存放
> In package.json		## 统统放在package.json中

#↓↓↓↓↓↓↓#

? Save this as a preset for future projects? (y/N) ## 是否保存该配置到本地文件, 如果选择Y, 选择需要输入名称

#↓↓↓↓↓↓↓#

Vue CLI v4.3.1		## 安装相应包, 等待创建项目中
✨  Creating project in D:/......
⚙️  Installing CLI plugins. This might take a while...

#↓↓↓↓↓↓↓#

npm run serve --open	## 内存中打包并开启服务

## 配置run serve自动打开浏览器
package.json --> scripts:{serve: "vue-cli-service serve --open"}

# 项目目录文件解析

## 项目总文件
  - node_modules		用node安装的依赖包
  - src				资源文件夹以后我们就在这个目录写代码
  - public			静态资源html(图片之类)json数据之类
  - tests				单元测试,代码测试
  - .gitignore		上传需要忽略的文件格式
  - babel.config.js		babel相关log信息
  - package.json		项目基本信息(项目开发所需模块,项目名称,版本, es配置)
  - README.md			项目说明(如何使用,有哪些方法等等)
## src目录文件 > 项目资源
  - assets		    静态资源(js,css之类可以放在这下面)
  - components	    公用组件编写的地方
  - router/index.js		路由配置文件
  - store/index.js		vuex状态数据管理配置文件
  - views			路由组件存放地(视图组件)
  - App.vue		    项目的主组件,所有页面都是在app.vue下切换的.一个标准的vue文件,分为三部分。
  - main.js	    	页面程序入口文件,加载各种公共组件

# 生产环境的操作

## 编译打包
npm run build
## 模拟后台 (静态服务器工具包)
安装:npm install serve -g
运行dist包文件夹:serve dist
访问: http://localhost:5000

## 修改打包项目名称
webpack.prod.conf.js --> output:{publicPath: '打包名称'}

## 动态web服务器如何开启打包项目
将打包文件拷贝到运行的tomcat(后端服务) 的运行目录下
访问: http://localost:8080/xxx

# 组件基本架构(define)

新版不使用new Vue进行创建应用,而使用createApp进行创建应用,且组件使用defineComponent函数进行创建。Vue在推送新语法的同时,向后兼容对象写法。

# 入口函数 src / main.ts

/* src/main.js */
import { createApp } from "vue"; // 引入创建应用方法
import App from "./App.vue"; // 引入入口Vue文件
createApp(App).mount("#app"); // 创建Vue且挂载至#app元素中

# 主组件格式 src / app.vue

<template>
  <!-- 模板页面(支持多跟节点) -->
  <span>count: </span>
  <span>{{ count }}</span>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup() {
    const count = ref(0);
    return {
      count
    };
  }
});
</script>

<style lang="scss"></style>

# 引入组件格式

<template>
  <!-- 使用组件标签(大小写区分) -->
  <HelloWorld></HelloWorld>
  <!-- 使用组件标签(中划线区分) -->
  <hello-world />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";

export default defineComponent({
  components: {
    HelloWorld
  }
});
</script>

<style lang="scss"></style>

# 组件基本架构(setup)

单文件组件 Composition API 语法糖,用于改善在单个文件组件中使用Composition API时的创作体验。

# 主组件格式 src / app.vue

<template>
  <span>count: </span>
  <span @click="inc">{{ count }}</span>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
const count = ref(0)
const inc = () => count.value++
</script>

<style lang="scss"></style>

# 引入组件格式

<template>
  <!-- 使用组件标签(大小写区分) -->
  <HelloWorld></HelloWorld>
  <!-- 使用组件标签(中划线区分) -->
  <hello-world />
</template>

<script lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

<style lang="scss"></style>

# 父子组件间通信

# 子接收父数据(define)

利用标签名从父组件传输数据到子组件

<!-- 父组件标签(App.vue)传输数据(任意JS属性或方法) -->
<TdoHead  :addTask='addTask' /> 
<!-- 子组件(TdoHead.vue)props接收数据 -->
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "HelloWorld",
  props: {
    addTask: Array as () => Array<any>
  },
  setup(props) {
    console.log(props.addTask)
  }
});
</script>

利用自定义事件传输父组件方法到子组件(暂未找到方法)

# 子接收父数据(setup)

利用标签名从父组件传输数据到子组件

<!-- 父组件标签(App.vue)传输数据(任意JS属性或方法) -->
<TdoHead  :addTask='addTask' /> 
<script setup lang="ts">
<!-- 子组件(TdoHead.vue)props接收数据 -->
import { defineComponent } from "vue";
const props = defineProps({
  addTask: Array
})
console.log(props.addTask)
</script>

利用自定义事件传输父组件方法到子组件(暂未找到方法)

# 子接受父标签

一、父组件利用子组件标签,传入实体标签

<!-- 传入子组件需要的标签 -->
<template> 
	<div slot="xxx">xxx 对应的标签结构</div>
	<div slot="yyy">yyy 对应的标签结构</div>
</template> 

二、子组件使用父组件传入的标签输出标签

<!-- 输出父组件标签 -->
<template>
	<slot name="xxx">父组件对应xxx的标签结构</slot>
	<div>组件确定的标签结构</div>
	<slot name="yyy">父组件对应yyy的标签结构</slot>
</template>

# 父接受子参数

一、子组件利用 slot 插槽传递任意属性 / 事件

<!-- 输出父组件标签, 传入参数 -->
<template>
	<slot name="xxx" :toggle="addCount" :count="count"></slot>
</template>

<script setup lang="ts">
import { ref } from "vue"
const count = ref(0)
const toggle = () => {
    count.value++
}
</script>

二、父组件从插槽中获取属性 / 事件

<!-- 传入子组件需要的标签, 接收参数 -->
<template> 
    <!-- slot-scope接收参数(废弃) -->
	<div slot="xxx" slot-scope="{ count, toggle }" @click="toggle"> {{ count }} </div>
    <!-- v-slot接收参数 -->
    <div v-slot:xxx="{ count, toggle }" @click="toggle"> {{ count }} </div>
</template> 

# 子组件间通信

# props通信

  1. 父组件定义状态数据 父组件定义改变状态数据方法
  2. 父组件传递状态数据给子组件B 父组件传递改变状态数据方法给子组件A
  3. 子组件A调用方法改变父组件状态数据 子组件B自动调用componentWillReceiveProps()方法并接收状态数据

# 消息订阅系统

  1. 引入消息订阅系统 import PubSub from 'pubsub-js'
  2. 发布消息 PubSub.publish('消息名',data)
  3. 订阅消息(当消息发送改变时执行,并接收数据) PubSub.subscribe('消息名',(msg, data){...})

# 瞬移与异步组件

# Teleport 瞬移组件

Vue 鼓励我们通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树。

然而,有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置。

一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。

考虑下面的 HTML 结构。

<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <modal-button></modal-button>
  </div>
</body>

让我们来看看 modal-button 组件:

该组件将有一个 button 元素来触发模态框的打开,以及一个带有 class .modaldiv 元素,它将包含模态框的内容和一个用于自关闭的按钮。

const app = Vue.createApp({});

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        I'm a modal! 
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

当在初始的 HTML 结构中使用这个组件时,我们可以看到一个问题——模态框是在深度嵌套的 div 中渲染的,而模态框的 position:absolute 以父级相对定位的 div 作为引用。

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。

让我们修改 modal-button 以使用 <teleport>,并告诉 Vue “Teleport 这个 HTML 该‘body’标签”。

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

因此,一旦我们单击按钮打开模态框,Vue 将正确地将模态框内容渲染为 body 标签的子级。

# Suspense 异步组件

Suspense 组件用于在等待某个异步组件解析时显示后备内容。

以下是异步组件有用的一些实例:

  • 在页面加载之前显示加载动画
  • 显示占位符内容
  • 处理延迟加载的图像

以前,在Vue2中,我们必须使用条件(例如 v-if 或 v-else)来检查我们的数据是否已加载并显示后备内容。

但是现在,Suspense随Vue3内置了,因此我们不必担心跟踪何时加载数据并呈现相应的内容。

# 实现异步组件处理

在这个例子中,我们有一个异步的 ArticleInfo.vue 组件。

setup 方法可以像其他方法一样被设置为异步,对于我们的示例,组件将具有异步 setup 方法,该方法将在返回之前加载用户数据。

async function getArticleInfo() { 
  // 一些异步API调用
  return { article }
}
export default {
  async setup () {
    var { article } = await getArticleInfo() 
    return { 
      article    
    }  
}} 

然后,假设我们有一个 ArticlePost.vue 组件,其中包含我们的ArticleInfo组件。

如果我们要在等待组件获取数据并解析时显示“正在拼了命的加载…”之类的内容,则只需三个步骤即可实现Suspense。

  • 将异步组件包装在<template #default>标记中
  • 在我们的Async组件的旁边添加一个兄弟姐妹,标签为<template #fallback>
  • 将两个组件都包装在<suspense>组件中

使用插槽,Suspense将渲染后备内容,直到默认内容准备就绪。然后,它将自动切换以显示我们的异步组件。

所以,看起来会像这样。

<Suspense> 
  <template #default> 
    <article-info/> 
  </template> 
  <template #fallback> 
    <div>正在拼了命的加载…</div> 
  </template> 
</Suspense> 

# 捕获异步组件错误

当我们开始使用异步组件时,可以捕获错误并向用户显示一些错误消息。

即使在Vue2中,也可以使用 errorCaptured 钩子函数实现,但是在Vue3中,它已重命名为 onErrorCaptured。

无论调用什么,此钩子函数都会在捕获到任何后代组件的错误时运行。如果出现问题,我们可以将其与Suspense一起使用以渲染错误。

如果我们处理了一个错误以显示错误消息,则上面的组件将是这样。

<template> 
  <div v-if="errMsg"> {{ errMsg }} </div> 
  <Suspense v-else> 
    <template #default> 
      <article-info /> 
    </template> 
    <template #fallback> 
      <div>正在拼了命的加载…</div> 
    </template> 
  </Suspense> 
</template> 
<script> 
import { onErrorCaptured } from 'vue' 
setup () { 
  const errMsg = ref(null) 
  onErrorCaptured(e => { 
    errMsg.value = '呃,出了点问题!' 
    return true 
  })} 
  return { error } 
</script> 

# 路由组件编程

Vue Router 4.0 提供了 Vue 3 支持,并有许多突破性的变化。

# Vue 路由管理器

# 定义路由组件

路由组件装载着需要的内容,通常这类组件放在 src/views 文件夹内(about.vue、home.vue)

# 定义路由控制器

import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import About from '../views/about.vue'

const routes: Array<RouteRecordRaw> = [
  { // 配置路由地址
    path: "/about",
    component: About,
    children: [ // 其他子路由
      // { path:'note', component: aboutl }
    ],
    meta: {} // $route元数据 router.meta
  },
  { // 配置异步路由组件
    path: "/home", component: () =>
      // 中间可添加打包后名称
      import(/* webpackChunkName: "home" */ "../views/home.vue")
  },
];

const router = createRouter({
  // 路由路径模式, 
  // createWebHashHistory对应hash模式,
  // createWebHistory对应history模式
  history: createWebHashHistory(),
  routes
});

export default router;

# Vue 入口函数引入路由

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router)
   .mount("#app");

# 静态组件使用路由组件

<div id="app">
	<router-link to="/about">Go to about</router-link>
	<router-link to="/home">Go to home</router-link>
	<router-view> </router-view>
</div>

1574341010

# 路由组件缓存

可让显示路由保存值和自身,在刷新页面时或重新启动浏览器不会消失

<keep-alive include="test-keep-alive"> <!-- 利用keep-alive标签包裹显示路由 -->
	<router-view class="w"></router-view>
</keep-alive>

# 组件中操作路由

# 组件中访问路由

<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
// 当前显示路由
const $route = useRoute();
// 路由管理器
const $router = useRouter();
</script>

# 组件中操纵跳转

<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
const $router = useRouter();
// 页面跳转(推入)
$router.push("/home")
// 页面跳转(替换)
$router.replace("/about")
// 页面回退(前提是前进页面)
$router.back()
// 页面前进(前提是返回页面)
$router.go(1)
</script>

# 路由跳转时传入参数

# 路径中传入占位符参数(params)

// 定义路由时, 定义占位符为id
roates: [{ path: '/home/:id', commponent: Home }]
<!-- 定义标签内跳转, 传入id为6 -->
<router-link :to="/home/6">User</router-link>
/* 定义命令跳转, 传入id为6 */
$router.push("/home/6")

# 对象中传入对象参数(params)

<!-- 定义标签内跳转, 传入id为6 -->
<router-link :to="{path: '/home', params: {id: 60}}">User</router-link>
/* 定义命令跳转, 传入id为6 */
$router.push({path: "/home", params: {id: 60}})

# 对象中传入查询参数(query)

<!-- 定义标签内跳转, 传入id为6 -->
<router-link :to="{path: '/home', query: {id: 60}}">User</router-link>
/* 定义命令跳转, 传入id为6 */
$router.push({path: "/home", query: {id: 60}})

# 监听路由参数变化

# 使用watch进行监听

import { watch } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
// 全局监听
watch(route, (to, from) => { });
// 单独监听
watch(route.params, (to, from) => {});

# 使用watchEffect进行监听

import { watchEffect } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
// 全局监听
watchEffect(() => {
  console.log("当前路由", route)
})
// 单独监听
watchEffect(() => {
  console.log("当前路由参数", route.params)
})

# 使用 beforeRouteUpdate 导航守卫

import { onBeforeRouteUpdate } from "vue-router";
onBeforeRouteUpdate((to, from, next) => {
  // 对路线变化做出反应...
  // 不要忘记调用 next()
});

# 导航守卫(路由拦截器)

# 全局前置守卫

使用 router.beforeEach 注册一个全局前置守卫:

const router = createRouter({
  history: createWebHashHistory(),
  routes
});
router.beforeEach((to, from, next) => {
  // ...
  next();
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。具体参数参考:

# 全局后置钩子

可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

router.afterEach((to, from) => {
  // ...
})

# 路由独享守卫

在路由配置上直接定义 beforeEnter 守卫:

const routes: Array<RouteRecordRaw> = [
  { path: '/foo',
    component: Foo,
    beforeEnter: (to, from, next) => {
      // ...
  }
  }
]

# 组件内的守卫

可以在路由组件内直接定义以下路由导航守卫:

import { onBeforeRouteUpdate, onBeforeRouteLeave } from "vue-router";
// 对路线变化做出反应, 不要忘记调用 next()
onBeforeRouteUpdate((to, from, next) => { });
// 导航离开该组件的对应路由时调用
onBeforeRouteLeave((to, from, next) => { });

# 当前路由路队列信息(matched)

// currentRoutePath
'/user/control/a'
// route.matched
[{path:'/user', ...}, {path: '/user/control',...},......]

#

# 动态路由添加(addRoute)

addRoutes 被 addRoute 替代,添加一条新的路由记录 (opens new window)作为现有路由的子路由。如果路由有一个 name,并且已经有一个与之名字相同的路由,它会先删除之前的路由。

// currentRoutes
[
    { path: '/login' component: /*....*/ }
]
// addRoute
routes.addRoute({ path: '/user', component: /*....*/ })
// currentRoutes
[
    { path: '/login' component: /*....*/ },
    { path: '/user', component: /*....*/ }
]

# addRoutes 兼容方法

const addRoutes = routes.forEach(route => router.addRoute(route))

# 退出登录后刷新路由方法

location.reload();

# 将 props 传递给路由组件

在你的组件中使用 $route 会与路由紧密耦合,这限制了组件的灵活性,因为它只能用于特定的 URL。虽然这不一定是件坏事,但我们可以通过 props 配置来解除这种行为:

我们可以将下面的代码

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]

替换成

const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]

这允许你在任何地方使用该组件,使得该组件更容易重用和测试。

#

# Vuex 集中式状态管理

Vuex 4.0 提供了 Vue 3 支持,其 API 与 3.x 基本相同。唯一的突破性变化是插件的安装方式 (opens new window)

# 简单store状态管理

# 1. 定义核心管理模块

// 新版创建方式由createStore创建
import { createStore } from "vuex";

// 暴露VuexStore
export default createStore({
  state: {}, // 状态对象
  mutations: {}, // 包含多个更新state函数的对象
  actions: {}, // 包含多个对应事件回调函数的对象
  getters: {}, // 包含多个getter计算属性函数的对象
  modules: {}  // 包含多个store模块的对象
});

# 2. 入口文件中引入

import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";

createApp(App)
  .use(store)
  .mount("#app");

# 模块化状态管理

# 1. 定义文件目录结构(src / store)

  • userInfo.ts / 状态对象 /
  • order.ts / 多个更新state函数的引用对象 /
// 定义 `modules` 中使用的对象模板
import { Module } from "vuex";
const createStore = <S>(store: Module<S, any>) => store
export default createStore({
  state: {}, // 状态对象
  mutations: {}, // 包含多个更新state函数的对象
  actions: {}, // 包含多个对应事件回调函数的对象
  getters: {}, // 包含多个getter计算属性函数的对象
  modules: {}  // 包含多个store模块的对象
})

# 2. 定义Vuex接口 (src / store / index.ts)

import { createStore } from "vuex";
import userInfo from "./userInfo"
import order from "./order"
export default createStore({
  modules: { // 包含多个store模块的对象
    userInfo,
    order
  }
});

# 模块访问全局状态

# action 获取全局状态

import { Module } from "vuex";
const createStore = <S>(store: Module<S, any>) => store
export default createStore({
  actions: {
    incrementRootCount({state, commit, rootState, rootGetters}) {
      console.log("全局状态: ", rootState)
      console.log("全局计算: ", rootGetters)
    }
  }
})

# getters 获取全局状态

import { Module } from "vuex";
const createStore = <S>(store: Module<S, any>) => store
export default createStore({
  getters: {
    allCount(state, getters, rootState) {
      console.log("当前储存库: ", state);
      console.log("当前储存库计算: ", getters);
      console.log("全局储存库: ", rootState);
    }
  }
})

# Vuex 组件 / 文件中使用

# 字符串模板访问 state

Vuexuse中引入后,全局属性将多出一个$store的属性,在ComponentAPI中,this并不能访问,但字符串模板可以访问。

<!-- 任意组件 -->
<div>{{ $store.state.cliNum }}</div>
<div>{{ $store.getters.type }}</div>

# 组件访问 state

Vue3中,this被废弃,获取当前状态由useStore钩子获取。

import { useStore } from "vuex";

const store = useStore();
console.log(store.state);
console.log(store.getters);

# 字符串模板调用 actions 函数

$store.dispatch会调用Vuex中的actions函数,第一个参数传入函数名称,第二个参数传入自定义参数,这里需要注意,$sotre.dispatch只能传输两个参数。

<!-- 任意组件 -->
<button @click="$store.dispatch('qaq', [a,b,c])">+</button>

# 组件调用 actions 函数

import { useStore } from "vuex";

const store = useStore();
store.dispatch('qaq', 5555)

# 第三方插件 VeeValidate 4.0

VeeValidate 是基于Vue的表单验证扩展,熟悉后可以很容易的设置声明式验证。带有直观的API和较小的占用空间,可以很快速的构建的表单验证。体积小,复杂度低,且可和任意的UI组件库搭配使用。带有内置规则,拥有全球化语音环境,是一块十分优秀的插件。

安装:npm i vee-validate@next --save

# 全局验证器

// plugins/vee-validate.ts (需初始化执行)
import { defineRule } from 'vee-validate';

// 添加必须项规则
defineRule('required', value => {
  if (!value || !value.length) {
    return '该选项为必选';
  }
  return true;
});

// 添加邮箱规则
defineRule('email', value => {
  // 字段为空, 应通过
  if (!value || !value.length) {
    return true;
  }
  // 监测是否是邮箱
  if (!/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/.test(value)) {
    return '邮箱格式不正确';
  }
  return true;
});

# 使用全局验证器

<template>
  <vee-from @submit="onSubmit" v-slot="{ isSubmitting }">
    <field name="name" as="input" rules="required" />
    <field name="email" as="input" rules="required|email" />
    <button :disabled="isSubmitting" :class="{ 'submitting': isSubmitting }">Submit</button>
    <error-message :name="name" />
  </vee-from>
</template>

<script setup lang="ts">
// 引入并暴露 vee-validate 组件
export { Field, Form as VeeForm, ErrorMessage } from "vee-validate";

// 表单通过验证
const onSubmit = (values: any) => {
  console.log(values);
};
</script>

# Vue 常见问题 | 扩展

在 Vue 开发中,遇到的疑难杂症,与发现的 Vue 相关扩展。

# 路由级别权限管理

  1. 定义好全部的路由地址以及映射地址
  2. 通过用户不同,向后台请求不同的路由权限数据
  3. 对用户权限做对比:请求数据 === 全部的路由,取出后添加路由配置

# 按钮级别权限管理

  1. 定义自定义指令,标签判断当前 route.mete 是否存在权限信息,不存在则移除DOM。

# 虚拟节点创建 (create vnode)

import { h } from 'vue'
// 传入组件内容, 与组件 props 参数, 生成虚拟节点
const vnode = h(Component, props)

// 组件内容可为导入组件, 例如
import Component from './Component.vue'
const vnode = h(Component, props)
// 也可为 template 例如
const vnode = h('<div>hello world!!</div>', props)
// 也可为节点的描述数据 例如
const vnode = h('div', { id: 'app' }, [h('span', '我是 span')])

# 虚拟节点渲染 (render vnode)

import { h, render } from 'vue'
// 渲染虚拟节点至真实节点上 ( 挂载组件 )
const container = document.createElement('div')
render(h(Component, props), container)
document.body.appendChild(container.firstElementChild)
// 销毁真实节点的所有实例 ( 卸载组件 )
render(null, container)

# 其他组合式 API

// 将 ref 转换为 .value
unref
// 浅响应式 ref
shallowRef
// 浅响应式 reactive
shallowReactive
// 将对象转换为只读对象
readonly
// 浅只读
shallowReadonly

# 标记与还原

  • toRaw
    • 返回由 reactivereadonly 方法转换成响应式代理的普通对象。
    • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
  • markRaw
    • 标记一个对象,使其永远不会转换为代理。返回对象本身
    • 应用场景:
      • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
      • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。

# 响应式判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

# 自定义 Ref

let value = ''
const vref = customRef((track, trigger) => {
    return {
        get() {
            // 初次调用 > 追踪依赖
            track()
        },
        set(_value) {
            // 修改完毕后, 通知 dep 更新
            value = _value
            trigger()
        }
    }
})

# TS 中常用的内置类型

// 所有 style 内容
CSSProperties
// 计算属性
ComputedRef<T>
// 监视源
WatchSource<T = any>
// 是或不是Ref
MaybeRef<T>