最近学习 Vue3, 这里做个简单的记录,Vue3 在 Vue2 的升级,语法和 API 都发生了变化。下面就一些新增的 API,写一些简单的 demo 进行记录。
创建项目 vue3 创建项目,使用 Vite
脚手架。放弃了原来的 vue-cli
。
我尝试着删除 src
目录,重新新建一个 src 目录,自己写一个 src/main.ts
和 src/App.vue
。
会发现 ts 检查在 App.vue
中报错。这时因为 ts 的检查中,不认识.vue 文件,需要配置 env.d.ts
文件。cmd (Mac)/ctrl (Windows) + 左键点击下面的 "vite/client"
路径
会跳转到 vite/client
文件,添加:
1 2 3 4 5 declare module "*.vue" { import { ComponentOptions } from "vue" ; const componentOptions: ComponentOptions; export default componentOptions; }
即可
Vue3 入口文件一些概念
Vite
项目中,index.html
是项目的入口文件,在项目最外层。
加载 index.html
后,Vite
解析 <script type="module" src="xxx">
指回的 Javascript
Vue3 中是通过createApp
函数创建一个应用实例。
Options API
和 Composition API
Vue2 属于 Option API
(配置选项式 API),Vue3 属于 Composition API
(组合式 API),我愿意称他为功能性组合 API。
随着 Vue2 里的各个组件越来越复杂,配置式 API 缺点非常明显,所有的值散落在诸如 data(), methods(), computed(), watch()等等方法中,修改一处有可能要修改多处。
比起原来 Options API(例如:data()
,methods()
),setup
有两个特点:
一个 vue 文件可以同时具备 data()
, methods()
等 Vue2 的功能,也可以同时具备 setup()
写法的 Vue3 功能。
setup()
函数中,不能再使用 this
。而且setup
是最早的生命周期函数。所以 data() 和 methods() 函数中,可以访问 setup() 函数中定义的变量。但 setup() 函数中,不能访问 data() 和 methods() 函数中定义的变量。
setup()
这个是 Vue3 追赶 React 函数式编程的新概念,在 Vue2 里,setup
普通的写法,他是执行在生命周期里的 beforeCreate
阶段之前的函数,因此,Vue3 将 setup
函数提升到一个新的高度。
1 2 3 4 5 <script > export default { setup ( ) {}, }; </script >
语法糖 <script setup lang="ts">
鉴于 setup()
的特殊性,Vue3 将其独立为一个语法糖的写法,将其整合进 <script lang="ts" setup>
里,下面是 vue3 App.vue
文件的 setup 改法:
1 2 3 4 5 6 7 8 <template > <ChildComp /> </template > <script setup lang ="ts" > import ChildComp from "./components/WatchEffectDemo.vue" ; </script >
setup
插件: VueSetupExtend
增强 script 标签用法1 npm i vite-plugin-vue-setup-extend -D
vite.config.ts
配置:plugins 中添加 VueSetupExtend()
,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { fileURLToPath, URL } from "node:url" ;import { defineConfig } from "vite" ;import vue from "@vitejs/plugin-vue" ;import VueSetupExtend from "vite-plugin-vue-setup-extend" ;export default defineConfig({ plugins: [vue(), VueSetupExtend()], resolve: { alias: { "@" : fileURLToPath(new URL("./src" , import .meta.url)), }, }, });
ref
和 reactive
,将数变为响应式让我们看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <template > <div class ="car" > <h2 > {{ car.brand }} -> {{ car.price }}元</h2 > <button @click ="addPrice" > 修改价格+10万</button > <button @click ="minusPrice" > 修改价格-1万</button > <button @click ="changeCarByReactive" > 修改整车by reactive</button > </div > <div > <h2 > {{ car2.brand }} -> {{ car2.price }}元</h2 > <button @click ="changeCarByRef" > 修改整车by ref</button > </div > </template > <script lang ="ts" setup name ="Car12" > import { ref, reactive, toRefs } from "vue" ; let car = reactive({ brand : "Toyota" , price : 200000 }); let { price } = toRefs(car); const addPrice = () => (price.value += 100000 ); const minusPrice = () => (car.price -= 10000 ); const changeCarByReactive = () => { Object .assign(car, { brand : "Audi" , price : 300000 }); }; let car2 = ref({ brand : "Benz" , price : 800000 }); const changeCarByRef = () => { car2.value = { brand : "Audi" , price : 300000 }; }; </script >
ref
可以包裹任意数据类型,作为响应式数据,并实时修改视图的值
被 ref
包裹的数据,如果要更改页面的值,必须用 .value
修改。例如例子中的:car2.value = { brand: "Audi", price: 300000 };
reactive
只能用来包裹一个对象,作为响应式数据。
被 reactive
包裹的对象,如果要整体修改,必须用 Object.assign
修改。例如例子中的: Object.assign(car, { brand: "Audi", price: 300000 });
如果遇到解构,必须用toRefs
包裹普通对象,使其变为响应式对象,要不页面上的响应式数据就不会更新。例如例子中的:let { price } = toRefs(car);
computed
计算属性computed
是一个计算属性,它依赖其他数据,当依赖的数据发生变化时,计算属性会重新计算。这是我们在 Vue2 中经常使用的。
但是 Vue3 中,我们还可以用computed
自带的 getter 和 setter 来实现计算属性的获取和修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <template > <div class ="person" > 全名:<span > 这个是计算属性计算出来的全名:{{ fullName }}</span > <button @click ="changeFullName" > 点击修改姓名为Li si</button > </div > </template > <script lang ="ts" setup name ="Person" > import { ref, computed } from "vue" ; let firstName = ref("zhang" ); let lastName = ref("san" ); const fullName = computed({ get ( ) { return `${firstName.value .slice(0, 1) .toUpperCase()}${firstName.value.slice(1)} - ${lastName.value}`; }, set (val ) { const [f, l] = val.split("-" ); firstName.value = f; lastName.value = l; }, }); const changeFullName = () => (fullName.value = "li-si" ); </script >
watch
监视watch
是一个监听器,它监听某个数据,当数据发生变化时,执行回调函数。watch
监听的数据可以是一个响应式数据,也可以是一个普通数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <template > <div class ="person" > <h2 > 姓名:{{ person.name }}</h2 > <h2 > 汽车:{{ person.car.c1 }} {{ person.car.c2 }}</h2 > <button @click ="changeName" > 改名</button > <button @click ="changeCar1" > 改第一台🚗</button > <button @click ="changeCar2" > 改第二台🚗</button > <button @click ="changeCars" > 改所有🚗</button > </div > </template > <script lang ="ts" setup > import { reactive, watch } from "vue" ; let person = reactive({ name: "张三" , car: { c1: "奔驰" , c2: "宝马" , }, }); const changeName = () => (person.name += "~" ); const changeCar1 = () => (person.car.c1 = "audi" ); const changeCar2 = () => (person.car.c2 = "Toyota" ); const changeCars = () => (person.car = { c1 : "byd" , c2 : "yy" }); watch( () => person.name, (newValue, oldValue) => { console .log("newValue" , newValue); console .log("oldValue" , oldValue); } ); watch( () => person.car, (newValue, oldValue) => { console .log("newValue" , newValue); console .log("oldValue" , oldValue); }, { deep : true } ); watch([() => person.name, () => person.car.c1], (newValue, oldValue ) => { console .log("newValue" , newValue); console .log("oldValue" , oldValue); }); </script > <style scoped > </style >
和 watch
等价的,有另一个 api watchEffect
,它更智能,它接收一个函数,函数里可以写异步代码,当函数执行时,会自动收集依赖,当依赖的数据发生变化时,会重新执行函数。
下面写一个 demo 实现:下面是一个简单的监测水温和水位的例子,当水温达到或超过 60℃,或者水位达到或超过 80cm 时,给服务器发请求。这里的请求用了 console.log,实际应用中,可以发送请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template > <div > <h2 > 当水温达到或超过60℃,或者水位达到或超过80cm时,给服务器发请求</h2 > <h2 > 水温:{{ temp }}</h2 > <h2 > 水位:{{ height }}</h2 > <button @click ="changeTemp" > 水温+10 ℃</button > <button @click ="changeHeight" > 水位+10 cm</button > </div > </template > <script lang ="ts" setup > import { ref, watch, watchEffect } from "vue" ; let temp = ref(10 ); let height = ref(0 ); const changeTemp = () => { temp.value += 10; }; const changeHeight = () => { height.value += 10; }; watch([temp, height], (value ) => { let [newTemp, newHeight] = value; if (newTemp >= 60 || newHeight >= 80) { console .log("给服务器发请求" ); } }); watchEffect(() => { if (temp.value >= 60 || height.value >= 80) { console .log("给服务器发请求" ); } }); </script >
defineExpose
暴露响应式数据
ref
除了定义一个引用之外,还可以给真实 DOM 元素或组件元素打标记,然后通过 ref
获取到该元素或者组件元素实例。
和 vue2 不一样,vue3 的引用值 ref 必须用 defineExpose 暴露给父组件,父组件才能获取到他的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div ref ="x" > hello vue3</div > <button @click ="showRef" > get ref DOM</button > </template > <script lang ="ts" setup > import { ref, defineExpose } from "vue" ; let a = ref("1" ); defineExpose({ a }); let x = ref(); const showRef = () => { console .log(x.value); }; </script >
ref 打在组件标签上,能打印出组件实例
1 2 3 4 5 6 7 8 9 10 11 12 13 <template > <RefDemo ref ="aa" /> <button @click ="showRef" > showRef</button > </template > <script setup lang ="ts" > import RefDemo from "./components/RefDemo.vue" ; import { ref } from "vue" ; let aa = ref(); const showRef = () => { console .log(aa.value); }; </script >
props
, defineProps
和 withDefaults
不废话,直接上 demo,和 React 不同的是,子组件如果要显示 Props,需要用defineProps
定义,否则会报错。
父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div class ="app" > <PropsDemo a ="hehe" :personsList ="persons" /> </div > </template > <script setup lang ="ts" > import PropsDemo from "./components/PropsDemo.vue" ; let persons = [ { id : 1 , name : "张三" , age : 18 }, { id : 2 , name : "李四" , age : 19 }, ]; </script >
子组件的写法就有点奇葩,如果定义默认值,还要再引入一个withDefaults
,他的定义是比 vue2 啰嗦的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div > {{ a }}</div > <ul > <li :key ="person.id" v-for ="person in personsList" > {{ person.name }}: {{ person.age }} </li > </ul > </template > <script lang ="ts" setup > import { defineProps, withDefaults } from "vue" ; import type { Person } from "@/types" ; const props = withDefaults( defineProps<{ a: string; personsList: Person[] }>(), { a: "xx" , personsList: () => [{ id : "kk" , name : "yy" , age : 18 }], } ); console .log(props); </script >
生命周期 Vue2
的生命周期: 创建阶段:beforeCreate
created
挂载阶段:beforeMount
mounted
更新阶段:beforeUpdate
updated
销毀阶段:beforeDestroy
destroyed
Vue3
的生命周期:
创建阶段:setup
挂载阶段:onBeforeMount
onMounted
更新阶段:onBeforeUpdate
onUpdated
销毁阶段:onBeforeUnmount
onUnmounted
由于 Vue3 我们把 setup 阶段写在 script 标签里,所以 Vue3 的生命周期比 Vue2 的少了 2 个,分别是beforeCreate
和created
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script > export default { created ( ) { console .log("created" ); }, }; </script > <script setup lang ="ts" > onMounted(() => { console .log("onMounted" ); }); </script >
Custom Hooks 模块化封装 这个概念完全是抄袭 React hooks,由于 Vue3 设计成了setup
语法,所以使得函数式编程称为可能,下面是一个 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template > <div class ="person" > <img v-for ="(image, idx) in dogImages" :src ="image" :key ="idx" alt ="dog" /> <br /> <button @click ="addNewDogImage" > 添加狗图</button > </div > </template > <script lang ="ts" setup > import useDogImages from "@/hooks/useDogImages" ; const { dogImages, addNewDogImage } = useDogImages(); </script > <style scoped > img { height : 150px ; margin-right : 10px ; } </style >
在 useDogImages.ts
文件里,可以写下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { ref, reactive } from "vue" ;export default (url: string = "https://dog.ceo/api/breeds/image/random" ) => { const defaultDogImage = "https://images.dog.ceo/breeds/affenpinscher/n02110627_11811.jpg" ; const dogImages = reactive([defaultDogImage]); const addNewDogImage = async () => { fetch(url) .then((res ) => res.json()) .then((data ) => dogImages.push(data.message)) .catch((err ) => alert(err)); }; return { dogImages, addNewDogImage, }; };
Pinia
状态管理pinia
是一个符合直觉 的状态管理库,和vuex
一样,都是基于vue3
的,但是pinia
比vuex
更简单易用,而且vuex
的作者已经推荐使用pinia
了。pinia
的使用非常简单,只需要在main.ts
里引入createPinia
,然后app.use(createPinia())
,然后就可以在setup
函数里使用defineStore
定义一个状态了。
createApp
main.ts
引入:
1 2 3 4 5 6 7 8 9 import { createApp } from "vue" ;import App from "./App.vue" ;import { createPinia } from "pinia" ;const app = createApp(App);const pinia = createPinia();app.use(pinia); app.mount("#app" );
defineStore
, state
, actions
创建一个store/count.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { defineStore } from "pinia" ;const useCountStore = defineStore("count" , { actions: { addToMaxTwenty (value: number ) { if (this .count <= 20 ) { this .count += value; } }, }, state ( ) { return { count: 0 , by: "X" , }; }, }); export default useCountStore;
在应用中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template > <div class ="talk" > <div > <h1 > count:{{ countStore.count }}</h1 > <h2 > countStore的其他数据: {{ countStore.by }}</h2 > <select v-model.number ="n" > <option value ="1" selected > 1</option > <option value ="2" > 2</option > <option value ="3" > 3</option > </select > <button @click ="plus" > +</button > <button @click ="minus" > -</button > </div > </div > </template > <script lang ="ts" setup > import { ref, reactive } from "vue" ; import useCountStore from "@/store/count" ; import useTalkStore from "@/store/talks" ; const countStore = useCountStore(); const n = ref(1 ); const plus = () => { countStore.addToMaxTwenty(n.value); }; const minus = () => (countStore.count -= n.value); </script >
defineStore
第二个参数改写为组合式 API上面的 talkStore
的例子,第二个参数,我们写的是配置式的 API(OptionAPI),但是 pinia
也支持组合式 API,可以改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { defineStore } from "pinia" ;import { reactive } from "vue" ;type Talk = { id: number ; content: string ; }; const useTalkStore = defineStore("talks" , () => { const talkList = reactive<Talk[]>( JSON .parse(localStorage .getItem("talkList" ) as string ) ?? [] ); const assertOneNewTalk = async () => { try { const response = await fetch( "https://api.uomg.com/api/rand.qinghua?format=json" ); if (!response.ok) throw new Error ("Network response was not ok" ); const { content } = await response.json(); return talkList.push({ id : talkList.length + 1 , content }); } catch (err) { console .error(err); } }; return { talkList, assertOneNewTalk }; }); export default useTalkStore;
storeToRefs
如果我们要省略上面的写法 countStore.count
,在 pinia
中,可以使用 storeToRefs
解构,这样我们就可以省略 countStore.count
,直接使用 count
。 这里最好不要使用 vue
自带的 **toRefs
**,因为 toRefs
会将所有属性都变成响应式,而 storeToRefs
只会将 state
中的属性变成响应式。
TalkStore
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template > <div class ="talk" > <button @click ="addNewTalk" > 获取一句土味情话</button > <ul > <li v-for ="talk in talkList" :key ="talk.id" > {{ talk.content }}</li > </ul > </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; import { storeToRefs } from "pinia" ; import useCountStore from "@/store/count" ; import useTalkStore from "@/store/talks" ; const talkStore = useTalkStore(); const { talkList } = storeToRefs(talkStore); const addNewTalk = () => talkStore.assertOneNewTalk(); </script >
store/talks
文件里,可以直接请求数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { defineStore } from "pinia" ;type Talk = { id: number ; content: string ; }; const useTalkStore = defineStore("talks" , { actions: { async assertOneNewTalk ( ) { try { const response = await fetch( "https://api.uomg.com/api/rand.qinghua?format=json" ); if (!response.ok) throw new Error ("Network response was not ok" ); const { content } = await response.json(); return this .talkList.push({ id : this .talkList.length + 1 , content }); } catch (err) { console .log(err); } }, }, state ( ) { return { talkList: [] as Talk[], }; }, }); export default useTalkStore;
getters
处理数据接着上面的例子,我们在 defineStore
函数中加入:getters
, 如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { defineStore } from "pinia" ;const useCountStore = defineStore("count" , { actions: { addToMaxTwenty (value: number ) { if (this .count <= 20 ) { this .count += value; } }, }, state ( ) { return { count: 0 , by: "X" , }; }, getters: { doubleCount: (state ) => state.count * 2 , square(): number { return this .count ** 2 ; }, }, }); export default useCountStore;
这样就可以在 template
里面直接使用 doubleCount
和 square
了。
$subscribe
store 的订阅接着上面TalkStore
的例子,添加$subscribe
,监视数据,并将其存在浏览器,进行简单的数据持久化,如:
1 2 3 talkStore.$subscribe((_, state ) => { localStorage .setItem("talkList" , JSON .stringify(state.talkList)); });
再将state
里的talkList
换成在 localStorage
里取:当然,必须判断第一次打开页面时,localStorage
里没有数据,所以需要写一下空值判断符:
1 2 3 state: () => ({ talkList: JSON .parse(localStorage .getItem("talkList" ) as string ) ?? [], });
vue-router
路由安装
创建路由 创建 src/router/index.ts
,当然,前提还必须创建页面的组件如 Home.vue
等 3 个,这里不详细介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { createRouter, createWebHistory } from "vue-router" ;import Home from "@/views/Home.vue" ;import News from "@/views/News.vue" ;import About from "@/views/About.vue" ;const router = createRouter({ history: createWebHistory(), routes: [ { path: "/" , name: "Home" , component: Home, }, { path: "/news" , name: "News" , component: News, }, { path: "/about" , name: "About" , component: About, }, ], }); export default router;
history 模式 在 Nginx
上配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 server { listen 80; server_name your-domain.com; root /path/to/your/webroot; index index.html; location / { try_files $uri $uri / /index.html; } }
在 main.ts
中,引入路由,并挂载到 app
上:
1 2 3 4 5 6 7 import { createApp } from "vue" ;import App from "./App.vue" ;import router from "./router" ;const app = createApp(App);app.use(router); app.mount("#app" );
使用路由 在路由页面可以显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div class ="router-demo" > <h2 class ="title" > Vue Router</h2 > <div class ="navigator" > <RouterLink to ="/" active-class ="active" > 首页</RouterLink > <RouterLink :to ="{ name: 'News' }" active-class ="active" > 新闻</RouterLink > <RouterLink :to ="{ path: '/about' }" active-class ="active" > 关于</RouterLink > </div > <div class ="content" > <RouterView > </RouterView > </div > </div > </template > <script lang ="ts" setup > import { RouterView, RouterLink } from "vue-router" ; </script > <style scoped > .navigator a .active { background-color : rgb (76 , 146 , 199 ); font-weight : 900 ; text-shadow : 0 0 1px black; } </style >
<RouterView></RouterView>
为各个路由组件的占位符,
<RouterLink to="/" active-class="active"/>
是<a/>
标签的二次封装
属性 to
有 2 种写法,可to="/"
, 也可 :to="{path: '/'}"
或 :to="{name: 'Home'}"
, 这里的 name 对应路由文件 router/index.ts
里定义的 name
路由导航到的页面,挂载到页面的根节点上,切换时则卸载该节点,可以用在具体页面里用 onMounted
和 onUnmounted
来监听,验证这点。
子路由 在上面我们创建的 router/index.ts
文件里,用 news
页面创建二级路由, 在 /news
路由下,添加 children
数组,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import NewsDetail from '@/views/NewsDetail.vue' ... { path: '/news' , name: 'News' , component: News, children: [ { path: 'newsDetail' , name: 'NewsDetail' , component: NewsDetail } ] } ...
路由传参 query
传参这种是放在 url 的后面,如:/news?id=1&title=xxx&content=xxx
,比较啰嗦,不太推荐。 改造 views/News.vue
文件,添加 RouterLink
组件,并添加 query
参数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template > <div class ="news" > <ul > <li v-for ="news in newsList" :key ="news.id" > <RouterLink :to="{ path: '/news/newsDetail', query: { id: news.id, title: news.title, content: news.content } }" >{{ news.title }} </RouterLink > </li > </ul > <div class ="news-content" > <RouterView > </RouterView > </div > </div > </template > <script lang ="ts" setup > import { reactive } from "vue" ; import { RouterView, RouterLink } from "vue-router" ; const newsList = reactive([ { id : 1 , title : "1111111" , content : "aaaaaa" }, { id : 2 , title : "2222222" , content : "bbbbbb" }, { id : 3 , title : "3333333" , content : "cccccc" }, ]); </script >
在 views/NewsDetail.vue
文件里,添加 props
接收传参,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div > <div > 编号:{{ query.id }}</div > <div > 标题:{{ query.title }}</div > <div > 内容:{{ query.content }}</div > </div > </template > <script lang ="ts" setup > import { toRefs } from "vue" ; import { useRoute } from "vue-router" ; const route = useRoute(); const { query } = toRefs(route); </script >
params
传参这种传参是放在 url 的后面,如:/news/newsDetail/1
,写起来比 query 传参简洁些
先改造 router/index.ts
文件,添加 params
参数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 { path: '/news' , name: 'News' , component: News, children: [ { path: 'newsDetail/:id/:title/:content' , name: 'NewsDetail' , component: NewsDetail } ] },
在views/News.vue
文件,改造如下,值得注意的是,写法 2 对象里不能用 path
,必须写路由里的 name
参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <template > <div class ="news" > <ul > <li v-for ="news in newsList" :key ="news.id" > <RouterLink :to="{ name: 'NewsDetail', params: { id: news.id, title: news.title, content: news.content } }" >{{ news.title }} </RouterLink > </li > </ul > <div class ="news-content" > <RouterView > </RouterView > </div > </div > </template > <script lang ="ts" setup > import { reactive } from "vue" ; import { RouterView, RouterLink } from "vue-router" ; const newsList = reactive([ { id : 1 , title : "1111111" , content : "aaaaaa" }, { id : 2 , title : "2222222" , content : "bbbbbb" }, { id : 3 , title : "3333333" , content : "cccccc" }, ]); </script >
NewsDetail.vue
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div > <div > 编号:{{ params.id }}</div > <div > 标题:{{ params.title }}</div > <div > 内容:{{ params.content }}</div > </div > </template > <script lang ="ts" setup > import { toRefs } from "vue" ; import { useRoute } from "vue-router" ; const route = useRoute(); const { params } = toRefs(route); </script >
路由props
参数 上面提到的params
传参,都可以通过 props
参数来接收,最推荐 这种写法,改写如下:
在 router/index.ts
文件里,添加 props
参数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { path: '/news' , name: 'News' , component: News, children: [ { path: 'newsDetail/:id/:title/:content' , name: 'NewsDetail' , component: NewsDetail, props: true , } ] }
views/News.vue
文件 和上面提到的一样,无需改动,views/NewsDetail.vue
改造如下:
1 2 3 4 5 6 7 8 9 10 11 <template > <div > <div > 编号:{{ id }}</div > <div > 标题:{{ title }}</div > <div > 内容:{{ content }}</div > </div > </template > <script lang ="ts" setup > defineProps(["id" , "title" , "content" ]); </script >
replace
replace
是一个<RouterLink>
组件的属性,加上则表示不缓存该页面,不能点击浏览器上的后退按钮,如:
1 <RouterLink replace to ="/" active-class ="active" > 首页</RouterLink >
useRouter
编程式路由导航上面我们提到的例子都是<RouterLink>
组件,实际应用中,还有一种编程式路由导航,使用频率远大于<RouterLink>
组件,例如我们用上面的例子,在首页有一个 button,点击可以跳转到 news 新闻页面,views/Home.vue
文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template > <div > home page</div > <button @click ="jumpToNews" > jump to news page</button > </template > <script lang ="ts" setup > import { useRouter } from "vue-router" ; const navigateTo = useRouter(); const jumpToNews = () => { navigateTo.push({ path: "/news" , }); }; </script >
redirect
重定向这个太简单,不赘述了,直接上代码router/index.ts
:
1 2 3 4 { path: "/" , redirect: "/home" , }
组件间通信 ICC inner-component communication
父子互传 definedProps
和回调函数父子组件间通信,demo 如下:
父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template > <div class ="parent" > <h3 > parent</h3 > <h4 > parent's house:{{ house }}</h4 > <h4 > toy fr child:{{ toy }}</h4 > <Child :house ="house" :sendToyToParent ="getToyFromChild" /> </div > </template > <script lang ="ts" setup > import Child from "./ChildComp.vue" ; import { ref } from "vue" ; const house = ref("🏠" ); const toy = ref("" ); const getToyFromChild = (value: string ) => { console .log(value); toy.value = value; }; </script >
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <div class ="app" > house fr parent:{{ house }} <div > my toy: {{ toy }}</div > <button @click ="sendToyToParent(toy)" > send to parent</button > </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; const toy = ref("🐻" ); defineProps(["house" , "sendToyToParent" ]); </script >
v-model
双绑 -> 组件封装说实话,这个属性在项目中是不常用的特性,项目中我们多用第三方组件库,
而 vue2 的v-model
必须写在原生的 input 框上,
但 vue3 中的 v-model
,可以直接写在组件标签上 ,有了这个特性,可以使其拥有更多的用法
首先,我们先看一下 v-model
在 vue2 中的实现原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template > <div > {{ inputValue }}</div > <input v-model ="inputValue" type ="text" /> <input :value="inputValue" type="text" @input="inputValue = ($event.target as HTMLInputElement).value" /> </template > <script lang ="ts" setup > import { ref } from "vue" ; const inputValue = ref("hello" ); </script >
原始的 v-model
是一个语法糖,实际上,是将 input 框的 :value
和 @input
,利用这个特性,我们可以实现一个多 input 的组件,实现和 vue2 的 v-model 一样的效果。
被封装的多 input 组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template > <input type="text" :value="uName" @input="emit('update:uName', ($event.target as HTMLInputElement).value)" /> <input type="password" :value="pwd" @input="emit('update:pwd', ($event.target as HTMLInputElement).value)" /> </template > <script lang ="ts" setup > defineProps(["uName" , "pwd" ]); const emit = defineEmits(["update:uName" , "update:pwd" ]); </script > <style scoped > input { background-color : #26b9b1 ; } </style >
在页面里使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 <template > <h4 > {{ userName }}</h4 > <h4 > {{ userPwd }}</h4 > <Inputs v-model:uName ="userName" v-model:pwd ="userPwd" /> </template > <script lang ="ts" setup > import { ref } from "vue" ; import Inputs from "./FormInputs.vue" ; const userName = ref("hello" ); const userPwd = ref("123456" ); </script >
通过以上的例子,可以看出以下特征:
被封装的组件中,:value
和 @input
事件,实现了使用中 <Inputs v-model:uName="userName" v-model:pwd="userPwd" />
的 v-model
效果。
v-model:uName="userName"
可以取别名uName
,我们在封装的组件里提取时,也可以使用其别名
slot
相当于 react 的 children
,自定义组件双标签中包的那一块内容
匿名插槽:数据 父->子 父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template > <div class ="parent" > <h3 > parent Comp</h3 > <div class ="content" > <Category title ="热门游戏列表" > <ul > <li v-for ="game in ganmes" :key ="game.id" > {{ game.name }}</li > </ul > </Category > </div > </div > </template > <script lang ="ts" setup > import Category from "./CategoryBoard.vue" ; import { reactive } from "vue" ; const ganmes = reactive([ { id : 1 , name : "英雄联盟" }, { id : 2 , name : "王者荣耀" }, { id : 3 , name : "绝地求生" }, { id : 4 , name : "原神" }, ]); </script >
子组件:相当于用 slot 进行占位 html 的内容:
1 2 3 4 5 6 7 8 9 10 11 <template > <div class ="category" > <h2 > {{ title }}</h2 > <slot > 默认内容</slot > </div > </template > <script lang ="ts" setup > defineProps(["title" ]); </script >
具名插槽:数据 父->子 父组件:其中 #s1 #s2 是插槽的名称,可以自定义,#s1 号是v-slot:s1
的缩写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template > <div class ="parent" > <h3 > parent Comp</h3 > <div class ="content" > <Category title ="热门游戏列表" > <template #s1 > <h2 > 热门游戏列表</h2 > </template > <template #s2 > <div class ="games" > <div v-for ="game in ganmes" :key ="game.id" > {{ game.name }}</div > </div > </template > </Category > </div > </div > </template > <script lang ="ts" setup > import Category from "./CategoryBoard.vue" ; import { reactive } from "vue" ; const ganmes = reactive([ { id : 1 , name : "英雄联盟" }, { id : 2 , name : "王者荣耀" }, { id : 3 , name : "绝地求生" }, { id : 4 , name : "原神" }, ]); </script >
子组件:可以看到,slot 分为 s1 和 s2,分别对应子组件中的上下的两个插槽
1 2 3 4 5 6 <template > <div class ="category" > <slot name ="s1" > 默认内容1</slot > <slot name ="s2" > 默认内容2</slot > </div > </template >
作用域插槽:数据 子->父 简单来说,这个是要解决数据放在子组件处,将子组件数据传递给父组件而设计的插槽。
父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <div class ="game" > <ScopedSlotChild > <template v-slot:game-slot ="params" > <ol > <li v-for ="game in params.games" :key ="game.id" > {{ game.name }}</li > </ol > </template > </ScopedSlotChild > </div > </template > <script lang ="ts" setup > import ScopedSlotChild from "./ScopedSlotChild.vue" ; </script >
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template > <div class ="game" > <h2 > 游戏列表</h2 > <slot name ="game-slot" :games ="games" > </slot > </div > </template > <script lang ="ts" setup > import { reactive } from "vue" ; const games = reactive([ { id : 1 , name : "Dota 2" }, { id : 2 , name : "Counter-Strike: Global Offensive" }, { id : 3 , name : "League of Legends" }, ]); </script >
以上有几个注意的点:
slot 的 name 属性 name="game-slot"
,可以省略,当 slot 没有 name 属性,那父组件也不用起别名
slot 的 name 属性如果出现,那么在父组件的别名,例如:v-slot:game-slot="params"
的:game
必须和 slot 的 name 属性一致
当 slot 的 name 属性值为 default 时,父组件的别名也可以省略
emit
事件这种方案其实我比较反感,因为上面的回调函数完全可以取代他,但既然框架提供了,那也得介绍一下
接着上面的例子,简单来说就是在子组件中
将点击事件加上 emit
api
根据 vue2 的文档,将命名由上面的驼峰法,改为 kebab-case
命名法则,即用 -
连接
1 2 3 4 5 6 7 <button @click ="emit('send-toy-to-parent', toy)" > send to parent</button > <script > const emit = defineEmits(["send-toy-to-parent" ]); </script >
父组件中,接收子组件的点击事件,并做相应处理
将原来的 :click
改为 vue2 的写法: @send-toy-to-parent
,如下:
1 <Child :house ="house" @send-toy-to-parent ="getToyFromChild" />
$ref
-> 父子, $parent
-> 子父先上代码,父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template > <div class ="parent" > <h3 > parent comp</h3 > <h4 > house: {{ house }}</h4 > <button @click ="getAllChildrenBooks($refs)" > + Children 3 books</button > <Child1Comp ref ="child1" /> <Child2Comp ref ="child2" /> </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; import Child1Comp from "./Child1Comp.vue" ; import Child2Comp from "./Child2Comp.vue" ; const house = ref(4 ); const getAllChildrenBooks = (refs: { [x: string]: any } ) => { for (let key in refs) { refs[key].books += 3; } }; defineExpose({ house }); </script >
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template > <div class ="child" > <h3 > Child 1 Comp</h3 > <h4 > books: {{ books }}</h4 > <button @click ="minusHouse($parent)" > take one house</button > </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; const books = ref(3 ); defineExpose({ books }); const minusHouse = (parent: any ) => { if (parent.house <= 0 ) return ; parent.house--; }; </script >
可以看出这个属性得配合 defineExpose()
使用,且必须是父子组件
在各自组件里,通过回调函数 $parent
获取父组件,通过 $refs
获取子组件
这种写法,允许在父组件获取多个子组件的变量值数据
隔代\跨代传值 $attr
ParentComp.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template > <div class ="parent" > <h1 > Parent Comp</h1 > <h2 > a: {{ a }}</h2 > <h2 > b: {{ b }}</h2 > <ChildComp :a ="a" :b ="b" v-bind ="{ x: 100 }" :updateA ="updateA" /> </div > </template > <script lang ="ts" setup > import ChildComp from "./ChildComp.vue" ; import { ref } from "vue" ; const a = ref(1 ); const b = ref(2 ); const updateA = (val: number ) => { a.value += val; }; </script >
ChildComp.vue
中间的组件,仅写一个属性就够了 v-bind="$attrs"
1 2 3 4 5 6 7 8 9 10 <template > <div class ="child" > <h1 > Child Comp</h1 > <GrandChildComp v-bind ="$attrs" /> </div > </template > <script lang ="ts" setup > import GrandChildComp from "./GrandChildComp.vue" ; </script >
GrandChildComp.vue
最底层的组件直接接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div class ="grandChild" > <h1 > Grand Child Comp</h1 > <h2 > props from Parent:</h2 > <h3 > {{ a }}</h3 > <h3 > {{ b }}</h3 > <h3 > {{ x }}</h3 > <button @click ="updateA(3)" > cb to parent updateA</button > </div > </template > <script lang ="ts" setup > defineProps(["a" , "b" , "x" , "updateA" ]); </script >
从上面的代码可以看出,中间的层级如果只写 v-bind="$attrs"
就可以无限层级的传参,而从最底层的子组件开始,如果要回传到最顶层,也只需写回调函数即可
provide
+ inject
这个比起上一个更简单,上一个中间组件还需要写一个 v-bind="$attrs"
属性,而这种方法中间组件不用添加任何属性
祖先组件用 provide 注入数据,后代组件用 inject 获取数据
祖先组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template > <div class ="parent" > <h1 > Parent Comp</h1 > <h2 > house: {{ house }} 万元</h2 > <h2 > car: {{ car.price }} 万元</h2 > <ChildComp /> </div > </template > <script lang ="ts" setup > import ChildComp from "./ChildComp.vue" ; import { ref, reactive, provide } from "vue" ; const house = ref(800 ); const car = reactive({ brand: "Benz" , price: 80, }); const updateHousePrice = (value: number ) => { house.value -= value; }; provide("houseContext" , { house, updateHousePrice }); provide("car" , car); </script >
最底层的孙子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template > <div class ="grandgrandChild" > <h1 > Grand Grand Child Comp</h1 > <h2 > house & car from parent:</h2 > <h3 > house: {{ house }} 万元</h3 > <h3 > car: 一辆{{ y.brand }}车,价值{{ y.price }} 万元</h3 > <button @click ="updateHousePrice(1)" > 修改house价值</button > </div > </template > <script lang ="ts" setup > import { inject } from "vue" ; const { house, updateHousePrice } = inject("houseContext" , { house: 0, updateHousePrice: (num: number ) => {}, }); const y = inject("car" , { brand : "BMW" , price : 60 }); </script >
从上面的代码可以看出:
provide
的第一个参数是定义所传的值的名字,第二个参数可以是单一值,也可以是复杂的对象
inject
的第一个参数是定义所传的值的名字,第二个参数是默认值,如果传值失败,则使用默认值
provide
和 inject
是 vue3 的新特性,在 vue2 中,可以通过 $parent
和 $children
来实现
任意组件互传 mitt
这是一个独立压缩后只有几百字节的库 mitt
,node 环境下均可用,不仅仅局限于 vue
创建 utils/emitter.ts
文件
1 2 3 4 5 6 7 8 import mitt, { type Emitter } from "mitt" ;type Events = { [key: string ]: string ; }; const emitter: Emitter<Events> = mitt();export default emitter;
组件 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <div class ="comp1" > <h2 > 组件1</h2 > <h3 > 我的玩具:{{ toyCar }}</h3 > <button @click ="emitter.emit('send-to-comp2', toyCar)" > 发送给组件2</button > </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; import emitter from "@/utils/emitter" ; const toyCar = ref("🚙" ); </script >
组件 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template > <div class ="comp2" > <div > my toy: {{ toyCarFrComp1 }}</div > </div > </template > <script lang ="ts" setup > import { ref, onUnmounted } from "vue" ; import emitter from "@/utils/emitter" ; const toyCarFrComp1 = ref("" ); emitter.on("send-to-comp2" , (value: string ) => { toyCarFrComp1.value = value; }); onUnmounted(() => { emitter.off("send-to-comp2" ); }); </script >
发送数据方,可以看到上面的 emitter
里调用了 .emit()
方法,.emit()
方法接收两个参数,第一个参数是事件名称,第二个参数是事件携带的数据。
接受数据方,可以看到上面的 emitter
里调用了 .on()
方法,.on()
方法接收两个参数,第一个参数是事件名称,第二个参数是事件监听函数。赋值给页面对应的变量名
onUnmounted
的作用是在组件卸载时,也卸载掉监听事件,避免内存留存
Pinia
这种在上面单独拎出来的讲了,这里就不多说了,直接看上面的例子
customRef
自定义响应式这是一个常用的 api,例如实现防抖,我们用普通 ref 时加上双向数据绑定时,input 框输入实时变化都会实时渲染,如果页面信息过多,那么会影响性能,所以需要用到 customRef
实现防抖功能:
我们把他写为一个钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { customRef } from "vue" ;const useDelayInput = (initVal: string = "" , delay: number = 500 ) => { const msg = customRef((track, trigger ) => { return { get ( ) { track(); return initVal; }, set (value ) { clearTimeout (delay); delay = setTimeout (() => { initVal = value; trigger(); }, delay); }, }; }); return msg; }; export default useDelayInput;
使用:
1 2 3 4 5 6 7 8 9 10 11 <template > <div class ="app" > <h2 > {{ msg }}</h2 > <input type ="text" v-model ="msg" /> </div > </template > <script lang ="ts" setup > import useDelayInput from "../hooks/useDelayInput" ; const msg = useDelayInput("customRef" , 800 ); </script >
从钩子函数可以出:
customRef
里是一个函数,返回一个对象,对象里有 get
和 set
方法,get
方法是读取数据,set
方法是修改数据
track
和 trigger
是两个函数,track
是读取数据时调用,trigger
是修改数据时调用
track
的作用是告诉 vue 追踪依赖,trigger
是告诉 vue 触发依赖
shallowRef
和 shallowReactive
这 2 个 api 旨在对付那些比较大的 Object,只监听第一层,层次比较深的数据不会被监听到
shallowRef
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script lang ="ts" setup > import { shallowRef, shallowReactive } from "vue" ; const toy = shallowRef({ toy: "🚗" , }); const change = () => { toy.value = { toy: "🚙" , }; }; </script >
shallowRef
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script lang ="ts" setup > import { shallowReactive } from "vue" ; const nations = shallowRef({ China: "CN" , }); const changenations = () => { nations.value = { China: "中国" , }; }; </script >
shallowReactive
举例:
1 2 3 4 5 6 7 8 9 10 11 <script lang ="ts" setup > const toys = shallowReactive({ car: "🚗" , animals: { dog : "🐭" , cat : "🐱" }, }); const changeToyCar = () => (toys.car = "🚙" ); const changeAnimals = () => (toys.animals.dog = "🐶" ); </script >
但当我把上面 const changeToyCar = () => (toys.car = "🚙");
函数改为:
1 2 3 4 5 6 7 8 <script lang ="ts" setup > const changeToyCar = () => { toys.car = "🚙" ; toys.animals.dog = "🐶" ; }; </script >
确两个都成功了,toys.animals.dog
,尽管这个属性不是响应式的,但因为之前对 toys.car
的修改已经触发了更新,所以任何依赖于 toys
的视图都会重新渲染,这时你会看到 toys.animals.dog
的新值。所以不推荐这种写法,他与shallowRef
的原意背道而驰,直接用 ref
即可。
readOnly
和 shallowReadOnly
对象整体只读和浅层只读,只针对于 reactive
包裹的对象属性,看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <template > <div > <p > Deep Readonly Car: {{ deepReadonlyCar }}</p > <p > Shallow Readonly Car: {{ shallowReadonlyCar }}</p > <p > Nested Property in Shallow Readonly: {{ shallowReadonlyCar.details.color }} </p > <button @click ="tryToModify" > Try to Modify</button > </div > </template > <script lang ="ts" setup > import { readonly, shallowReadonly, reactive } from "vue" ; const car = reactive({ model: "Tesla Model 3" , details: { color: "Red" , year: 2023, }, }); const deepReadonlyCar = readonly(car); const shallowReadonlyCar = shallowReadonly(car); const tryToModify = () => { deepReadonlyCar.model = "Tesla Model S" ; shallowReadonlyCar.model = "Tesla Model X" ; shallowReadonlyCar.details.color = "Blue" ; }; </script >
toRaw
和 markRaw
toRaw
: 这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特辣方法。不建议保存对原始对象的持久引用,请还慎使用。toRaw
何时使用?—-在露要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象
markRaw
标记一个对象,使其永远不会变成响应式的
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template > <div > <p > Raw Value: {{ rawValue.foo }}</p > <p > Marked Raw Value: {{ markedRawValue.foo }}</p > <button @click ="modifyToRaw" > Modify to Raw</button > <button @click ="modifyMarkRaw" > Modify Marked Raw</button > </div > </template > <script setup > import { reactive, toRaw, markRaw } from "vue" ; const reactiveValue = reactive({ foo : "Reactive Foo" }); const rawValue = toRaw(reactiveValue); const originalObject = { foo : "Marked Raw Foo" }; const markedRawValue = markRaw(originalObject); const modifyToRaw = () => { rawValue.foo = "Modified Raw Foo" ; }; const modifyMarkRaw = () => { markedRawValue.foo = "Modified Marked Raw Foo" ; }; </script >
Teleport
这个类似 React 的 createPortal
功能。 我们看看 React 官网的示例代码:
1 2 3 4 <div> <SomeComponent /> {createPortal(children, domNode, key?)} </div>
实际使用中,我们多把弹框从底层传送到 <body>
上,而不是某层的子组件上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div > <button @click ="showModal = true" > pop out modal</button > <teleport to ="body" > <div v-if ="showModal" class ="modal" > <h2 > title</h2 > <p > this is a modal create by teleport</p > <button @click ="showModal = false" > close</button > </div > </teleport > </div > </template > <script lang ="ts" setup > import { ref } from "vue" ; const showModal = ref(false ); </script >
suspence
异步组件参考了 React 18 的 suspence
组件,suspence
也是 Vue3 的新特性,用于实现组件的懒加载。
使用例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template > <div > <h1 > Vue 3 Suspense Example</h1 > <Suspense > <template #default > <FetchComp /> </template > <template #fallback > <h1 > Loading...</h1 > </template > </Suspense > <FetchComp /> </div > </template > <script setup lang ="ts" > import { defineAsyncComponent, Suspense } from "vue" ; import FetchComp from "./FetchComp.vue" ; const AsyncComponent = defineAsyncComponent(() => import ("./AsyncComp.vue" )); </script >
FetchComp.vue
1 2 3 4 5 6 7 8 9 10 11 <template > <h2 > fetch comp</h2 > <div class ="chlid" > {{ content }}</div > </template > <script lang ="ts" setup > const request = await fetch( "https://api.uomg.com/api/rand.qinghua?format=json" ); const { content } = await request.json(); </script >
全局 api Vue3 在 main.ts
中,可以用全局 api 注册全局组件,
app.component(name, component)
注册全局组件
app.directive(name, directive)
注册全局指令
app.provide(key, value)
注册全局 provide/inject
app.config.globalProperties.$foo = 'bar'
注册全局属性
app.unmount(element)
卸载应用
app.use(plugin)
安装插件
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { createApp } from "vue" ;import App from "./App.vue" ;import { createPinia } from "pinia" ;import router from "./router" ;import Hello from "./GlobalHello.vue" ;const app = createApp(App);const pinia = createPinia();app.use(pinia); app.use(router); app.mount("#app" ); app.component("globalHello" , Hello); app.config.globalProperties.globalVariable = 999 ; declare module "vue" { interface ComponentCustomProperties { globalVariable: number ; } } app.directive("focus" , (ele ) => { ele.style.color = "red" ; ele.style.backgoundColor = "green" ; });
在任意一个 src 下的组件中使用,且不需要 import
1 2 3 4 5 6 7 <template > <GlobalHello /> <div > {{ globalNumber }}</div > <input v-focus /> </template > <script lang ="ts" setup > </script >
效果如下图:
非兼容性改变 可以参考官方文档
重点关注这些比较常用的:
过渡类名 v-enter
修改为 v-enter-from
、过渡类名 v-leave
修改为 v-leave-from
。
keyCode
作为 v-on
修饰符的支持
v-model
指令在组件上的使用已经被重新设计,替换掉了 y-bind.sync
v-if
和 v-for
在同一个元素身上使用时的优先级发生了变化。v-if
优先级 > v-for
优先级
移除了 $on
、$off
和 $once
实例方法。
移除了过滤器 filter
移除了 $children
实例 propert