Vue3 + TypeScript 开发指南
0x00 概述
- 阅读以下内容需要具备一定的 Vue2 基础
- 代码采用规范为:TypeScript + 组合式 API + setup 语法糖
(1)Vue3 简介
- Vue3 第一个正式版发布于 2020 年 9 月 18 日
- Vue3 中文官网
- Vue3 相比 Vue2 的优势:
- 性能提升:打包体积减小,初次渲染和更新渲染都更快,内存使用减少
- 源码升级:使用 Proxy 实现响应式,重写虚拟 DOM 的实现和
Tree-Shaking
- 支持 TypeScript
- 新增特性:组合式 API、新内置组件、新生命周期钩子等
(2)TypeScript 概述
- TypeScript 入门:学习 TypeScript | 稀土掘金-SRIGT
0x01 第一个项目
(1)创建项目
创建项目共有两种方法
使用 vue-cli 创建
在命令提示符中使用命令
npm install -g @vue/cli
下载并全局安装 vue-cli如果已经安装过,则可以使用命令
vue -V
查看当前 vue-cli 的版本,版本要求在 4.5.0 以上如果需要多版本 vue-cli 共存,则可以参考文章:安装多版本Vue-CLI的实现方法 | 脚本之家-webgiser
使用命令
vue create project_name
开始创建项目使用方向键选择
Default ([Vue3 babel, eslint])
等待创建完成后,使用命令
cd project_name
进入项目目录使用命令
npm serve
启动项目
使用 vite 创建
相比使用 vue-cli 创建,使用 vite 的优势在于
- 轻量快速的热重载,实现极速的项目启动
- 对 TypeScript、JSX、CSS 等支持开箱即用
- 按需编译,减少等待编译的时间
- 使用命令
npm create vue@latest
创建 Vue3 项目 - 输入项目名称
- 添加 TypeScript 支持
- 不添加 JSX 支持、Vue Router、Pinia、Vitest、E2E 测试、ESLint 语法检查、Prettier 代码格式化
- 使用命令
cd [项目名称]
进入项目目录 - 使用命令
npm install
添加依赖 - 使用命令
npm run dev
启动项目
(2)项目结构
node_modules:项目依赖
public:项目公共资源
src:项目资源
assets:资源目录
components:组件目录
main.ts:项目资源入口文件
import './assets/main.css' // 引入样式文件 import { createApp } from 'vue' // 引入创建 Vue 应用方法
import App from './App.vue' // 引入 App 组件 createApp(App).mount('#app') // 创建以 App 为根组件的应用实例,并挂载到 id 为 app 的容器中(该容器为 index.html)
App.vue:应用根组件
<template>
<!-- 模型 -->
</template> <script lang="ts">
/* 控制 */
</script> <style>
/* 样式 */
</style>
env.d.ts:TypeScript 环境配置文件,包括文件类型声明等
index.html:项目入口文件
vite.config.ts:项目配置文件
0x02 核心语法
(1)组合式 API
首先使用 Vue2 语法完成一个组件
项目结构
graph TB
src-->components-->Person.vue
src-->App.vue & main.ts详细代码
main.ts
import { createApp } from 'vue'
import App from './App.vue' createApp(App).mount('#app')
App.vue
<template>
<div class="app">
<h1>App</h1>
<Person />
</div>
</template> <script lang="ts">
import Person from './components/Person.vue';
export default {
name: 'App', // 当前组件名
components: { // 注册组件
Person
}
}
</script> <style scoped>
.app {
padding: 20px;
}
</style>
Person.vue
<template>
<h2>Name: {{ name }}</h2>
<h2>Age: {{ age }}</h2>
<button @click="showDetail">Detail</button>
<hr />
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
</template> <script lang="ts">
export default {
name: "Person",
data() { // 数据配置
return { // 包含组件的所有数据
name: "John",
age: 18,
telephone: "1234567890",
email: "john@gmail.com"
}
},
methods: { // 方法配置
showDetail() { // 包含组件的全部方法
alert(`Detail: \ntelephone: ${this.telephone}\n email: ${this.email}`)
},
changeName() {
this.name = "Jane";
},
changeAge() {
this.age += 1;
}
}
}
</script> <style>
</style>
目前,使用 Vue2 语法完成的组件 Person 中使用了选项式 API(Options API),其中
name
、data
、methods
均称为选项(或称配置项)选项式 API 的问题在于:数据、方法、计算属性等分散在
data
、methods
、computed
等选项中,当需要新增或修改需求时,就需要分别修改各个选项,不便于维护和复用
Vue3 采用组合式 API
组合式 API(Composition API)优势在于:使用函数的方式组织代码,使相关代码更加有序的组织在一起,便于新增或修改需求
选项式 API 与 组合式 API 对比(下图来自《做了一夜动画,就为让大家更好的理解Vue3的Composition Api | 稀土掘金-大帅老猿》)
(2)setup
a. 概述
setup
是 Vue3 中一个新的配置项,值是一个函数,其中包括组合式 API,组件中所用到的数据、方法、计算属性等均配置在setup
中- 特点
setup
函数返回的可以是一个对象,其中的内容可以直接在模板中使用;也可以返回一个渲染函数setup
中访问this
是undefined
,表明 Vue3 已经弱化this
的作用setup
函数会在方法beforeCreate
之前调用,在所有钩子函数中最优先执行
b. 使用 setup
修改本章第(1)小节采用 Vue2 语法的项目
App.vue
<template>
<Person />
</template> <script lang="ts">
import Person from './components/Person.vue';
export default {
name: 'App',
components: {
Person
}
}
</script> <style scoped>
</style>
Person.vue
引入
setup
<script lang="ts">
export default {
name: "Person",
setup() {
}
}
</script>
在
setup
中声明数据<script lang="ts">
export default {
name: "Person",
setup() {
let name = "John"
let age = 18
let telephone = "1234567890"
let email = "john@gmail.com"
}
}
</script>
将需要使用的数据返回到模板中
可以为返回的变量设置别名:如
n: name
<template>
<h2>Name: {{ n }}</h2>
<h2>Age: {{ age }}</h2>
</template> <script lang="ts">
export default {
name: "Person",
setup() {
let name = "John"
let age = 18
let telephone = "1234567890"
let email = "john@gmail.com" return {
n: name,
age
}
}
}
</script> <style>
</style>
在
setup
中声明方法并返回<template>
<h2>Name: {{ n }}</h2>
<h2>Age: {{ age }}</h2>
<button @click="showDetail">Detail</button>
<hr />
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
</template> <script lang="ts">
export default {
name: "Person",
setup() {
// let ... function showDetail() {
alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`)
}
function changeName() {
name = "Jane";
}
function changeAge() {
age += 1;
} return {
n: name,
age,
showDetail,
changeName,
changeAge
}
}
}
</script> <style>
</style>
此时可以发现,点击“姓名修改”或“年龄修改”按钮后,页面并未发生改变,这是因为之前使用
let
声明的数据不是响应式的,具体方法参考本章第 x 小节
setup
可以与data
和methods
同时存在在
data
或methods
中使用this
均可以访问到在setup
中声明的数据与函数- 因为
setup
的执行早于data
和methods
<template>
<h2>Age: {{ a }}</h2>
</template> <script lang="ts">
export default {
name: "Person",
data() {
return { a: this.age }
},
setup() {
let name = "John"
let age = 18
return { n: name, age }
}
}
</script>
- 因为
在
setup
中无法使用data
或methods
中声明的数据或函数
c. 语法糖
在上述项目中,每次在
setup
中声明新数据时,都需要在return
中进行“注册”,之后才能在模板中使用该数据,为解决此问题,需要使用setup
语法糖<script setup lang="ts"></script>
相当于如下代码:<script lang="ts">
export default {
setup() {
return {}
}
}
</script>
修改 Person.vue
<script setup lang="ts">
let name = "John";
let age = 18;
let telephone = "1234567890";
let email = "john@gmail.com"; function showDetail() {
alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`);
}
function changeName() {
name = "Jane";
}
function changeAge() {
age += 1;
}
</script>
在
<script setup lang="ts"></script>
中无法直接设置组件的name
属性,因此存在以下两种方式进行设置:使用两个
<script>
标签<script lang="ts">
export default {
name: "Person"
}
</script>
<script setup lang="ts">
// ...
</script>
(推荐)基于插件实现
使用命令
npm install -D vite-plugin-vue-setup-extend
安装需要的插件在 vite.config.ts 中引入并调用这个插件
// import ...
import vpvse from 'vite-plugin-vue-setup-extend' export default defineConfig({
plugins: [
vue(),
vpvse(),
],
// ...
})
重新启动项目
<script setup lang="ts" name="Person">
// ...
</script>
将 App.vue 中的 Vue2 语法修改为 Vue3
<template>
<Person />
</template> <script setup lang="ts" name="App">
import Person from './components/Person.vue';
</script> <style scoped>
</style>
(3)创建基本类型的响应式数据
使用
ref
创建
引入
ref
<script setup lang="ts" name="Person123">
import {ref} from 'vue'
</script>
对需要成为响应式的数据使用
ref
<script setup lang="ts" name="Person123">
import {ref} from 'vue' let name = ref("John");
let age = ref(18);
let telephone = "1234567890";
let email = "john@gmail.com";
</script>
使用
console.log()
可以发现使用ref
的name
变成了// RefImpl
{
"__v_isShallow": false,
"dep": {},
"__v_isRef": true,
"_rawValue": "John",
"_value": "John"
}
未使用
ref
的telephone
依然是字符串'1234567890'
修改方法
<script setup lang="ts" name="Person123">
import {ref} from 'vue' let name = ref("John");
let age = ref(18);
let telephone = "1234567890";
let email = "john@gmail.com"; function showDetail() {
alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`);
}
function changeName() {
name.value = "Jane";
}
function changeAge() {
age.value += 1;
}
</script>
在函数方法中使用
name
之类被响应式的数据,需要在属性value
中获取或修改值,而在模板中不需要<template>
<h2>Name: {{ name }}</h2>
<h2>Age: {{ age }}</h2>
<button @click="showDetail">Detail</button>
<hr />
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
</template>
(4)创建对象类型的响应式数据
a. 使用 reactive
创建
创建对象以及函数方法,并在模板中使用
<template>
<h2>Name: {{ Teacher.name }}</h2>
<h2>Age: {{ Teacher.age }}</h2>
<p><button @click="changeTeacherAge">Change Teacher Age</button></p>
</template> <script setup lang="ts" name="Person">
let Teacher = { name: "John", age: 18 } function changeTeacherAge() {
Teacher.age += 1;
}
</script>
引入
reactive
<script setup lang="ts" name="Person">
import {reactive} from 'vue' // ...
</script>
对需要成为响应式的对象使用
reactive
<script setup lang="ts" name="Person">
import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) // ...
</script>
- 使用
console.log()
可以发现John
变成了 Proxy(Object) 类型的对象
- 使用
按钮方法不做调整:
Teacher.age += 1;
添加对象数组以及函数方法,并在模板中遍历
<template>
<!-- ... -->
<hr />
<ul>
<li v-for="student in Student" :key="student.id">{{ student.name }}</li>
</ul>
<button @click="changeFirstStudentName">Change First Student Name</button>
</template> <script setup lang="ts" name="Person">
// ...
let Student = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
] // ...
function changeFirstStudentName() {
Student[0].name = "Alex";
}
</script>
将对象数组变为响应式
<script setup lang="ts" name="Person">
// ...
let Student = reactive([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
]) // ...
</script>
此时点击按钮即可修改第一个学生的名字为 Alex
b. 使用 ref
创建
修改上述使用
reactive
的组件内容<template>
<h2>Name: {{ Teacher.name }}</h2>
<h2>Age: {{ Teacher.age }}</h2>
<p><button @click="changeTeacherAge">Change Teacher Age</button></p>
<hr />
<ul>
<li v-for="student in Student" :key="student.id">{{ student.name }}</li>
</ul>
<button @click="changeFirstStudentName">Change First Student Name</button>
</template> <script setup lang="ts" name="Person">
let Teacher = { name: "John", age: 18 }
let Student = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
] function changeTeacherAge() {}
function changeFirstStudentName() {}
</script>
引入
ref
并为需要成为响应式的对象使用<script setup lang="ts" name="Person">
import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 })
let Student = ref([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
]) // ...
</script>
修改函数方法
<script setup lang="ts" name="Person">
// ... function changeTeacherAge() {
Teacher.value.age += 1;
}
function changeFirstStudentName() {
Student.value[0].name = "Alex";
}
</script>
c. 对比 ref
与 reactive
功能上:
ref
可以用来定义基本类型数据和对象类型数据- 在使用
ref
创建响应式对象过程中,ref
底层实际借用了reactive
的方法
- 在使用
reactive
可以用来定义对象类型数据
区别在于:
使用
ref
创建的数据必须使用.value
解决方法:使用插件 volar 自动添加
插件设置方法:
- 点击左下角齿轮图标(管理),并选择“设置”
- 在左侧选项列表中依次选择“扩展”-“Volar”
- 将功能“Auto Insert: Dot Value”打勾即可
使用
reactive
重新分配一个新对象会失去响应式(reactive
的局限性)解决方法:使用
Object.assign
整体替换举例:修改
Teacher
对象reactive
<template>
<h2>Name: {{ Teacher.name }}</h2>
<h2>Age: {{ Teacher.age }}</h2>
<p><button @click="changeTeacher">Change Teacher</button></p>
</template> <script setup lang="ts" name="Person">
import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) function changeTeacher() {
Object.assign(Teacher, { name: "Mary", age: 19 })
}
</script>
ref
<template>
<h2>Name: {{ Teacher.name }}</h2>
<h2>Age: {{ Teacher.age }}</h2>
<p><button @click="changeTeacher">Change Teacher</button></p>
</template> <script setup lang="ts" name="Person">
import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 }) function changeTeacher() {
Teacher.value = { name: "Mary", age: 19 }
}
</script>
使用原则:
- 若需要一个基本类型的响应式数据,则只能选择
ref
- 若需要一个对象类型、层级较浅的响应式数据,则选择任何一个都可以
- 若需要一个对象类型、层级较深的响应式数据,则推荐选择
reactive
- 若需要一个基本类型的响应式数据,则只能选择
(5)toRefs
和 toRef
- 作用:将一个响应式对象中的每一个属性转换为
ref
对象 toRefs
与toRef
功能相同,toRefs
可以批量转换
修改 Person.vue
<template>
<h2>Name: {{ John.name }}</h2>
<h2>Age: {{ John.age }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
</template> <script setup lang="ts" name="Person">
import {reactive} from 'vue' let John = reactive({ name: "John", age: 18 }) function changeName() {
John.name += "~"
}
function changeAge() {
John.age += 1
}
</script> <style>
</style>
声明两个新变量,并使用
Person
对其进行赋值,之后修改模板<template>
<h2>Name: {{ name }}</h2>
<h2>Age: {{ age }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
</template> <script setup lang="ts" name="Person">
// ... let John = reactive({ name: "John", age: 18 })
let { name, age } = John // ...
</script>
此时,新变量
name
和age
并非响应式,点击按钮无法在页面上修改(实际上已发生修改)引入
toRefs
,将变量name
和age
变为响应式<script setup lang="ts" name="Person">
import {reactive, toRefs} from 'vue' let John = reactive({ name: "John", age: 18 })
let { name, age } = toRefs(John) // ...
</script>
修改函数方法
<script setup lang="ts" name="Person">
// ... function changeName() {
name.value += "~"
}
function changeAge() {
age.value += 1
}
</script>
引入
toRef
替代toRefs
<script setup lang="ts" name="Person">
import {reactive, toRef} from 'vue' let John = reactive({ name: "John", age: 18 })
let name = toRef(John, 'name')
let age = toRef(John, 'age') function changeName() {
name.value += "~"
}
function changeAge() {
age.value += 1
}
</script>
(6)computed
computed
是计算属性,具有缓存,当计算方法相同时,computed
会调用缓存,从而优化性能
修改 Person.vue
<template>
<h2>First Name: <input type="text" v-model="firstName" /></h2>
<h2>Last Name: <input type="text" v-model="lastName" /></h2>
<h2>Full Name: <span>{{ firstName }} {{ lastName }}</span></h2>
</template> <script setup lang="ts" name="Person">
import {ref} from 'vue' let firstName = ref("john")
let lastName = ref("Smith")
</script> <style>
</style>
引入计算属性
computed
<script setup lang="ts" name="Person">
import {ref, computed} from 'vue' // ...
</script>
将姓与名的首字母大写,修改模板与控制器
<template>
<!-- ... -->
<h2>Full Name: <span>{{ fullName }}</span></h2>
</template> <script setup lang="ts" name="Person">
// ... let fullName = computed(() => {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
})
</script>
此时的
fullName
是只读的,无法修改,是一个使用ref
创建的响应式对象修改
fullName
,实现可读可写<script setup lang="ts" name="Person">
// ... let fullName = computed({
get() {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
},
set() {}
})
</script>
在模板中添加按钮,用于修改
fullName
,并在控制器中添加相应的函数方法<template>
<!-- ... -->
<button @click="changeFullName">Change Full Name</button>
</template> <script setup lang="ts" name="Person">
// ... function changeFullName() {
fullName.value = "bob jackson"
}
</script>
修改计算属性中的
set()
方法<script setup lang="ts" name="Person">
// ... let fullName = computed({
get() {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
},
set(value) {
const [newFirstName, newLastName] = value.split(" ")
firstName.value = newFirstName
lastName.value = newLastName
}
}) // ...
</script>
(7)watch
watch
是监视属性- 作用:监视数据变化(与 Vue2 中的
watch
作用一致) - 特点:Vue3 中的
watch
只监视以下数据:ref
定义的数据reactive
定义的数据- 函数返回一个值
- 一个包含上述内容的数组
a. 情况一:监视 ref
定义的基本类型数据
直接写数据名即可,监视目标是
value
值的改变
修改 Person.vue
<template>
<h2>Sum: {{ sum }}</h2>
<button @click="changeSum"> +1 </button>
</template> <script setup lang="ts" name="Person">
import {ref} from 'vue' let sum = ref(0) function changeSum() {
sum.value += 1
}
</script> <style>
</style>
引入
watch
<script setup lang="ts" name="Person">
import {ref, watch} from 'vue' // ...
</script>
使用
watch
,一般传入两个参数,依次是监视目标与相应的回调函数<script setup lang="ts" name="Person">
import {ref, watch} from 'vue' // ... watch(sum, (newValue, oldValue) => {
console.log("Sum changed from " + oldValue + " to " + newValue)
})
</script>
修改
watch
,设置“停止监视”<script setup lang="ts" name="Person">
// ... const stopWatch = watch(sum, (newValue, oldValue) => {
console.log("Sum changed from " + oldValue + " to " + newValue)
if(newValue >= 10) {
stopWatch()
}
})
</script>
b. 情况二:监视 ref
定义的对象类型数据
- 直接写数据名即可,监视目标是对象的地址值的改变
- 如需监视对象内部的数据,需要手动开启深度监视
- 若修改的是
ref
定义的对象中的属性,则newValue
和newValue
都是新值,因为它们是同一个对象- 若修改整个
ref
定义的对象,则newValue
是新值,oldValue
是旧值,因为它们不是同一个对象
修改 Person.vue
<template>
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.age }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
<hr />
<p><button @click="changeAll">Change All</button></p>
</template> <script setup lang="ts" name="Person">
import { ref, watch } from "vue" let person = ref({
name: "John",
age: 18
}) function changeName() {
person.value.name += '~'
}
function changeAge() {
person.value.age += 1
}
function changeAll() {
person.value = {
name: "Mary",
age: 19
}
}
</script> <style>
</style>
使用
watch
监视整个对象的地址值变化<script setup lang="ts" name="Person">
// ... watch(person, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
此时,只有点击“Change All” 按钮才会触发监视,
newValue
与oldValue
不同手动开启深度监视,监视对象内部的数据
<script setup lang="ts" name="Person">
// ... watch(
person,
(newValue, oldValue) => {
console.log(newValue, oldValue)
},
{
deep: true,
}
)
</script>
此时,点击“Change Name”或“Change Age”也能触发监视,
newValue
与oldValue
相同,但是点击“Change All”时,newValue
与oldValue
依旧不同
c. 情况三:监视 reactive
定义的对象类型数据
该情况下,默认开启了深度监视且无法关闭
修改 Person.vue
<template>
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.age }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
<hr />
<p><button @click="changeAll">Change All</button></p>
</template> <script setup lang="ts" name="Person">
import { reactive, watch } from "vue" let person = reactive({
name: "John",
age: 18,
}) function changeName() {
person.name += "~"
}
function changeAge() {
person.age += 1
}
function changeAll() {
Object.assign(person, {
name: "Mary",
age: 19,
})
}
</script> <style>
</style>
使用
watch
监视对象<script setup lang="ts" name="Person">
// ... watch(person, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
对于使用
reactive
创建的对象,在使用Object.assign()
时,仅修改了对象里的内容(覆盖原来的内容),并非创建了新对象,故无法监视对象地址值的变化(因为没有变化)
d. 情况四:监视 ref
或 reactive
定义的对象类型数据中的某个属性
若该属性值不是对象类型,则需写成函数形式
若该属性值是对象类型,则建议写成函数形式
修改 Person.vue
<template>
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.age }}</h2>
<h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
<hr />
<p><button @click="changeNickname1">Change Nickname 1</button></p>
<p><button @click="changeNickname2">Change Nickname 2</button></p>
<p><button @click="changeNickname">Change Nickname</button></p>
</template> <script setup lang="ts" name="Person">
import { reactive, watch } from "vue" let person = reactive({
name: "John",
age: 18,
nickname: {
n1: "J",
n2: "Jack"
}
}) function changeName() {
person.name += "~"
}
function changeAge() {
person.age += 1
}
function changeNickname1() {
person.nickname.n1 = "Big J"
}
function changeNickname2() {
person.nickname.n2 = "Joker"
}
function changeNickname() {
person.nickname = {
n1: "Joker",
n2: "Big J"
}
}
</script> <style>
</style>
使用
watch
监视全部的变化<script setup lang="ts" name="Person">
// ... watch(person, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
修改
watch
,设置监视基本类型数据person.name
<script setup lang="ts" name="Person">
// ... watch(
() => {
return person.name;
},
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
})
</script>
修改
watch
,设置监视对象类型数据person.nickname
<script setup lang="ts" name="Person">
// ... watch(
() => person.nickname,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{ deep: true }
})
</script>
以下写法仅能监视对象类型内部属性数据变化
<script setup lang="ts" name="Person">
// ... watch(
person.nickname,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
})
</script>
以下写法仅能监视对象类型整体地址值变化
<script setup lang="ts" name="Person">
// ... watch(
() => person.nickname,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
})
</script>
e. 情况五:监视多个数据
修改 Person.vue
<template>
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.age }}</h2>
<h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2>
<p><button @click="changeName">Change Name</button></p>
<p><button @click="changeAge">Change Age</button></p>
<hr />
<p><button @click="changeNickname1">Change Nickname 1</button></p>
<p><button @click="changeNickname2">Change Nickname 2</button></p>
<p><button @click="changeNickname">Change Nickname</button></p>
</template> <script setup lang="ts" name="Person">
import { reactive, watch } from "vue" let person = reactive({
name: "John",
age: 18,
nickname: {
n1: "J",
n2: "Jack"
}
}) function changeName() {
person.name += "~"
}
function changeAge() {
person.age += 1
}
function changeNickname1() {
person.nickname.n1 = "Big J"
}
function changeNickname2() {
person.nickname.n2 = "Joker"
}
function changeNickname() {
person.nickname = {
n1: "Joker",
n2: "Big J"
}
}
</script> <style>
</style>
使用
watch
监视person.name
和person.nickname.n1
<script setup lang="ts" name="Person">
// ... watch(
[() => person.name, () => person.nickname.n1],
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{ deep: true }
)
</script>
(8)watchEffect
- 作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
watchEffect
与watch
对比,两者都能监视响应式数据,但是监视数据变化的方式不同watch
需要指明监视的数据watchEffect
则不用指明,函数中用到哪些属性,就监视哪些属性
修改 Person.vue
<template>
<h2>Sum1: {{ sum1 }}</h2>
<h2>Sum2: {{ sum2 }}</h2>
<p><button @click="changeSum1">Sum1 +1</button></p>
<p><button @click="changeSum2">Sum2 +3</button></p>
</template> <script setup lang="ts" name="Person">
import { ref, watch } from 'vue' let sum1 = ref(0)
let sum2 = ref(0) function changeSum1() {
sum1.value += 1
}
function changeSum2() {
sum2.value += 3
}
</script> <style>
</style>
使用
watch
监视sum1
和sum2
,获取最新的值<script setup lang="ts" name="Person">
// ... watch([sum1, sum2], (value) => {
console.log(value)
})
</script>
对
sum1
和sum2
进行条件判断<script setup lang="ts" name="Person">
// ... watch([sum1, sum2], (value) => {
let [newSum1, newSum2] = value
if(newSum1 >= 10 || newSum2 >= 30) {
console.log('WARNING: Sum1 or Sum2 is too high')
}
})
</script>
此时,仅对
sum1
和sum2
进行监视,当需要监视的目标更多时,建议使用watchEffect
引入
watchEffect
<script setup lang="ts" name="Person">
import { ref, watch, watchEffect } from 'vue' // ...
</script>
使用
watchEffect
<script setup lang="ts" name="Person">
// ... watchEffect(() => {
if(sum1.value >= 10 || sum2.value >= 30) {
console.log('WARNING: Sum1 or Sum2 is too high')
}
})
</script>
(9)标签的 ref
属性
- 作用:用于注册模板引用
- 用在普通 DOM 标签上,获取到 DOM 节点
- 用在组件标签上,获取到组件实例对象
修改 Person.vue
<template>
<h2>Vue 3</h2>
<button @click="showH2">Show Element H2</button>
</template> <script setup lang="ts" name="Person">
function showH2() {
console.log()
}
</script> <style>
</style>
引入标签的
ref
属性<template>
<h2 ref="title">Vue 3</h2>
<button @click="showH2">Show Element H2</button>
</template> <script setup lang="ts" name="Person">
import { ref } from 'vue' // ...
</script>
创建一个容器,用于存储
ref
标记的内容容器名称与标签中
ref
属性值相同<script setup lang="ts" name="Person">
import { ref } from 'vue' let title = ref() // ...
</script>
修改函数方法
showH2()
,输出标签<script setup lang="ts" name="Person">
import { ref } from 'vue' let title = ref() function showH2() {
console.log(title.value)
}
</script>
此时,
ref
属性用在普通 DOM 标签上,获取到 DOM 节点在 App.vue 中,对组件 Person 设置
ref
属性<template>
<Person ref="person" />
<hr />
<button @click="showPerson">Show Element Person</button>
</template> <script setup lang="ts" name="App">
import Person from './components/Person.vue';
import { ref } from 'vue' let person = ref() function showPerson() {
console.log(person.value)
}
</script> <style scoped>
</style>
此时,
ref
属性用在组件标签上,获取到组件实例对象在 Person.vue 中引入
defineExpose
实现父子组件通信<script setup lang="ts" name="Person">
import { ref, defineExpose } from 'vue' let title = ref()
let number = ref(12345) function showH2() {
console.log(title.value)
} defineExpose({ title, number })
</script>
此时,再次点击按钮“Show Element Person”便可在控制台中看到来自 Person.vue 组件中的
title
和number
重置 App.vue
<template>
<Person />
</template> <script setup lang="ts" name="App">
import Person from './components/Person.vue'
</script> <style scoped>
</style>
(9.5)TypeScript 回顾
TypeScript 基本概述:学习 TypeScript | 稀土掘金-SRIGT
在 Vue3 项目中,TS 接口等位于 ~/src/types/index.ts 中
定义接口,用于限制对象的具体属性
interface IPerson {
id: string,
name: string,
age: number
}
暴露接口
暴露接口有三种方法:默认暴露、分别暴露、统一暴露,以下采用分别暴露方法
export interface IPerson {
id: string,
name: string,
age: number
}
修改 Person.vue,声明新变量,引入接口对新变量进行限制
<template>
</template> <script setup lang="ts" name="Person">
import { type IPerson } from '@/types' let person:IPerson = {
id: "dpb7e82nlh",
name: "John",
age: 18
}
</script> <style>
</style>
此时,仅声明了一个变量,对于同类型的数组型变量,需要使用泛型
修改 Perosn.vue,声明一个数组,使用泛型
<script setup lang="ts" name="Person">
import { type IPerson } from '@/types' let personList:Array<IPerson> = [
{ id: "dpb7e82nlh", name: "John", age: 18 },
{ id: "u55dyu86gh", name: "Mary", age: 19 },
{ id: "ad3d882dse", name: "Niko", age: 17 }
]
</script>
修改 index.ts,定义一个自定义类型,简化对数组限制的使用
export interface IPerson {
id: string,
name: string,
age: number,
} // 自定义类型
export type Persons = Array<IPerson>
或:
export type Persons = IPerson[]
修改 Person.vue
<script setup lang="ts" name="Person">
import { type Persons } from '@/types' let personList:Persons = [
{ id: "dpb7e82nlh", name: "John", age: 18 },
{ id: "u55dyu86gh", name: "Mary", age: 19 },
{ id: "ad3d882dse", name: "Niko", age: 17 }
]
</script>
(10)props
修改 App.vue
<template>
<Person />
</template> <script setup lang="ts" name="App">
import Person from './components/Person.vue' import { reactive } from 'vue' let personList = reactive([
{ id: "dpb7e82nlh", name: "John", age: 18 },
{ id: "u55dyu86gh", name: "Mary", age: 19 },
{ id: "ad3d882dse", name: "Niko", age: 17 }
])
</script> <style scoped>
</style>
引入接口并使用
<script setup lang="ts" name="App">
// ...
import { type Persons } from '@/types'; let personList = reactive<Persons>([
{ id: "dpb7e82nlh", name: "John", age: 18 },
{ id: "u55dyu86gh", name: "Mary", age: 19 },
{ id: "ad3d882dse", name: "Niko", age: 17 }
])
</script>
修改父组件 App.vue 的模板内容,向子组件 Person.vue 发送数据
<template>
<Person note="This is a note" :list="personList" />
</template>
修改子组件 Person.vue,从父组件 App.vue 中接收数据
<template>
<h2>{{ note }}</h2>
<ul>
<li v-for="person in list" :key="person.id">{{ person.name }}-{{ person.age }}</li>
</ul>
</template> <script setup lang="ts" name="Person">
import { defineProps } from 'vue' // 只接收
// defineProps(['note', 'list']) // 接收并保存
let personList = defineProps(['note', 'list'])
console.log(personList)
console.log(personList.note)
</script> <style>
</style>
接收限制类型
<script setup lang="ts" name="Person">
import { defineProps } from 'vue'
import type { Persons } from '@/types' defineProps<{ list:Persons }>()
</script>
接收限制类型并指定默认值
<script setup lang="ts" name="Person">
import { defineProps } from 'vue'
import type { Persons } from '@/types' withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: "dpb7e82nlh", name: "John", age: 18 }],
})
</script>
重置 App.vue
<template>
<Person />
</template> <script setup lang="ts" name="App">
import Person from './components/Person.vue'
</script> <style scoped>
</style>
(11)生命周期
- 组件的生命周期包括:创建、挂载、更新、销毁/卸载
- 组件在特定的生命周期需要调用特定的生命周期钩子(生命周期函数)
a. Vue2 的生命周期
Vue2 生命周期包括八个生命周期钩子
生命周期 生命周期钩子 创建 创建前 beforeCreate
创建完成后 created
挂载 挂载前 beforeMount
挂载完成后 mounted
更新 更新前 beforeUpdate
更新完成后 updated
销毁 销毁前 beforeDestroy
销毁完成后 destroyed
使用命令
vue create vue2_test
或vue init webpack vue2_test
创建一个 Vue2 项目重置 ~/src/App.vue
<template>
</template> <script>
export default {
name: 'App',
}
</script> <style>
</style>
在 ~/src/components 中新建 Person.vue
<template>
<div>
<h2>Sum: {{ sum }}</h2>
<button @click="changeSum">+1</button>
</div>
</template> <script>
export default {
// eslint-disable-next-line
name: 'Person',
data() {
return {
sum: 0
}
},
methods: {
changeSum() {
this.sum += 1
}
}
}
</script> <style scoped>
</style>
在 App.vue 引入 Person.vue
<template>
<PersonVue />
</template> <script>
import PersonVue from './components/Person.vue' export default {
name: 'App',
components: {
PersonVue
}
}
</script> <style>
</style>
使用生命周期钩子
<script>
export default {
// eslint-disable-next-line
name: 'Person',
data() {
// ...
},
methods: {
// ...
}, // 创建前
beforeCreate() {
console.log("beforeCreate")
},
// 创建完成后
created() {
console.log("created")
}, // 挂载前
beforeMount() {
console.log("beforeMount")
},
// 挂载完成后
mounted() {
console.log("mounted")
}, // 更新前
beforeUpdate() {
console.log("beforeUpdate")
},
// 更新完成后
updated() {
console.log("updated")
}, // 销毁前
beforeDestroy() {
console.log("beforeDestroy")
},
// 销毁完成后
destroyed() {
console.log("destroyed")
}
}
</script>
使用命令
npm run serve
启动项目,在开发者工具中查看生命周期过程
b. Vue3 的生命周期
Vue3 生命周期包括七个生命周期钩子
生命周期 生命周期钩子 创建 setup
挂载 挂载前 onBeforeMount
挂载完成后 onMounted
更新 更新前 onBeforeUpdate
更新完成后 onUpdated
卸载 卸载前 onBeforeUnmount
卸载完成后 onUnmounted
在之前的 Vue3 项目中,修改 Person.vue
<template>
<h2>Sum: {{ sum }}</h2>
<button @click="changeSum">+1</button>
</template> <script setup lang="ts" name="Person">
import { ref } from 'vue' let sum = ref(0) function changeSum() {
sum.value += 1
}
</script> <style>
</style>
引入并使用生命周期钩子
<script setup lang="ts" name="Person">
import {
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onUnmounted,
onUpdated,
ref
} from 'vue' // ... // 创建
console.log("setup") // 挂载前
onBeforeMount(() => {
console.log("onBeforeMount")
})
// 挂载完成后
onMounted(() => {
console.log("onMounted")
}) // 更新前
onBeforeUpdate(() => {
console.log("onBeforeUpdate")
})
// 更新完成后
onUpdated(() => {
console.log("onUpdated")
}) // 卸载前
onBeforeUnmount(() => {
console.log("onBeforeUnmount")
})
// 卸载完成后
onUnmounted(() => {
console.log("onUnmounted")
})
</script>
(12)自定义Hooks
使用命令
npm install -S axios
安装 Axios,用于网络请求
修改 Person.vue
<template>
<h2>Sum: {{ sum }}</h2>
<button @click="changeSum">+1</button>
<hr />
<img
v-for="(img, index) in imgList"
:src="img"
:key="index"
style="height: 100px"
/>
<p><button @click="changeImg">Next</button></p>
</template> <script setup lang="ts" name="Person">
import { reactive, ref } from "vue"; let sum = ref(0);
let imgList = reactive([""]); function changeSum() {
sum.value += 1;
}
function changeImg() {}
</script> <style>
</style>
引入 Axios
<script setup lang="ts" name="Person">
import axios from 'axios'
// ...
</script>
使用 Axios 获取图片地址
<script setup lang="ts" name="Person">
import axios from 'axios'
// ...
async function changeImg() {
try {
let result = await axios.get(
"https://dog.ceo/api/breed/pembroke/images/random"
);
imgList.push(result.data.message);
} catch (error) {
alert("Error! Please try again.");
console.log(error);
}
}
</script>
此时,多个功能(求和、请求图片)同时在组件中互相交织,可以使用 Hooks 重新组织代码
在 src 目录下新建目录 hooks,其中分别新建 useSum.ts、useImg.ts
useSum.ts
import { ref } from 'vue' export default function () {
let sum = ref(0) function changeSum() {
sum.value += 1
} return {
sum,
changeSum
}
}
useImg.ts
import axios from 'axios'
import { reactive } from 'vue' export default function () {
let imgList = reactive([""]) async function changeImg() {
try {
let result = await axios.get(
"https://dog.ceo/api/breed/pembroke/images/random"
)
imgList.push(result.data.message)
} catch (error) {
alert("Error! Please try again.")
console.log(error)
}
} return {
imgList,
changeImg
}
}
在 Person.vue 中引入并使用两个 Hooks
<script setup lang="ts" name="Person">
import useSum from '@/hooks/useSum'
import useImg from '@/hooks/useImg' const { sum, changeSum } = useSum()
const { imgList, changeImg } = useImg()
</script>
0x03 路由
(1)概述
- 此处的路由是指前后端交互的路由
- 路由(route)就是一组键值的对应关系
- 多个路由需要经过路由器(router)的管理
- 路由组件通常存放在 pages 或 views 文件夹,一般组件通常存放在 components 文件夹
(2)路由切换
重置 ~/src 目录结构
graph TB
src-->components & pages & App.vue & main.ts
components-->Header.vue
pages-->Home.vue & Blog.vue & About.vue修改 App.vue(样式可忽略)
<template>
<div class="app">
<Header />
<div class="navigation">
<a href="/home" class="active">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</div>
<div class="main-content">
Content
</div>
</div>
</template> <script setup lang="ts" name="App">
import Header from './components/Header.vue'
</script> <style scoped>
.app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.navigation {
width: 15%;
height: 506px;
float: left;
background: #03deff;
}
.navigation a {
display: block;
text-decoration-line: none;
color: black;
text-align: center;
font-size: 28px;
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 3px solid #fff;
}
.navigation a.active {
background: rgb(1, 120, 144);
color: white;
}
.main-content {
display: inline;
width: 80%;
height: 500px;
float: left;
margin-left: 10px;
font-size: 26px;
border: 3px solid #000;
}
</style>
修改 Header.vue、Home.vue、Blog.vue、About.vue
Header.vue
<template>
<h2 class="title">Route Test</h2>
</template> <script setup lang="ts" name="Header">
</script> <style scope>
.title {
width: 100%;
text-align: center;
}
</style>
Home.vue、Blog.vue、About.vue
<template>
<h2>Home</h2>
<!-- <h2>Blog</h2> -->
<!-- <h2>About</h2> -->
</template> <script setup lang="ts">
</script> <style scope>
</style>
创建路由器
使用命令
npm install vue-router -S
安装路由器在 ~/src 目录下新建目录 router,其中新建 index.ts,用于创建一个路由器并暴露
引入
createRouter
和createWebHistory
import {
createRouter,
createWebHistory
} from 'vue-router'
其中
createWebHistory
是路由器的工作模式,在本章第(3)小节有详细介绍引入子组件
// ... import Home from '@/pages/Home.vue'
import Blog from '@/pages/Blog.vue'
import About from '@/pages/About.vue'
创建路由器
// ... const router = createRouter({
history: createWebHistory(),
routes: []
})
制定路由规则
// ...
routes: [
{
path: '/home',
component: Home
},
{
path: '/blog',
component: Blog
},
{
path: '/about',
component: About
}
]
// ...
暴露路由
// ... export default router
修改 main.ts,引入并使用路由器
// ...
import router from './router' createApp(App).use(router).mount('#app')
修改 App.vue,引入路由器视图,修改模板中的超链接
<template>
<div class="app">
<Header />
<div class="navigation">
<RouterLink to="/home" active-class="active">Home</RouterLink>
<RouterLink to="/blog" active-class="active">Blog</RouterLink>
<RouterLink to="/about" active-class="active">About</RouterLink>
</div>
<div class="main-content">
<RouterView></RouterView>
</div>
</div>
</template> <script setup lang="ts" name="App">
import Header from './components/Header.vue'
import { RouterLink, RouterView } from 'vue-router'
</script> <style scoped>
/* ... */
</style>
- 此时,点击导航后,“消失”的路由组件默认是被卸载的,需要的时候再重新挂载
- 可以在组件中使用生命周期钩子验证
<RouterLink>
中的to
属性有两种写法to="/home"
:to="{path:'/home'}"
- 此写法的优点在本章第()小节介绍
- 此时,点击导航后,“消失”的路由组件默认是被卸载的,需要的时候再重新挂载
(3)路由器的工作模式
a. history 模式
优点:URL 更美观,更接近于传统网站的 URL
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 报错
使用方法:
Vue2:
mode: 'history'
Vue3:
// router/index.ts
import {
createRouter,
createWebHistory
} from 'vue-router' const router = createRouter({
history: createWebHistory(),
// ...
})
React:
BrowserRouter
b. hash 模式
优点:兼容性更好,不需要服务端处理路径
缺点:URL 中带有
#
号,在 SEO 优化方面相对较差使用方法:
Vue3:
// router/index.ts
import {
createRouter,
createWebHashHistory
} from 'vue-router' const router = createRouter({
history: createWebHashHistory(),
// ...
})
(4)命名路由
- 作用:简化路由跳转与传参
在 router/index.ts 中为路由命名
// ...
routes: [
{
name: 'zy',
path: '/home',
component: Home
},
{
name: 'bk',
path: '/blog',
component: Blog
},
{
name: 'gy',
path: '/about',
component: About
}
]
// ...
修改 App.vue,使用命名路由的方法进行跳转
<template>
<!-- ... -->
<div class="navigation">
<RouterLink :to="{ name: 'zy' }" active-class="active">Home</RouterLink>
<RouterLink to="/blog" active-class="active">Blog</RouterLink>
<RouterLink to="/about" active-class="active">About</RouterLink>
</div>
<!-- ... -->
</template>
此时,共有两类方式三种方法实现路由跳转:
字符串写法 to="/home"
对象写法 命名跳转 :to="{ name: 'zy' }"
路径跳转 :to="{ path: '/home' }"
(5)嵌套路由
在博客页面实现路由的嵌套,实现博客内容根据选择进行动态展示
在 pages 目录下新建 Detail.vue
<template>
<ul>
<li>id: id</li>
<li>title: title</li>
<li>content: content</li>
</ul>
</template> <script setup lang="ts" name="detail">
</script> <style scope>
ul {
list-style: none;
padding-left: 20px;
}
ul>li {
line-height: 30px;
}
</style>
修改 router/index.ts,引入 Detail.vue
// ...
import Detail from '@/pages/Detail.vue' const router = createRouter({
// ...
routes: [
// ...
{
path: '/blog',
component: Blog,
children: [
{
path: 'detail',
component: Detail
}
]
},
// ...
]
}) export default router
修改 Blog.vue,添加博客导航列表、博客内容展示区、相关数据以及样式
<template>
<h2>Blog</h2>
<ul>
<li v-for="blog in blogList" :key="blog.id">
<RouterLink to="/blog/detail">{{ blog.title }}</RouterLink>
</li>
</ul>
<div class="blog-content">
<RouterView></RouterView>
</div>
</template> <script setup lang="ts" name="Blog">
import { reactive } from 'vue' const blogList = reactive([
{ id: 'fhi27df4sda', title: 'Blog01', content: 'Content01' },
{ id: 'opdcd2871cb', title: 'Blog02', content: 'Content02' },
{ id: 'adi267f4hp5', title: 'Blog03', content: 'Content03' }
])
</script> <style scope>
ul {
float: left;
}
ul li {
display: block;
}
ul li a {
text-decoration-line: none;
}
.blog-content {
float: left;
margin-left: 200px;
width: 70%;
height: 300px;
border: 3px solid black;
}
</style>
(6)路由传参
- 在 Vue 中,路由用两种参数:query 和 params
a. query
修改 Blog.vue,发送参数
修改
to
属性为:to
,使用模板字符串,在路由后添加?
,之后使用格式key1=val1&key2=val2
的方式传递参数<template>
<!-- ... -->
<li v-for="blog in blogList" :key="blog.id">
<RouterLink :to="`/blog/detail?id=${blog.id}&title=${blog.title}&content=${blog.content}`">
{{ blog.title }}
</RouterLink>
</li>
<!-- ... -->
</template>
简化传参
<template>
<!-- ... -->
<RouterLink
:to="{
path: '/blog/detail',
query: {
id: blog.id,
title: blog.title,
content: blog.content
}
}"
>
{{ blog.title }}
</RouterLink>
<!-- ... -->
</template>
修改 Detail.vue,接收参数
使用 Hooks
useRoute
接收<template>
<ul>
<li>id: {{ route.query.id }}</li>
<li>title: {{ route.query.title }}</li>
<li>content: {{ route.query.content }}</li>
</ul>
</template> <script setup lang="ts" name="detail">
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
简化数据展示
<template>
<ul>
<li>id: {{ query.id }}</li>
<li>title: {{ query.title }}</li>
<li>content: {{ query.content }}</li>
</ul>
</template> <script setup lang="ts" name="detail">
import { toRefs } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
let { query } = toRefs(route)
</script>
此时,模板中仍旧使用了很多
query.xxx
的语法,为省略query.
,本章第(7)小节有相关处理方法
b. params
修改 router/index.ts
// ...
children: [
{
path: 'detail/:id/:title/:content',
component: Detail
}
]
// ...
可以在相关参数后添加
?
设置参数的必要性,如:path: 'detail/:id/:title/:content?'
修改 Blog.vue,发送参数
<template>
<!-- ... -->
<RouterLink :to="`/blog/detail/${blog.id}/${blog.title}/${blog.content}`">
{{ blog.title }}
</RouterLink>
<!-- ... -->
</template>
简化传参
修改 router/index.ts,为
/detail
路由命名// ...
children: [
{
name: 'detail',
path: 'detail/:id/:title/:content',
component: Detail
}
]
// ...
修改 Blog.vue
<template>
<!-- ... -->
<RouterLink
:to="{
name: 'detail',
params: {
id: blog.id,
title: blog.title,
content: blog.content
}
}"
>
{{ blog.title }}
</RouterLink>
<!-- ... -->
</template>
修改 Detail.vue,接收参数
<template>
<ul>
<li>id: {{ route.params.id }}</li>
<li>title: {{ route.params.title }}</li>
<li>content: {{ route.params.content }}</li>
</ul>
</template> <script setup lang="ts" name="detail">
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
简化数据展示
<template>
<ul>
<li>id: {{ params.id }}</li>
<li>title: {{ params.title }}</li>
<li>content: {{ params.content }}</li>
</ul>
</template> <script setup lang="ts" name="detail">
import { toRefs } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
let { params } = toRefs(route)
</script>
(7)路由的 props 配置
- 作用:让路由组件更方便地接收参数
- 原理:将路由参数作为
props
传递给组件
a. 对象写法
修改 Blog.vue
<template>
<!-- ... -->
<RouterLink
:to="{
name: 'detail',
query: {
id: blog.id,
title: blog.title,
content: blog.content
}
}"
>
{{ blog.title }}
</RouterLink>
<!-- ... -->
</template>
修改 router/index.ts
{
name: 'detail',
path: 'detail',
component: Detail,
props(route) {
return route.query
}
}
此时相当于将
<Detail />
修改为<Detail id=xxx title=xxx content=xxx />
,可以按照第二章第(10)小节的方法将参数从 props 中取出使用修改 Detail.vue
<template>
<ul>
<li>id: {{ id }}</li>
<li>title: {{ title }}</li>
<li>content: {{ content }}</li>
</ul>
</template> <script setup lang="ts" name="detail">
defineProps(['id', 'title', 'content'])
</script>
b. 布尔值写法
修改 Blog.vue
<template>
<!-- ... -->
<RouterLink
:to="{
name: 'detail',
params: {
id: blog.id,
title: blog.title,
content: blog.content
}
}"
>
{{ blog.title }}
</RouterLink>
<!-- ... -->
</template>
修改 router/index.ts
// ...
{
name: 'detail',
path: 'detail/:id/:title/:content',
component: Detail,
props: true
}
// ...
修改 Detail.vue
c. 对象写法
不常用
修改 router/index.ts
// ...
{
name: 'detail',
path: 'detail/:id/:title/:content',
component: Detail,
props: {
id: xxx,
title: yyy,
content: zzz
}
}
// ...
修改 Detail.vue
(8)replace
默认情况下,采用 push 模式,即记录浏览先后顺序,允许前进和回退
replace 模式不允许前进和回退
修改 App.vue,在
RouterLink
标签中添加replace
属性<template>
<!-- ... -->
<RouterLink replace to="/home" active-class="active">Home</RouterLink>
<RouterLink replace to="/blog" active-class="active">Blog</RouterLink>
<RouterLink replace to="/about" active-class="active">About</RouterLink>
<!-- ... -->
</template>
(9)编程式导航
RouterLink
标签的本质是a
标签编程式导航的目的是脱离
RouterLink
标签,实现路由跳转修改 Home.vue,实现进入该页面 3 秒后,以 push 的模式跳转到 /blog
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router' const router = useRouter() onMounted(() => {
setTimeout(() => {
router.push('/blog')
}, 3000)
})
</script>
重置 Home.vue
修改 Blog.vue
<template>
<h2>Blog</h2>
<ul>
<li v-for="blog in blogList" :key="blog.id">
<button @click="showDetail(blog)">More</button>
<!-- ... -->
</li>
<!-- ... -->
</template> <script setup lang="ts" name="Blog">
import { useRouter } from "vue-router"; // ... const router = useRouter(); interface IBlog {
id: string;
title: string;
content: string;
} function showDetail(blog: IBlog) {
router.push({
name: "detail",
query: {
id: blog.id,
title: blog.title,
content: blog.content,
},
});
}
</script>
(10)重定向
修改 router/index.ts,将路径 / 重定向到 /home
routes: [
{
path: '/',
redirect: '/home'
},
// ...
]
此时,访问 http://localhost:5173/ 时,会重定向到 http://localhost:5173/home
0x04 pinia
(1)概述
pinia 是 Vue3 中的集中式状态管理工具
- 类似的工具有:redux(React)、vuex(Vue2)等
- 集中式:将所有需要管理的数据集中存放在一个容器中,相对的称为分布式
(2)准备
重置 ~/src 目录结构
graph TB
src-->components & App.vue & main.ts
components-->Count.vue & Text.vueCount.vue
<template>
<div class="count">
<h2>Sum: {{ sum }}</h2>
<select v-model.number="number">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add">Add</button>
<button @click="sub">Sub</button>
</div>
</template> <script setup lang="ts" name="Count">
import { ref } from 'vue' let sum = ref(0)
let number = ref(1) function add() {
sum.value += number.value
}
function sub() {
sum.value -= number.value
}
</script> <style scope>
.count {
text-align: center;
width: 50%;
height: 120px;
background: #03deff;
border: 3px solid black;
}
select, button {
margin: 10px;
}
</style>
当在
select
标签中进行选择时,为向变量number
中传入数字,可以使用以下方法之一:- 修改
select
标签中的v-model
为v-model.number
(上面使用的方法) - 修改
option
标签中的value
为:value
- 修改
Text.vue
<template>
<div class="text">
<button @click="getText">Get Text</button>
<ul>
<li v-for="text in textList" :key="text.id">
{{ text.content }}
</li>
</ul>
</div>
</template> <script setup lang="ts" name="Text">
import { reactive } from 'vue'
import axios from 'axios' let textList = reactive([
{ id: '01', content: "Text01" },
{ id: '02', content: "Text02" },
{ id: '03', content: "Text03" }
]) async function getText() {
let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
textList.unshift({ id: Date.now().toString(), content})
console.log(content)
}
</script> <style scope>
.text {
width: 50%;
height: 150px;
background: #fbff03;
border: 3px solid black;
padding-left: 10px;
}
</style>
App.vue
<template>
<Count />
<br />
<Text />
</template> <script setup lang="ts" name="App">
import Count from './components/Count.vue'
import Text from './components/Text.vue'
</script> <style scope>
</style>
main.ts
import { createApp } from 'vue'
import App from './App.vue' createApp(App).mount('#app')
(3)搭建环境
使用命令
npm install -S pinia
安装 pinia在 main.ts 中引入并安装 pinia
import { createApp } from 'vue'
import App from './App.vue' import { createPinia } from 'pinia' createApp(App).use(createPinia()).mount('#app')
(4)存储与读取数据
store 是一个保存状态、业务逻辑的实体,每个组件都可以读写它
store 中有三个概念,与组件中的一些属性类似
store 属性 state
data
getter
computed
actions
methods
store 目录下的 ts 文件命名与组件的相同
以下涉及对文件、变量等命名的格式均符合 pinia 官方文档规范
在 ~/src 目录下,新建 store 目录,其中新建 count.ts 和 text.ts
count.ts
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', {
state() {
return {
sum: 10
}
}
})
text.ts
import { defineStore } from 'pinia' export const useTextStore = defineStore('text', {
state() {
return {
textList: [
{ id: '01', content: "Text01" },
{ id: '02', content: "Text02" },
{ id: '03', content: "Text03" }
]
}
}
})
修改 Count.vue 和 Text.vue
Count.vue
<template>
<div class="count">
<h2>Sum: {{ countStore.sum }}</h2>
<select v-model.number="number">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add">Add</button>
<button @click="sub">Sub</button>
</div>
</template> <script setup lang="ts" name="Count">
import { ref } from 'vue'
import { useCountStore } from '@/store/count' const countStore = useCountStore() let number = ref(1) function add() {}
function sub() {}
</script>
Text.vue
<template>
<div class="text">
<button @click="getText">Get Text</button>
<ul>
<li v-for="text in textStore.textList" :key="text.id">
{{ text.content }}
</li>
</ul>
</div>
</template> <script setup lang="ts" name="Text">
import { useTextStore } from '@/store/text' const textStore = useTextStore() async function getText() {}
</script>
(5)修改数据
方式一:直接手动修改
修改 Count.vue
<template>
<div class="count">
<!-- ... -->
<br />
<button @click="change">Change</button>
</div>
</template> <script setup lang="ts" name="Count">
// ...
import { useCountStore } from '@/store/count' const countStore = useCountStore()
// ...
function change() {
countStore.sum = 100
}
</script>
方式二:手动批量修改
修改 Count.vue
<script setup lang="ts" name="Count">
// ...
function change() {
countStore.$patch({
sum: 100,
num: 1
})
}
</script>
方式三:使用
actions
修改修改 count.ts
import { defineStore } from 'pinia' export const useCountStore = defineStore('count', {
// ...
actions: {
increment(value:number) {
if(this.sum < 20) {
this.sum += value
}
}
}
})
修改 Count.vue
<script setup lang="ts" name="Count">
// ... function add() {
countStore.increment(number.value)
}
// ...
</script>
(6)storeToRefs
对于从 store 中获得的数据,可以按以下方法在模板中展示
<template>
<div class="count">
<h2>Sum: {{ countStore.sum }}</h2>
<!-- ... -->
</template> <script setup lang="ts" name="Count">
// ...
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
</script>
为使模板更加简洁,可以使用
toRefs
将sum
从 store 中解构出来<template>
<div class="count">
<h2>Sum: {{ sum }}</h2>
<!-- ... -->
</template> <script setup lang="ts" name="Count">
// ...
import { toRefs } from 'vue'
const { sum } = toRefs(countStore)
</script>
直接使用
toRefs
会将 store 中携带的方法也变为响应式数据,因此可以使用 pinia 提供的storeToRefs
<script setup lang="ts" name="Count">
// ...
import { storeToRefs } from 'pinia'
const { sum } = storeToRefs(countStore)
</script>
(7)getters
- 在
state
中的数据需要经过处理后再使用时,可以使用getters
配置
修改 store/count.ts
export const useCountStore = defineStore('count', {
state() {
return {
sum: 10
}
},
getters: {
bigSum(state) {
return state.sum * 100
}
},
// ...
})
使用
this
可以替代参数state
的传入getters: {
bigSum(): number {
return this.sum * 100
}
},
如果不使用
this
则可以使用箭头函数getters: {
bigSum: state => state.sum * 100
},
修改 Count.vue,使用
bigSum
<template>
<div class="count">
<h2>Sum: {{ sum }}, BigSum: {{ bigSum }}</h2>
<!-- ... -->
</template> <script setup lang="ts" name="Count">
// ...
const { sum, bigSum } = storeToRefs(countStore)
</script>
(8)$subscribe
- 作用:订阅,监听
state
及其变化
修改 Text.vue
<script setup lang="ts" name="Text">
// ...
textStore.$subscribe(() => {
console.log('state.textList changed')
})
</script>
此时,点击按钮后,在开发者工具中可以看到输出了“state.textList changed”
$subscribe
中可以添加参数mutate
:本次修改的数据state
:当前的数据
<script setup lang="ts" name="Text">
// ...
textStore.$subscribe((mutate, state) => {
console.log(mutate, state)
})
</script>
使用
state
,借助浏览器本地存储,实现数据变化后,不会在刷新后初始化修改 Text.vue
<script setup lang="ts" name="Text">
import { useTextStore } from '@/store/text' const textStore = useTextStore()
textStore.$subscribe((mutate, state) => {
localStorage.setItem('textList', JSON.stringify(state.textList))
}) function getText() {
textStore.getText()
}
</script>
修改 store/text.ts
import axios from 'axios'
import { defineStore } from 'pinia' export const useTextStore = defineStore('text', {
state() {
return {
textList: JSON.parse(localStorage.getItem('textList') as string) || []
}
},
actions: {
async getText() {
let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
this.textList.unshift({ id: Date.now().toString(), content})
console.log(content)
}
}
})
(9)store 组合式写法
之前的内容使用了类似 Vue2 语法的选项式写法
修改 Text.ts
import axios from 'axios'
import { defineStore } from 'pinia'
import { reactive } from 'vue' export const useTextStore = defineStore('text', () => {
const textList = reactive(JSON.parse(localStorage.getItem('textList') as string) || []) async function getText() {
let { data: { result: { content } } } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
textList.unshift({ id: Date.now().toString(), content })
console.log(content)
} return {
textList,
getText
}
})
0x05 组件通信
(0)概述
Vue3 中移出了事件总线,可以使用
pubsub
代替- Vuex 换成了 pinia
.sync
优化到v-model
中$listeners
合并到$attrs
中
重置 ~/src 目录结构
graph TB
src-->components & App.vue & main.tsParent.vue
<template>
<div class="parent">
<h2>Parent</h2>
<Child />
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
</script> <style scope>
.parent {
width: 50%;
padding: 20px;
background: #03deff;
border: 3px solid black;
}
</style>
Child.vue
<template>
<div class="child">
<h2>Child</h2>
</div>
</template> <script setup lang="ts" name="Child">
</script> <style scope>
.child {
width: 50%;
padding: 20px;
background: #fbff03;
border: 3px solid black;
}
</style>
App.vue
<template>
<Parent />
</template> <script setup lang="ts" name="App">
import Parent from './components/Parent.vue'
</script> <style scope>
</style>
main.ts
import { createApp } from 'vue'
import App from './App.vue' createApp(App).mount('#app')
共有 \(7+2\) 种通信方式
(1)props
a. 父组件向子组件传递数据
修改 Parent.vue,在控制器中创建数据,在模板标签中发送数据
<template>
<div class="parent">
<h2>Parent</h2>
<h4>Parent's number: {{ numberParent }}</h4>
<Child :number="numberParent" />
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
import { ref } from 'vue' let numberParent = ref(12345)
</script>
修改 Child.vue,接收数据
<template>
<div class="child">
<h2>Child</h2>
<h4>Child's number: {{ numberChild }}</h4>
<h4>Number from Parent: {{ number }}</h4>
</div>
</template> <script setup lang="ts" name="Child">
import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number'])
</script>
b. 子组件向父组件传递数据
修改 Parent.vue,创建用于接收子组件发送数据的方法,将方法发送给子组件
<template>
<div class="parent">
<h2>Parent</h2>
<h4>Parent's number: {{ numberParent }}</h4>
<h4 v-show="number">Number from Child: {{ number }}</h4>
<Child :number="numberParent" :sendNumber="getNumber" />
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
import { ref } from 'vue' let numberParent = ref(12345)
let number = ref(null) function getNumber(value: Number) {
number.value = value
}
</script>
修改 Child.vue,接收父组件发送的方法,并添加按钮使用该方法
<template>
<div class="child">
<h2>Child</h2>
<h4>Child's number: {{ numberChild }}</h4>
<h4>Number from Parent: {{ number }}</h4>
<button @click="sendNumber(numberChild)">Send Child's number to Parent</button>
</div>
</template> <script setup lang="ts" name="Child">
import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number', 'sendNumber'])
</script>
(2)自定义事件
获取事件,即使用
$event
<template>
<div>
<button @click="getEvent($event)">Event</button>
</div>
</template> <script setup lang="ts">
function getEvent(event: Event) {
console.log(event)
}
</script>
修改 Child.vue,声明自定义事件
<template>
<div class="child">
<h2>Child</h2>
<h4>Child's number: {{ numberChild }}</h4>
<button @click="emit('custom-event', numberChild)">Send Child's number to Parent</button>
</div>
</template> <script setup lang="ts" name="Child">
import { ref } from 'vue' let numberChild = ref(12345) const emit = defineEmits(['custom-event'])
</script>
修改 Parent.vue,绑定自定义事件
<template>
<div class="parent">
<h2>Parent</h2>
<h4 v-show="number">Number from Child: {{ number }}</h4>
<Child @custom-event="getNumber" />
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
import { ref } from 'vue' let number = ref(null) function getNumber(value: Number) {
number.value = value
}
</script>
(3)mitt
a. 概述与准备
- 可以实现任意组件通信
- 与 pubsub、$bus 相类似,都是消息订阅与发布
- 接收方:提前绑定事件,即订阅消息
- 发送方:适时触发事件,即发布消息
使用命令
npm install -S mitt
安装 mitt在 ~/src 目录下新建 utils 目录,其中新建 emitter.ts,引入、调用、暴露 mitt
import mitt from 'mitt' const emitter = mitt() export default emitter
emitter.all()
:获取所有绑定事件emitter.emit()
:触发指定事件emitter.off()
:解绑指定事件emitter.on()
:绑定指定事件
b. 基本使用方法
在 main.ts 中引入 emitter.ts
// ...
import emitter from './utils/emitter'
修改 utils/emitter.ts,绑定事件
import mitt from 'mitt' const emitter = mitt() emitter.on('event1', () => {
console.log('event1')
})
emitter.on('event2', () => {
console.log('event2')
}) export default emitter
触发事件
// ...
setInterval(() => {
emitter.emit('event1')
emitter.emit('event2')
}, 1000) export default emitter
解绑事件
// ...
setTimeout(() => {
emitter.off('event1')
console.log('event1 off')
}, 3000) export default emitter
解绑所有事件
setTimeout(() => {
emitter.all.clear()
console.log('All clear')
}, 3000)
c. 实际应用
目录结构:
graph TB
src-->components & utils & App.vue & main.ts
components-->Child1.vue & Child2.vue & Parent.vue
utils-->emitter.ts
Child1.vue(Child2.vue)
<template>
<div class="child">
<h2>Child1</h2>
<h4>Name: {{ name }}</h4>
</div>
</template> <script setup lang="ts" name="Child1">
import { ref } from 'vue' // Child1.vue
let name = ref('Alex')
// Child2.vue
// let name = ref('Bob')
</script> <style scope>
.child {
width: 50%;
padding: 20px;
background: #fbff03;
border: 3px solid black;
}
</style>
Parent.vue
<template>
<div class="parent">
<h2>Parent</h2>
<Child1 />
<br />
<Child2 />
</div>
</template> <script setup lang="ts" name="Parent">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
</script> <style scope>
.parent {
width: 50%;
padding: 20px;
background: #03deff;
border: 3px solid black;
}
</style>
emitter.ts
import mitt from 'mitt' const emitter = mitt() export default emitter
修改 Child2.vue,绑定事件
<template>
<div class="child">
<!-- ... -->
<h4 v-show="brotherName">Brother's name: {{ brotherName }}</h4>
</div>
</template> <script setup lang="ts" name="Child2">
// ...
import emitter from '@/utils/emitter' let brotherName = ref('') emitter.on('send-name', (value: string) => {
brotherName.value = value
})
</script>
修改 Child1.vue,发送数据
<template>
<div class="child">
<!-- ... -->
<button @click="emitter.emit('send-name', name)">Send Name</button>
</div>
</template> <script setup lang="ts" name="Child1">
// ...
import emitter from '@/utils/emitter'
</script>
修改 Child2.vue,卸载组件时解绑事件
<script setup lang="ts" name="Child2">
// ...
import { ref, onUnmounted } from 'vue' onUnmounted(() => {
emitter.off('send-name')
})
</script>
(4)v-model
- 实际开发中不常用,常见于 UI 组件库
a. HTML 标签
修改 Parent.vue,将
v-model
用在input
标签上<template>
<div class="parent">
<h2>Parent</h2>
<input type="text" v-model="name" placeholder="Enter your name" />
</div>
</template> <script setup lang="ts" name="Parent">
import { ref } from 'vue' let name = ref("John")
</script> <style scope>
.parent {
width: 50%;
padding: 20px;
background: #03deff;
border: 3px solid black;
}
</style>
在
input
标签中使用v-model
相当于:<input type="text" :value="name" @input="name=(<HTMLInputElement>$event.target).value" />
(<HTMLInputElement>$event.target)
:TypeScript 断言检查
b. 组件标签
在 components 中新建 CustomInput.vue
修改 Parent.vue
<template>
<div class="parent">
<h2>Parent</h2>
<CustomInput :modelValue="name" @update:modelValue="name = $event" />
</div>
</template> <script setup lang="ts" name="Parent">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue' let name = ref("John")
</script>
修改 CustomInput.vue
<template>
<input
type="text"
:value="modelValue"
@input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)"
/>
</template> <script setup lang="ts" name="CustomInput">
defineProps(["modelValue"])
const emit = defineEmits(["update:modelValue"])
</script> <style scope>
</style>
- 对于
$event
的target
判定:- 当触发的是原生事件时,
$event
是事件对象,需要.target
- 当触发的是自定义事件时,
$event
是触发事件数据,不需要.target
- 当触发的是原生事件时,
- 对于
修改 Parent.vue,使用
v-model
<template>
<div class="parent">
<h2>Parent</h2>
<!-- <CustomInput :modelValue="name" @update:modelValue="name = $event" /> -->
<CustomInput v-model="name" />
</div>
</template>
此时,可以通过设置
v-model
,实现修改modelValue
以及添加多个v-model
修改 Parent.vue
<template>
<div class="parent">
<!-- ... -->
<CustomInput v-model:m1="name1" v-model:m2="name2" />
</div>
</template> <script setup lang="ts" name="Parent">
// ...
let name1 = ref("John")
let name2 = ref("Mary")
</script>
修改 CustomInput.vue
<template>
<input
type="text"
:value="m1"
@input="emit('update:m1', (<HTMLInputElement>$event.target).value)"
/> <input
type="text"
:value="m2"
@input="emit('update:m2', (<HTMLInputElement>$event.target).value)"
/>
</template> <script setup lang="ts" name="CustomInput">
defineProps(["m1", "m2"])
const emit = defineEmits(["update:m1", "update:m2"])
</script>
(5)$attrs
$attrs
是一个对象,包含所有父组件传入的标签属性,用于实现祖父组件向孙子组件通信
目录结构:
graph TB
components-->Grand.vue & Parent.vue & Child.vue
修改 Grand.vue,创建数据,并通过 props 发送
<template>
<div class="grand">
<h2>Grand</h2>
<h4>Number: {{ number }}</h4>
<Parent :number="number" />
</div>
</template> <script setup lang="ts" name="Grand">
import Parent from './Parent.vue'
import { ref } from 'vue' let number = ref(123)
</script>
修改 Parent.vue,使用
$attrs
将来自 Grand.vue 的数据以 props 的方式转发给 Child.vue<template>
<div class="parent">
<h2>Parent</h2>
<Child v-bind="$attrs"/>
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
</script>
修改 Child.vue,接收数据并展示
<template>
<div class="child">
<h2>Child</h2>
<h4>Number from Grand: {{ number }}</h4>
</div>
</template> <script setup lang="ts" name="Child">
defineProps(['number'])
</script>
此时,祖父组件 Grand.vue 也可以向孙子组件 Child.vue 传递方法,从而可以在孙子组件中修改祖父组件的数据
(6)$refs
& $parent
父子组件通信
属性 作用 说明 $refs
父组件向子组件通信 值
为
对
象包含所有被 ref
属性标识的 DOM 元素或组件实例$parent
子组件向父组件通信 值为对象,当前组件的父组件实例对象
目录结构:
graph TB
components-->Parent.vue & Child1.vue & Child2.vue
a. $refs
修改 Child1.vue 和 Child2.vue,创建数据并允许访问
<template>
<div class="child">
<h2>Child1</h2>
<!--
Child2.vue
<h2>Child2</h2>
-->
<h4>Number: {{ number }}</h4>
</div>
</template> <script setup lang="ts" name="Child1">
import { ref } from 'vue' let number = ref(456)
// Child2.vue
// let number = ref(789) defineExpose({ number })
</script>
修改 Parent.vue,设置按钮使其能够修改子组件中的
number
<template>
<div class="parent">
<h2>Parent</h2>
<button @click="changeNumber">Change Child1's number</button>
<hr />
<Child1 ref="c1" />
<br />
<Child2 ref="c2" />
</div>
</template> <script setup lang="ts" name="Parent">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref } from 'vue' let c1 = ref()
let c2 = ref() function changeNumber() {
c1.value.number = 123
c2.value.number = 123
}
</script>
使用
$refs
,使其能够批量修改<template>
<div class="parent">
<!-- ... -->
<button @click="getAll($refs)">Get All Child's number</button>
<!-- ... -->
</div>
</template> <script setup lang="ts" name="Parent">
// ...
function getAll(refs: any) {
for (let key in refs) {
refs[key].number = 123
}
}
</script>
b. $parent
修改 Parent.vue,创建数据并允许访问
<template>
<div class="parent">
<h2>Parent</h2>
<h4>Number: {{ number }}</h4>
<hr />
<Child1 />
<br />
<Child2 />
</div>
</template> <script setup lang="ts" name="Parent">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref } from 'vue' let number = ref(123) defineExpose({ number })
</script>
修改 Child1.vue
<template>
<div class="child">
<h2>Child1</h2>
<button @click="changeNumber($parent)">Change Number</button>
</div>
</template> <script setup lang="ts" name="Child1">
function changeNumber(parent: any) {
parent.number = 456
}
</script>
(7)provide
& inject
祖孙组件通信
使用方法:
graph LR
A(祖父组件)--provide 发送数据-->B(孙子组件)--inject 接收数据-->A
目录结构:
graph TB
components-->Grand.vue & Parent.vue & Child.vue
修改 Grand.vue,创建数据
<template>
<div class="grand">
<h2>Grand</h2>
<h4>Number: {{ number }}</h4>
<Parent />
</div>
</template> <script setup lang="ts" name="Grand">
import Parent from './Parent.vue'
import { ref } from 'vue' let number = ref(123)
</script>
使用
provide
发送数据<script setup lang="ts" name="Grand">
// ...
import { ref, provide } from 'vue' provide('number', number)
</script>
修改 Child.vue,接收并展示数据
<template>
<div class="child">
<h2>Child</h2>
<h4>Number from Grand: {{ number }}</h4>
</div>
</template> <script setup lang="ts" name="Child">
import { inject } from 'vue'
let number = inject('number')
</script>
当
inject
的数据不存在时,可以展示默认值<template>
<div class="child">
<h2>Child</h2>
<h4>Number from Grand: {{ number }}</h4>
<h4>Number from Parent: {{ numberP }}</h4>
</div>
</template> <script setup lang="ts" name="Child">
import { inject } from 'vue'
let number = inject('number')
let numberP = inject('numberP', 456)
</script>
修改 Grand.vue,提供方法用于修改祖父组件中的数据
<script setup lang="ts" name="Grand">
// ...
function changeNumber() {
number.value = 456
} provide('number', {
number, changeNumber
})
</script>
修改 Child.vue,接收方法并使用按钮触发
<template>
<div class="child">
<h2>Child</h2>
<h4>Number from Grand: {{ number }}</h4>
<button @click="changeNumber">Change Number</button>
</div>
</template> <script setup lang="ts" name="Child">
import { inject } from 'vue'
let { number, changeNumber } = inject('number')
</script>
(8)pinia
参考第四章内容
(9)slot
- slot 翻译为“插槽”,分为默认插槽、具名插槽、作用域插槽
a. 默认插槽
修改 Parent.vue,创建数据
<template>
<div class="parent">
<h2>Parent</h2>
<div class="category">
<Child title="C1"></Child>
<Child title="C2"></Child>
<Child title="C3"></Child>
</div>
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
import { reactive } from 'vue' let c1List = reactive([
{ id: "01", Text: "C1-Text1" },
{ id: "02", Text: "C1-Text2" },
{ id: "03", Text: "C1-Text3" }
])
let c2List = reactive([
{ id: "01", Text: "C2-Text1" },
{ id: "02", Text: "C2-Text2" },
{ id: "03", Text: "C2-Text3" }
])
let c3List = reactive([
{ id: "01", Text: "C3-Text1" },
{ id: "02", Text: "C3-Text2" },
{ id: "03", Text: "C3-Text3" }
])
</script> <style scope>
.parent {
width: 80%;
padding: 20px;
background: #03deff;
border: 3px solid black;
}
.category {
display: flex;
justify-content: space-around;
}
.category div {
margin: 10px;
}
</style>
修改 Child.vue,接收数据并展示
<template>
<div class="child">
<h2>{{ title }}</h2>
</div>
</template> <script setup lang="ts" name="Child">
defineProps(['title'])
</script> <style scope>
.child {
width: 50%;
padding: 20px;
background: #fbff03;
border: 3px solid black;
}
</style>
修改 Parent.vue 中的模板内容
<template>
<div class="parent">
<h2>Parent</h2>
<div class="category">
<Child title="C1">
<ul>
<li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
</ul>
</Child>
<Child title="C2">
<ul>
<li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
</ul>
</Child>
<Child title="C3">
<!-- <ul>
<li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
</ul> -->
</Child>
</div>
</div>
</template>
修改 Child.vue,引入插槽并设置默认值
<template>
<div class="child">
<h2>{{ title }}</h2>
<slot>Default</slot>
</div>
</template>
此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值
b. 具名插槽
具有名字的插槽
修改 Child.vue,为插槽添加名字
<template>
<div class="child">
<h2>{{ title }}</h2>
<slot name="list">Default</slot>
</div>
</template>
修改 Paren.vue,为引用的组件标签添加
v-slot:
属性使用
#
简便写法可以替代v-slot:
写法<template>
<div class="parent">
<h2>Parent</h2>
<div class="category">
<Child title="C1" #list>
<ul>
<li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
</ul>
</Child>
<Child title="C2" v-slot:List>
<ul>
<li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
</ul>
</Child>
<Child title="C3">
<ul>
<li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
</ul>
</Child>
</div>
</div>
</template>
此时,C1 的内容正常展示;C2 和 C3 的内容均无法展示,而是显示默认值
修改 Child.vue,设置更多的具名插槽,使
title
通过插槽进行传递<template>
<div class="child">
<slot name="title"><h2>No Title</h2></slot>
<slot name="slot">Default</slot>
</div>
</template> <script setup lang="ts" name="Child">
</script>
修改 Parent.vue,使用
template
标签<template>
<div class="parent">
<h2>Parent</h2>
<div class="category">
<Child>
<template v-slot:title>
<h2>C1</h2>
</template>
<template v-slot:list>
<ul>
<li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
</ul>
</template>
</Child>
<Child>
<template v-slot:list>
<ul>
<li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
</ul>
</template>
<template v-slot:title>
<h2>C2</h2>
</template>
</Child>
<Child>
<template v-slot:list>
<ul>
<li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
</ul>
<h2>C3</h2>
</template>
</Child>
</div>
</div>
</template>
此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值
c. 作用域插槽
修改 Child.vue,原始数据在子组件中,使用 slot 将数据传递到父组件中
<template>
<div class="child">
<slot :cl="cList">Default</slot>
</div>
</template> <script setup lang="ts" name="Child">
import { reactive } from 'vue' let cList = reactive([
{ id: "01", text: "C-Text1" },
{ id: "02", text: "C-Text2" },
{ id: "03", text: "C-Text3" }
])
</script>
修改 Parent.vue,接收并使用子组件传递来的数据
<template>
<div class="parent">
<h2>Parent</h2>
<div class="category">
<Child>
<template v-slot="params"> <!-- 接收传递来的数据 -->
<ul>
<li v-for="item in params.cl" :key="item.id">{{ item.text }}</li>
</ul>
</template>
</Child>
<Child>
<template v-slot="{ cl }"> <!-- 解构传递来的数据 -->
<ol>
<li v-for="item in cl" :key="item.id">{{ item.text }}</li>
</ol>
</template>
</Child>
<Child>
<template v-slot:default="{ cl }"> <!-- 具名作用域插槽(default是插槽默认名) -->
<h4 v-for="item in cl" :key="item.id">{{ item.text }}</h4>
</template>
</Child>
</div>
</div>
</template> <script setup lang="ts" name="Parent">
import Child from './Child.vue'
</script>
(10)总结
组件关系 | 通信方式 |
---|---|
父传子 | props |
v-model |
|
$refs |
|
默认插槽 / 具名插槽 | |
子传父 | props |
自定义事件 | |
v-model |
|
$parent |
|
作用域插槽 | |
祖孙互传 | $attrs |
provide & inject |
|
任意组件 | mitt |
pinia |
0x06 其他 API
- 以下 API 使用场景不多,但需要了解
(1)shallowRef
& shallowReactive
- 两种方法一般用于绕开深度响应,提高性能,加快访问速度
a. shallowRef
作用:创建一个响应式数据,只对顶层属性进行响应式处理
特点:只跟踪引用值的变化,不跟踪值内部的属性的变化
用法
修改 App.vue,创建数据与方法,并在模板中展示
<template>
<div class="app">
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.age }}</h2>
<p><button @click="changerName">Change Name</button></p>
<p><button @click="changerAge">Change Age</button></p>
<p><button @click="changerAll">Change All</button></p>
</div>
</template> <script setup lang="ts" name="App">
let person = {
name: 'John',
age: 18
} function changerName() {
person.value.name = 'Mary'
}
function changerAge() {
person.value.age = 19
}
function changerAll() {
person.value = {
name: 'Mary',
age: 19
}
}
</script>
引入
shallowRef
并使用<script setup lang="ts" name="App">
import { shallowRef } from 'vue' let person = shallowRef({
name: 'John',
age: 18
})
// ...
</script>
此时,姓名和年龄均不可修改,而所有信息可以通过自定义的
changeAll()
方法修改
b. shallowReactive
作用:创建一个浅层响应式对象,只将对象的最顶层属性变成响应式,其他属性不变
特点:对象的顶层属性是响应式的,嵌套对象的属性不是
用法
修改 App.vue,创建数据与方法,并在模板中展示
<template>
<div class="app">
<h2>Name: {{ person.name }}</h2>
<h2>Age: {{ person.detail.age }}</h2>
<p><button @click="changerName">Change Name</button></p>
<p><button @click="changerAge">Change Age</button></p>
<p><button @click="changerAll">Change All</button></p>
</div>
</template> <script setup lang="ts" name="App">
let person = {
name: 'John',
detail: {
age: 18
}
} function changerName() {
person.name = 'Mary'
}
function changerAge() {
person.detail.age = 19
}
function changerAll() {
Object.assign(person, {
name: 'Mary',
detail: {
age: 19
}
})
}
</script>
引入
shallowReactive
并使用<script setup lang="ts" name="App">
import { shallowReactive } from 'vue' let person = shallowReactive({
name: 'John',
detail: {
age: 18
}
})
// ...
</script>
此时,姓名可以修改,但年龄不可修改,而所有信息可以通过自定义的
changeAll()
方法修改
(2)readonly
& shallowReadonly
a. readonly
作用:创建一个对象的深只读副本
特点:
- 对象的所有嵌套属性变为只读
- 阻止对象被修改
应用场景:
- 创建不可变的状态快照
- 保护全局状态/配置不可修改
用法
修改 App.vue,创建数据
<template>
<div class="app">
<h2>Sum: {{ sum1 }}</h2>
<h2>Sum readonly: {{ sum2 }}</h2>
<p><button @click="changeSum1">Change Sum</button></p>
<p><button @click="changeSum2">Change Sum readonly</button></p>
</div>
</template> <script setup lang="ts" name="App">
import { ref } from 'vue' let sum1 = ref(0)
let sum2 = ref(0) function changeSum1() {
sum1.value += 1
}
function changeSum2() {
sum2.value += 1
}
</script>
引入
readonly
并使用<script setup lang="ts" name="App">
import { ref, readonly } from 'vue'
// ...
let sum2 = readonly(sum1)
// ...
</script>
此时,
sum2
会随着sum1
改变而改变,但点击按钮“Change Sum readonly”不会对sum
修改
b. shallowReadonly
作用:创建一个对象的浅只读副本
特点:只将对象的顶层属性设置为只读,对象内部的嵌套属性依旧可读可写
应用场景:仅需对对象顶层属性保护时使用
用法
修改 App.vue,创建数据
<template>
<div class="app">
<h2>Name: {{ personRO.name }}</h2>
<h2>Age: {{ personRO.detail.age }}</h2>
<p><button @click="changerName">Change Name</button></p>
<p><button @click="changerAge">Change Age</button></p>
<p><button @click="changerAll">Change All</button></p>
</div>
</template> <script setup lang="ts" name="App">
import { reactive } from 'vue' let person = reactive({
name: 'John',
detail: {
age: 18
}
})
let personRO = person function changerName() {
personRO.name = 'Mary'
}
function changerAge() {
personRO.detail.age = 19
}
function changerAll() {
Object.assign(personRO, {
name: 'Mary',
detail: {
age: 19
}
})
}
</script>
引入
shallowReadonly
并使用<script setup lang="ts" name="App">
import { reactive, shallowReadonly } from 'vue'
// ...
let personRO = shallowReadonly(person)
// ...
</script>
此时,姓名不可以修改,年龄可以修改,所有信息可以通过自定义的
changeAll()
方法修改
(3)toRaw
& markRaw
a. toRaw
作用:获取一个响应式对象的原始对象
特点:返回的对象不是响应式的,不会触发视图更新
应用场景:将响应式对象传递到非 Vue 的库或外部系统时使用
用法
修改 App.vue,创建数据
<script setup lang="ts" name="App">
import { reactive } from 'vue' let person = reactive({
name: 'John',
age: 18
}) console.log(person)
</script>
引入
toRaw
并使用<script setup lang="ts" name="App">
import { reactive, toRaw } from 'vue' let person = reactive({
name: 'John',
age: 18
}) console.log(person)
console.log(toRaw(person))
</script>
b. markRaw
作用:标记一个对象,使其永远不会变成响应式对象
应用场景:例如使用 Mock.js 插件时,为防止误把
mockjs
变成响应式对象而使用用法
修改 App.vue,创建数据
<script setup lang="ts" name="App">
let person = {
name: 'John',
age: 18
}
</script>
引入
markRaw
并使用<script setup lang="ts" name="App">
import { reactive, markRaw } from 'vue' let person = markRaw({
name: 'John',
age: 18
}) person = reactive(person)
console.log(person)
</script>
此时,
person
并未变成响应式对象
(4)customRef
作用:创建一个自定义的
ref
,并对其依赖项跟踪和更新触发进行逻辑控制用法
修改 App.vue,引入
customRef
<template>
<div class="app">
<h2>Message: {{ msg }}</h2>
<input type="text" v-model="msg" />
</div>
</template> <script setup lang="ts" name="App">
import { customRef } from 'vue' let msg = customRef(() => {
return {
get() {},
set() {}
}
})
</script>
其中,
get()
在变量msg
被读取时调用;set()
在变量msg
被修改时调用声明新变量
initMsg
作为默认值在get()
中返回<script setup lang="ts" name="App">
import { customRef } from 'vue' let initMsg = "Default"
let msg = customRef(() => {
return {
get() {
return initMsg
},
set() {}
}
})
</script>
对
msg
的修改结果通过set()
接收<script setup lang="ts" name="App">
// ...
let msg = customRef(() => {
return {
// ...
set(value) {
console.log('set', value)
}
}
})
</script>
在
set()
中修改initMsg
<script setup lang="ts" name="App">
// ...
let msg = customRef(() => {
return {
// ...
set(value) {
// console.log('set', value)
initMsg = value
}
}
})
</script>
【核心内容】在
customRef
的回调函数中,接收两个参数:track
(跟踪)、trigger
(触发)<script setup lang="ts" name="App">
// ...
let msg = customRef((track, trigger) => {
return {
get() {
track()
return initMsg
},
set(value) {
initMsg = value
trigger()
}
}
})
</script>
track()
:依赖项跟踪,告知 Vue 需要对变量msg
持续关注,一旦变化立即更新trigger()
:更新触发,告知 Vue 变量msg
发生了变化
可以在
set()
通过设置延时函数实现指定时间后触发更新<script setup lang="ts" name="App">
// ...
let msg = customRef((track, trigger) => {
return {
// ...
set(value) {
setTimeout(() => {
initMsg = value
trigger()
}, 1000)
}
}
})
</script>
但是当输入过快时,会导致输入的数据被覆盖丢失,此时引入防抖方法
<script setup lang="ts" name="App">
// ...
let timer: number
let msg = customRef((track, trigger) => {
return {
// ...
set(value) {
clearTimeout(timer)
timer = setTimeout(() => {
initMsg = value
trigger()
}, 1000)
}
}
})
</script>
防抖原理:清除上一次
set()
中生产的定时器,并以最新定时器为准在 src 目录下新建 hooks 目录,其中新建 useMsgRef.ts,用于将上述自定义 ref 封装成 Hooks
import { customRef } from 'vue' export default function(initMsg: string, delaySecond: number) {
let timer: number
let msg = customRef((track, trigger) => {
return {
get() {
track()
return initMsg
},
set(value) {
clearTimeout(timer)
timer = setTimeout(() => {
initMsg = value
trigger()
}, delaySecond * 1000)
}
}
})
return { msg }
}
修改 App.vue,使用上述 Hooks
<script setup lang="ts" name="App">
import useMsgRef from '@/hooks/useMsgRef' let { msg } = useMsgRef('Default', 1)
</script>
0x07 新组件
(1)Teleport
是一种能够将组件 HTML 结构移动到指定位置的技术
举例:在页面上封装一个弹窗组件
目录结构:
graph TB
src-->components & App.vue & main.ts
components-->ModelDialog.vue修改 App.vue
<template>
<div class="outer">
<h2>App Component</h2>
<ModelDialog />
</div>
</template> <script setup lang="ts" name="App">
import ModelDialog from './components/ModelDialog.vue'
</script> <style scope>
.outer {
width: 80%;
height: 500px;
padding: 20px;
background: #03deff;
}
</style>
修改 ModelDialog.vue
<template>
<button>Show Dialog</button>
<div class="md">
<h2>Dialog Title</h2>
<p>Content Here</p>
<button>Close</button>
</div>
</template> <script setup lang="ts" name="ModelDialog">
</script> <style scope>
.md {
text-align: center;
width: 50%;
height: 30%;
background: #fbff03;
border: 3px solid black;
}
</style>
设置弹窗的显示与隐藏
<template>
<button @click="isShowDialog = true">Show Dialog</button>
<div class="md" v-show="isShowDialog">
<h2>Dialog Title</h2>
<p>Content Here</p>
<button @click="isShowDialog = false">Close</button>
</div>
</template> <script setup lang="ts" name="ModelDialog">
import { ref } from 'vue'
let isShowDialog = ref(false)
</script>
通过 CSS 调整弹窗位于视口正中央
<style scope>
.md {
// ...
position: fixed;
top: 5%;
left: 50%;
margin-left: -100px;
}
</style>
修改 App.vue,通过 CSS 设置变灰
<style scope>
.outer {
// ...
filter: saturate(0%);
}
</style>
此时,弹窗并未按照第 4 步的设置显示在视口正中央,可以通过 Teleport 解决此问题
修改 ModelDialog.vue
<template>
<button @click="isShowDialog = true">Show Dialog</button>
<Teleport to="body">
<div class="md" v-show="isShowDialog">
<h2>Dialog Title</h2>
<p>Content Here</p>
<button @click="isShowDialog = false">Close</button>
</div>
</Teleport>
</template>
to
属性用于指定移动的目的地,上述代码将弹窗内容移动到body
标签中此时,网页渲染后的 DOM 结构如下:
<!-- ... -->
<body>
<div id="app" data-v-app><!-- ... --></div>
<!-- ... -->
<div id="md"><!-- ... --></div>
</body>
(2)Suspense
- 等待异步组件时,渲染一些额外的内容,改善用户体验
目录结构:
graph TB
src-->components & App.vue & main.ts
components-->Child.vue
修改 App.vue
<template>
<div class="app">
<h2>App</h2>
<Child />
</div>
</template> <script setup lang="ts" name="App">
import Child from './components/Child.vue'
</script> <style scope>
.app {
width: 80%;
height: 500px;
padding: 20px;
background: #03deff;
}
</style>
修改 Child.vue
<template>
<div class="child"></div>
</template> <script setup lang="ts" name="Child">
</script> <style scope>
.child {
width: 200px;
height: 150px;
background: #fbff03;
}
</style>
添加异步任务
<script setup lang="ts" name="Child">
import axios from 'axios' let { data: { result: { content } } } = await axios.get("https://api.oioweb.cn/api/common/OneDayEnglish") console.log(content)
</script>
此时,子组件会在页面上“消失”,可以在 App.vue 中使用 Suspense 解决此问题
修改 App.vue,引入 Suspense 并使用
<template>
<div class="app">
<h2>App</h2>
<Suspense>
<template #default>
<Child />
</template>
</Suspense>
</div>
</template> <script setup lang="ts" name="App">
import Child from './components/Child.vue'
import { Suspense } from 'vue'
</script>
- Suspense 中预设了两个插槽,
default
插槽用于展示异步完成的内容,fallback
插槽用于展示异步进行中的内容 - 当网络状态不是很好时,子组件不会立即渲染完成,因此可以借助 Suspense 添加加载提示
- Suspense 中预设了两个插槽,
设置加载内容:Loading...
<template>
<div class="app">
<h2>App</h2>
<Suspense>
<template #default>
<Child />
</template>
<template #fallback>
<h2>Loading...</h2>
</template>
</Suspense>
</div>
</template>
(3)全局 API 转移到应用对象
graph LR
A(Vue2 全局API<br/>Vue.xxx) --> B(Vue3 应用对象API<br/>app.xxx)
app.component
修改 main.ts,注册全局组件
import { createApp } from 'vue'
import App from './App.vue'
import Child from './components/Child.vue' const app = createApp(App)
app.component('Child', Child)
app.mount('#app')
修改 App.vue,使用全局组件
<template>
<div class="app">
<h2>App</h2>
<Child />
</div>
</template> <script setup lang="ts" name="App">
</script>
app.config
修改 main.ts,注册全局属性
import { createApp } from 'vue'
import App from './App.vue' const app = createApp(App)
app.config.globalProperties.x = 12345 declare module 'vue' {
interface ComponentCustomProperties {
x: number;
}
} app.mount('#app')
修改 App.vue,使用全局属性
<template>
<div class="app">
<h2>App</h2>
<h4>{{ x }}</h4>
</div>
</template> <script setup lang="ts" name="App">
</script>
实际开发中,不建议该使用方法,容易污染全局
app.directive
修改 main.ts,注册全局指令
import { createApp } from 'vue'
import App from './App.vue' const app = createApp(App)
app.directive('custom', (element, {value}) => {
element.innerText += value
element.style.fontSize = '50px';
})
app.mount('#app')
修改 App.vue,使用全局指令
<template>
<div class="app">
<h2>App</h2>
<p v-custom="value">Hello,</p>
</div>
</template> <script setup lang="ts" name="App">
let value = "world!"
</script>
app.mount
在 main.ts 中挂载应用
import { createApp } from 'vue'
import App from './App.vue' const app = createApp(App)
app.mount('#app')
app.unmount
在 main.ts 中卸载应用
import { createApp } from 'vue'
import App from './App.vue' const app = createApp(App)
app.mount('#app') setTimeout(() => {
app.unmount()
}, 3000)
app.use
在 main.ts 中安装插件,如 pinia
import { createApp } from 'vue'
import App from './App.vue'
import { PiniaVuePlugin } from 'pinia' const app = createApp(App)
app.use(PiniaVuePlugin)
app.mount('#app')
(4)非兼容性改变
详见官方文档
-End-
Vue3 + TypeScript 开发指南的更多相关文章
- Vue3 + TypeScript 开发实践总结
前言 迟来的Vue3文章,其实早在今年3月份时就把Vue3过了一遍.在去年年末又把 TypeScript 重新学了一遍,为了上 Vue3 的车,更好的开车.在上家公司4月份时,上级领导分配了一个内部的 ...
- 《Vue3.x+TypeScript实践指南》已出版
转眼回长沙快2年了,图书本在去年就已经完稿,因为疫情,一直耽搁了,直到这个月才出版!疫情之下,众生皆苦!感觉每天都是吃饭.睡觉.上班.做核酸! 图书介绍 为了紧跟技术潮流,该书聚焦于当下火的Vue3和 ...
- TypeScript学习指南--目录索引
关于TypeScript: TypeScript是一种由微软开发的自由和开源的编程语言.它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程. TypeS ...
- TypeScript入门指南(JavaScript的超集)
TypeScript入门指南(JavaScript的超集) 你是否听过 TypeScript? TypeScript 是 JavaScript 的超集,TypeScript结合了类型检查和静态分析 ...
- [转帖]2019 简易Web开发指南
2019 简易Web开发指南 2019年即将到来,各位同学2018年辛苦了. 不管大家2018年过的怎么样,2019年还是要继续加油的! 在此我整理了个人认为在2019仍是或者将成为主流的技术 ...
- 现代前端库开发指南系列(二):使用 webpack 构建一个库
前言 在前文中,我说过本系列文章的受众是在现代前端体系下能够熟练编写业务代码的同学,因此本文在介绍 webpack 配置时,仅提及构建一个库所特有的配置,其余配置请参考 webpack 官方文档. 输 ...
- 2019 Vue开发指南:你都需要学点啥?
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://dzone.com/articles/vue-development-in-2019 ...
- Vite ❤ Electron——基于Vite搭建Electron+Vue3的开发环境【一】
背景 目前社区两大Vue+Electron的脚手架:electron-vue和vue-cli-plugin-electron-builder, 都有这样那样的问题,且都还不支持Vue3,然而Vue3已 ...
- 基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用
刚完成一些前端项目的开发,腾出精力来总结一些前端开发的技术点,以及继续完善基于SqlSugar的开发框架循序渐进介绍的系列文章,本篇随笔主要介绍一下基于Vue3+TypeScript的全局对象的注入和 ...
- 基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
在工作流页面中,除了特定的业务表单信息外,往往也需要同时展示通用申请单的相关信息,因此在页面设计的时候需要使用一些组件化的概念来实现动态的内容展示处理,本篇随笔介绍Vue3+TypeScript+El ...
随机推荐
- redis 安装的参考文章
redis 安装 : https://www.runoob.com/redis/redis-install.html
- [Linux] Linux 自动挂载mount --bind 实现类似目录硬链的效果 (包含ZFS方案)
说明 这个命令用以将一个目录挂载到另一个目录,以实现类似于硬链的操作 但是这个命令只是在内存中建立了一个映射,重启系统之后挂载就消失了 而linux是不支持目录硬链的,具体原因见linux为什么不能硬 ...
- golang官方包管理vendor模式无法引用非go文件
主页 微信公众号:密码应用技术实战 博客园首页:https://www.cnblogs.com/informatics/ 背景&问题 golang作为高级计算机语言之一,在云原生以及web网站 ...
- 远程服务调用(RPC与Rest本质区别)
一.背景 远程服务将计算机程序的工作范围从单机扩展到网络,从本地延伸至远程,是构建分布式系统的首要基础.远程服务调用(Remote Procedure Call,RPC)在计算机科学中已经存在了超过四 ...
- Linux 系统进程管理
Linux 系统进程管理 目录 Linux 系统进程管理 一.进程的概述 1.1 什么是进程? 1.2 进程和程序的区别 1.3 进程的生命周期 1.4 进程的运行过程 二. 静态显示进程状态-ps ...
- ansible 自动化运维(1)
ansible 简介 ansible 是什么? ansible是新出现的自动化运维工具,基于Python开发,集合了众多运维工具(puppet.chef.func.fabric)的优点,实现了批量 ...
- 2.4G无线音频一对多传输解决方案难点解析
前记 2.4G无线音频传输是一个非主流的应用,做这个的人 相对要比较少.但是,这个领域所涉及到的知识却不少,也就导致了这个领域是好入门,但是东西想做好特别难.这里涉及到声学,无线协议,电子,设 ...
- Typora自定义主题详解--打造自己的专属样式
你真的会使用Typora吗? 欢迎关注博主公众号「Java大师」, 专注于分享Java领域干货文章, 关注回复「主题」, 获取大师使用的typora主题: http://www.javaman.cn/ ...
- iot梳理
近段时间一直在搞公司的iot项目,没啥时间学习新的知识(也是自己懒),这边记录下整体对iot知识领域的认识. 首先说到iot会想到物联网,对于我们开发来说物联网很明显要用到几个不太常用到的技术,如mq ...
- String类为什么要用final修饰?
final修饰符的意义? https://www.cnblogs.com/loren-Yang/p/13380318.html String类被实现的目标是什么? 效率和安全 如何实现期望? 参考文献 ...