Vue3 渐进式JavaScript框架

1,827

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 发布会视频:www.bilibili.com/video/BV1iA…

全局 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 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTipremoved (见下方)
Vue.config.ignoredElementsapp.config.isCustomElement (见下方)
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use (见下方)

由于 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 中完整的细节,

Vuex

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

Devtools Extension

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

  • Chrome:从 Chrome web 商店中安装
    • 提示:beta 版本可能与 devtools 的稳定版本冲突,因此你可能需要暂时禁用稳定版本,以便 beta 版本正常工作。
  • Firefox:下载签名扩展 (assets 下的 .xpi 文件)

IDE 支持

推荐使用 VSCode 和我们官方拓展 Vetur,它为 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 定义响应式数据

定义数据(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";
// 定义单个响应式数据
export const count = ref(0);
// 通过改变count.value发生响应式变化
count.value++;

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

模板中使用

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

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";
export const firstName = ref("Aaa");
export const lastName = ref("bbbB");

export 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";
export const firstName = ref("Aaa");
export const lastName = ref("bbbB");

export 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>

指定数据源监视(watch)

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

import { ref, watch } from "vue";
export 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; // 是否初始化执行, 及以当前值执行

收集依赖并监视(effect)

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

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

停止监视

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

import { ref, watchEffect } from "vue";
export 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) { },
})

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

computed 替换过滤器

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

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

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 源码分析

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'
export const count = ref(0)
export const inc = () => count.value++
</script>

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

引入组件格式

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

<script lang="ts">
export { default as 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接收数据 -->
declare const props: {
  addTask: Array<any>
}
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"
export const count = ref(0)
export 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){...})

路由组件编程

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) => {});

使用effect?进行监听

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) => { });

Vuex 集中式状态管理

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

简单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";

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

第三方插件 ElementUI (开发中)