- 创建项目
npm create vue@latest
-
目录结构
├── public/ # 静态资源目录,其中的文件会被复制到 dist 目录下
│ └── favicon.ico # 示例:网站图标
│
├── src/ # 主要开发目录
│ ├── assets/ # 静态资源,如图片、字体等
│ ├── components/ # Vue 组件目录(如果你使用 Vue.js 的话)
│ ├── App.vue # Vue 应用的根组件(如果是 Vue 项目)
│ ├── main.js # 应用程序的入口脚本
│ └── ...
│
├── .gitignore # Git 忽略文件
├── index.html # Vite 服务启动时的基本 HTML 模板
├── package.json # npm 包配置文件,包含项目依赖和脚本任务
├── vite.config.js # Vite 配置文件,用于自定义 Vite 的行为
└── README.md # 项目说明文件(通常是 README 或其他文档)
OptionsAPI 和 compositionAPI
-
OptionsAPI(Vue2 是选项式 API)
- OptionsAPI 的数据、方法、计算属性是分散在 data、methods、computed 等选项中的,若想新增或者修改一个需求,就需要分别修改 data、methods、computed,不便于维护和复用。
-
compositionAPI(Vue3 是组合式 API)
- 可以用函数的方式组织代码,让相关功能的代码更加有序的组织在一起,让代码更加清晰易懂。
setup
- setup 函数是 Vue3 中一个新的配置项,值是一个函数。
注意
- setup 函数中 this 指向 undefined。
- 如果定义的数据不修改,可以直接定义成普通数据。如果定义的数据需要修改,那么必须定义成响应式,否则数据虽然修改了,但是页面不会改变。
- vue3 模板中可以使用 data 中的数据和 methods 中的方法,也可以使用 setup 中的数据和方法(不推荐)。
- data 中可以通过 this.name 读取到 setup 中的数据.反之 setup 中不能读取 data 中的数据(不推荐)。
<script lang="ts">
export default {
name: 'Person',
setup() {
// 定义普通数据
let name = "张三";
let tel = "13888888888";
// 定义方法
function changeName() {
name.value = "李四";
}
function showTel() {
alert(tel);
}
// 返回数据
return {
name,
changeName,
showTel,
};
}
}
</script>
注意
- 语法糖写法:直接在标签中写入
setup
,并且标签内的数据会自动return
。 - 需要要注意的是如果给组件重新命名需要单独写个
script
标签,并指定name
。 - 也可以通过安装插件来解决(详细配置请百度)
npm i vite-plugin-vue-setup-extend -D
。 - 工作中组件名称和文件名称一般保持一致,所以可以省略
name
属性。
<script setup lang="ts" name="Person">
...
</script>
ref 和 reactive
- ref 用来定义【基本类型】的响应式数据
- reactive 用来定义【对象类型】的响应式数据
<script setup lang="ts" name="Person">
import { ref , reactive } from "vue";
// 用ref定义【基本类型】响应式数据
let name = ref("孙悟空");
// 用reactive定义【对象类型】响应式数据(只能定义对象类型)
let user = reactive({
name: "猪八戒",
age: 18,
})
// 用ref定义【对象类型】响应式数据
let car = ref({
name:'宝马',
price:'40W'
})
// 定义普通数据
let tel = "13888888888";
// 定义方法
function changeName() {
// 当用ref包裹数据后,name就成了一个ref对象,所以需要使用name.value来读取值
name.value = "李四";
}
function showTel() {
alert(tel);
}
function changeUserName() {
user.name = "沙和尚";
}
function changeCarName() {
car.value.name = "奔驰";
}
</script>
注意
-
ref
内部是基于reactive
实现的。 -
ref
定义的基本类型数据,在模版中必须使用.value
的方式读取和修改数据。reactive
不需要。 -
reactive
重新分配一个新对象会失去响应式,可以使用 Object.assign()来解决。
let obj = reactive({ name: "张三", age: 19 });
function changeObj() {
obj = { name: "李四", age: 20 }; // 无法修改(页面不更新)
obj = Object.assign(obj, { name: "李四", age: 20 }); // 可以修改
}
使用原则
- 若需要一个基本类型的响应式数据,必须使用 ref。
- 若需要一个响应式对象,层级不深,ref、reactive 都可以。
- 若需要一个响应式对象,且层级较深,推荐使用 reactive。
toRefs 和 toRef
-
toRefs
可以将reactive
定义的对象,转换为普通对象,该普通对象的每个属性都是ref
类型。
import { reactive, toRefs } from "vue";
let user = reactive({
name: "张三",
age: 10,
});
// toRefs的作用就是把一个reactive所定义的对象,变成一个由n个ref所定义的对象
let { name, age } = toRefs(user);
-
toRef
可以将reactive
定义的对象中的某个属性,转换为ref
类型。
import { reactive, toRef } from "vue";
let user = reactive({
name: "张三",
age: 10,
});
let name = toRef(user, "name");
computed
- 计算属性。
<script lang="ts" setup name="Person">
import { ref, computed } from "vue";
let fname = ref("张");
let lname = ref("三");
// 这样定义的fullName是一个计算属性,但是是一个只读的计算属性
let fullName = computed(() => {
return fname.value + lname.value;
})
// 这样定义的fullName是一个可读可写的计算属性
let fullName = computed({
get() {
return fname.value + lname.value;
},
set(value) {
let arr = value.split(" ");
fname.value = arr[0];
lname.value = arr[1];
}
})
// 计算属性返回的也是一个ref类型的数据
// 所以需要经过 .value 才能获取到值
function changeFullName(){
fullName.value = "李-四";
}
</script>
watch
监听数据的变化。
-
Vue3 中的 watch 只能监视以下四种数据
- ref 所定义的数据。
- reactive 所定义的数据。
- 函数返回一个值(
getter
函数)。 - 一个包含上述内容的数组。
场景 1:监听 ref 定义的基本类型的数据。
<script lang="ts" setup name="Person">
import { ref, watch } from "vue";
let sum = ref(0);
// 参数:监听对象 回调函数(newValue, oldValue)
// watch的返回值是一个函数
const stopWatch = watch(sum, (newValue, oldValue) => {
console.log(newValue, oldValue);
if (newValue >= 10) {
// 停止监听
stopWatch();
}
});
</script>
- 场景 2:监听 ref 定义的对象类型的数据。
<script lang="ts" setup name="Person">
import { ref, watch } from "vue";
let user = ref({
name: "张三",
age: 18,
});
// 参数:监听对象 回调函数(newValue, oldValue) 配置对象
// 监听对象时,所监听的是对象的地址,如果需要监听对象内部某个值的改变需要开启第三个参数
// deep 深度监视
// immediate 立即执行(一进来先执行一下)
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true, immediate: true }
);
</script>
- 场景 3:监听 reactive 定义的对象类型的数据。
<script lang="ts" setup name="Person">
import { reactive, watch } from "vue";
let user = reactive({
name: "张三",
age: 18,
});
// 参数:监听对象 回调函数(newValue, oldValue)
// 默认开启深度监视,且无法关闭
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true, immediate: true }
);
</script>
- 场景 4:监听 ref 或 reactive 定义的对象类型的数据的某个属性。
<script lang="ts" setup name="Person">
import { ref, reactive, watch } from "vue";
let user = reactive({
name: "张三",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
// 监听reactive定义的对象中的某个属性,watch的第一个参数需要换成一个函数,这个函数返回一个值,这个值就是要监听的属性,如果监听的这个值是一个对象形式,可以手动开启深度监听deep:true
watch(
() => user.name,
(newVal, oldVal) => {
console.log("newVal", newVal);
console.log("oldVal", oldVal);
}
);
watch(
() => user.car,
(newVal, oldVal) => {
console.log("newVal", newVal);
console.log("oldVal", oldVal);
},
{ deep: true }
);
// 监听多个属性
watch(
[() => user.name, () => user.car.c1]
(newVal, oldVal) => {
console.log("newVal", newVal);
console.log("oldVal", oldVal);
},
{ deep: true }
);
</script>
watchEffect
- 当有数据变化时,会自动监听变化,并执行回调函数
watch
和watchEffect
两者区别:-
watch
需要手动指定监听的数据。 -
watchEffect
则不需要,它会自动收集依赖。
-
import { ref, watchEffect } from "vue";
let width = ref(0);
let height = ref(0);
watchEffect(() => {
if (width.value > 100 || height.value > 100) {
console.log("干点啥...");
}
});
标签的 ref 属性
- 给标签添加 ref 属性,可以获取到标签的 DOM 元素
- 给组件标签添加 ref 属性,可以获取到组件实例对象
注意- 在父组件中获取子组件的实例对象,需要在子组件中通过
defineExpose({})
暴露出来。
- 在父组件中获取子组件的实例对象,需要在子组件中通过
<h1>标签的ref属性</h1>
import { ref, defineExpose } from "vue";
let title = ref();
function getDom() {
console.log(title.value);
}
v-if
和v-show
的区别
-
v-if
是动态地创建或移除 DOM 元素。 -
v-show
是通过设置 CSS 属性 display 来显示或隐藏元素。
生命周期
- 创建
- setup 函数
setup
函数代替了 vue2 中的beforeCreate
和created
- 挂载
- onBeforeMount
import { onBeforeMount } from "vue";
onBeforeMount(() => {
console.log("挂载前");
});
- onMounted
import { onMounted } from "vue";
onMounted(() => {
console.log("挂载完毕");
});
- 更新
- onBeforeUpdate
import { onBeforeUpdate } from "vue";
onBeforeUpdate(() => {
console.log("更新前");
});
- onUpdated
import { onUpdated } from "vue";
onUpdated(() => {
console.log("更新完毕");
});
- 卸载
- onBeforeUnmount
import { onBeforeUnmount } from "vue";
onBeforeUnmount(() => {
console.log("卸载前");
});
- onUnmounted
import { onUnmounted } from "vue";
onUnmounted(() => {
console.log("卸载完毕");
});
自定义 hooks
- 什么是自定义 hooks?
- hook 本质就是一个函数,一般放在 src 下的 hooks 文件夹下,在文件内部可以使用钩子函数等。
-
为什么使用自定义 hooks?
- 可以将重复的逻辑或者同一个模块的功能进行封装,使代码更加模块化,便于维护。
如何创建自定义 hooks?
import { ref, reactive } from "vue";
export default function useUser() {
let userId = ref("001");
async function getUserInfo() {
let res = await fetch("http://localhost:3000/data");
}
return { userId, getUserInfo };
}
- 如何使用?
<h1>{{ userId }}</h1>
<button @click="getUserInfo">获取用户信息</button>
import useUser from "./hooks/useUser";
const { userId, getUserInfo } = useUser();
注意
- hooks 的命名一般以 use 开头,后面是功能的描述。
路由
- vue3 中的路由使用
- 安装
npm i vue-router
- 在
main.ts
中引入
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
- 创建路由文件
import { createRouter, createWebHistory } from "vue-router";
import Home from "../pages/Home.vue";
import News from "../pages/News.vue";
import About from "../pages/About.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/home",
component: Home,
},
{
path: "/news",
component: News,
},
{
path: "/about",
component: About,
},
],
});
export default router;
- 在页面中使用
<RouterLink to="/home">首页</RouterLink>
<RouterView></RouterView>
注意
在 vue3 中,原本 vue2 中的<router-link/>
和<router-view>
变成了<RouterLink>
和<RouterView>
命名路由
- 就是在路由配置时,给路由添加个 name 的属性
注意:其他使用方法与 vue2 一致
// 创建并暴露路由器
import { createRouter, createWebHistory } from "vue-router";
import Home from "../pages/Home.vue";
import News from "../pages/News.vue";
import About from "../pages/About.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
name:'home',
path: "/home",
component: Home,
},
...
],
});
export default router;
嵌套路由
- 就是在一个路由下再嵌套一个路由
import { createRouter, createWebHistory } from "vue-router";
import Home from "../pages/Home.vue";
import News from "../pages/News.vue";
import Detail from "../pages/Detail.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
name: "home",
path: "/home",
component: Home,
},
{
name: "news",
path: "/news",
component: News,
children: [
{
name: "detail",
path: "detail",
component: Detail,
},
],
},
],
});
export default router;
路由传参
- query 传参
优点:不需要修改路由文件
缺点:html 代码稍微复杂
<RouterLink
:to="{
// 如果是name,可以利用命名路由时定义的名字来简写
name: 'detail',
// 如果写path,需要将路由写完整
path: '/news/detail',
query: { title:news.title,content:news.content},
}"
>
新闻详情
</RouterLink>
<script lang="ts" setup name="Detail">
import {useRoute} from "vue-router"; const route = useRoute();
console.log(route.query.title) console.log(route.query.content)
</script>
- params 传参
优点:html 代码风格简洁
缺点:需要更改路由文件
注意 1:路由跳转必须使用命名路由时起的 name
注意 2:在路由匹配规则后面增加问号,表示该参数可传可不传
// 创建并暴露路由器
import { createRouter, createWebHistory } from "vue-router";
import News from "../pages/News.vue";
import Detail from "../pages/Detail.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
name: "news",
path: "/news",
component: News,
children: [
{
name: "detail",
path: "detail/:title/:content?",
component: Detail,
},
],
},
],
});
export default router;
<RouterLink :to="`/news/detail/${news.title}/${news.content}`">
新闻详情
</RouterLink>
<RouterLink
:to="{
name: 'detail',
params: { title:news.title,content:news.content},
}"
>
新闻详情
</RouterLink>
<script lang="ts" setup name="Detail">
import {useRoute} from "vue-router"; const route = useRoute();
console.log(route.params.title) console.log(route.params.content)
</script>
- 路由的 props 配置
注意:需要在路由文件中配置 props 属性,不同传参方式配置也不相同,但是接收参数方式都是用 defineProps
<template>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</template>
<script lang="ts" setup name="Detail">
// 利用props传参 + defineProps收参,实现优雅写法
defineProps(["title", "content"]);
</script>
import { createRouter, createWebHistory } from "vue-router";
import News from "../pages/News.vue";
import Detail from "../pages/Detail.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
name: "news",
path: "/news",
component: News,
children: [
{
name: "detail",
path: "detail",
component: Detail,
// 写法1:这种自定义写法适合query传参
props(route) {
return route.query;
},
// 写法2:这种写法适合params传参
props: true,
},
],
},
],
});
export default router;
replace 模式
- 只需要在跳转的时候加上
replace
<RouterLink replace to="/about">关于我们</RouterLink>
编程式路由
- 编程式路由就是通过 js 代码实现路由跳转
import { onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
onMounted(() => {
setTimeout(() => {
router.push({
name: "about",
});
}, 5000);
});
重定向
- 重定向就是当用户访问某个路径时,自动跳转到另一个路径
import { createRouter, createWebHistory } from "vue-router";
import Home from "../pages/Home.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/",
redirect: "/home",
},
{
name: "home",
path: "/home",
component: Home,
},
],
});
export default router;
Pinia
- 安装
npm i pinia
- 在 main.ts 中引入
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.mount("#app");
- 在 src 下建立 store 文件夹并在其下创建与页面对应的文件
例如:我有一个 User.vue 的组件,就创建一个 user.ts 的文件。需要注意的是不需要每个.vue 文件都在 store 文件夹下创建文件,只是哪些组件需要状态管理,就新建对应的 ts 文件。
3.1 user.ts 文件
import { defineStore } from "pinia";
import axios from "axios";
import { pinyin } from "pinyin-pro";
export const useUserStore = defineStore("user", {
// 存储数据的地方
state() {
return {
name: "刘洪涛",
age: 9,
tel: "1388888888",
data: [
{
id: "d001",
content: "一句话",
},
],
};
},
// actions里存储动作函数(修改数据的方法)
// 注意:actions里的this指的是state
actions: {
updateTel(tel: string) {
console.log("updateTelupdateTel", tel);
this.tel = tel;
},
async getSentence() {
const res = await axios.get(
"https://api.uomg.com/api/rand.qinghua?format=json"
);
console.log(res.data.content);
this.data.unshift({
id: new Date().getTime() + "",
content: res.data.content,
});
},
},
getters: {
pinyinName(state) {
return pinyin(state.name);
},
},
});
3.2 User.vue 文件
<template>
<div class="count">
<h2>用户信息</h2>
<p>姓名:{{ name }} / {{ pinyinName }}</p>
<p>年龄:{{ age }}</p>
<p>手机号:{{ tel }}</p>
<p>
<button @click="increase">加一岁</button>
<button @click="changeName">改名</button>
<button @click="changeUserInfo">修改全部信息</button>
<button @click="changeTel">修改手机号</button>
</p>
<div class="centenes">
<ul>
<li v-for="d in data" :key="d.id">{{ d.content }}</li>
</ul>
<button @click="getSentence">获取一句话</button>
</div>
</div>
</template>
<script lang="ts" setup name="CountPage">
import { storeToRefs } from "pinia";
import { useUserStore } from "@/store/user";
const userStore = useUserStore();
const { name, age, tel, data, pinyinName } = storeToRefs(userStore);
// 修改数据的第一种方式
function changeName() {
name.value = "红桃六";
}
function increase() {
age.value++;
}
// 修改数据的第二种方式(批量修改)
function changeUserInfo() {
userStore.$patch({
name: "孙悟空",
age: 500,
tel: "12345678910",
});
}
// 修改数据的第三种方式(利用actions)
function changeTel() {
userStore.updateTel("010-67689999");
}
// 通过actions里的方法请求接口,获取数据
function getSentence() {
userStore.getSentence();
}
// $subscribe 监听pinia里的数据修改
userStore.$subscribe((mutate, state) => {
console.log(mutate, state);
});
</script>
组件通信
props
-
父组件向子组件传值
- 父组件通过标签的属性将需要传递的数据传递给子数据。
-
子组件向父组件传值
- 父组件先向子组件传递一个方法
- 子组件通过调用该方法将参数传递给父组件
- 父组件就可以在这个方法的参数中获取到子组件传递过来的数据
<template>
<Person :personList="personList" :sendToy="sendToy" />
</template>
// 利用父组件的传递方法来接收子组件传递的数据
sendToy(){
console.log(data);
}
- 子组件通过
defineProps
来接收。
<template>
<button @click="sendToy('奥特曼')">把玩具传给父组件</button>
</template>
import { defineProps, withDefaults } from "vue";
// 方式一:只接收
defineProps(["personList", "sendToy"]);
// 方式二:只接收 + 限制类型
defineProps<{ personList: Person[] }>();
// 方式三:只接收 + 限制类型 + 设置默认值 + 设置可选
withDefaults(defineProps<{ personList?: Person[] }>(), {
personList: () => [[{ id: "9", name: "孙悟空", age: 500 }]],
});
// 方式四:defineProps有返回值,值为所有传递过来的数据
const props = defineProps(["personList"]);
自定义事件
注意 1:在 vue 中给方法传递事件对象 要用 $event
注意 2:当自定义事件由多个单词组成 推荐使用 kebab-case 肉串命名
- 子组件向父组件传递数据
- 在父组件中定义一个自定义事件
<template>
<Child @send-toy="getChildSendData" />
</template>
function getChildSendData(data) {
console.log("拿到子组件传递过来的数据", data);
}
- 在子组件中拿到自定义事件并调用
<button @click="emit('send-toy','奥特曼')">把玩具传给父组件</button>
import {(onMounted, defineEmits)} from "vue";
// 声明一个事件
const emit = defineEmits(["send"]);
mitt
- 安装
npm install mitt
- 在 src 下新建一个 utils 文件夹,在 utils 文件夹下新建一个 emitter.ts 文件
import mitt from "mitt";
// 调用mitt得到emitter,emitter能绑定和触发事件
const emitter = mitt();
export default emitter;
- 在 main.ts 中引入
import emitter from "@/utils/emitter";
- 在传递数据的组件中
<template>
<h1>about内容</h1>
<button @click="sendToy">向其他组件传递数据</button>
</template>
<script setup lang="ts" name="About">
import emitter from "@/utils/emitter";
function sendToy() {
// 给emitter绑定自定义事件
emitter.emit("send-toy", "小黄鸭");
}
</script>
- 在接收数据的组件中
<template>
<div class="title">我有一个玩具是 {{ toy }}</div>
</template>
<script setup lang="ts" name="Header">
import emitter from "@/utils/emitter";
import { ref, onUnmounted } from "vue";
let toy = ref("奥特曼");
emitter.on("send-toy", (value: any) => {
toy.value = value;
});
// 建议:组件卸载时解绑自定义事件
onUnmounted(() => {
emitter.off("send-toy");
});
</script>
$attrs
- 适用于父组件向孙子组件传递数据
- 只需要在儿子组件中使用 v-bind="$attrs" 接收父组件传递过来的数据并传递下去
<Grandson v-bind="$attrs" />
parent
- $refs 适用于父传子
- 父组件
<template>
<button @click="getAllSon($refs)">获取素所有子组件实例</button>
<Son1 ref="son1" />
<Son2 ref="son2" />
</template>
<script lang="ts" name="Father" setup>
import Son1 from "./Son1.vue";
import Son2 from "./Son2.vue";
function getAllSon(refs) {
// refs 就是拿到的所有子组件的实力对象,可以用来修改子组件的数据
// 注意在使用refs获取子组件实例对象的时候,必须要给子组件增加ref属性
console.log(refs);
}
</script>
- 子组件
<template>
<div class="son1">...</div>
</template>
<script lang="ts" name="Son1" setup>
import { ref } from "vue";
let toy = ref("嘎嘎");
let books = ref(3);
// 一定要将需要修改的数据通过 defineExpose 暴露出去
defineExpose({ toy, books });
</script>
- $parent
- 子组件
<template>
<button @click="getFather($parent)">获取父组件的数据</button>
</template>
<script lang="ts" name="Son1" setup>
function getFather(parent) {
// 可以拿到父组件的数据和方法
console.log(parent);
}
</script>
- 父组件
<script lang="ts" name="Father" setup>
// 父组件也需要将需要修改的数据通过 defineExpose 暴露出去
defineExpose({ house });
</script>
provide 和 inject
- 父组件
<script lang="ts" name="Father" setup>
import { ref, reactive, provide } from "vue";
let car = reactive({ name: "奔驰", price: "40W" });
let name = ref("张老三");
// 调用 provide 函数,向后代提供数据
provide("name", name.value);
provide("car", car);
</script>
- 后代组件
<script lang="ts" name="Son1" setup>
import { ref, inject } from "vue";
const fatherName = inject("name");
</script>
总结:组件间数据通信有很多种方式,实际开发中根据实际情况来选择用哪种方式实现。利用 v-model 也可以父子组件之间传递数据,但实际开发中用到的地方不多。
其他 API
shallowRef 和 shallowReactive
- 只处理对象最外层属性的响应式(也就是浅响应式),避免对内部每个属性做响应式带来的成本,可提升性能
readonly 和 shallowReadonly
- 用来保护数据,使其变为只读的,避免被修改
-
readonly
常用,因为shallowReadonly
只能处理浅层次的只读