首先声明:这篇笔记中是在学习禹神最新Vue3教程记下来的,其中有一些自己写的笔记见解、搬禹神、vue、vite官网的一些笔记。主要目的还是给自己多一点知识点的总结。其实一遍下来,基本大部分都会,所以学习会很快。有几点我得好好在下面总结。
1.学习到的新点
2.学习的建议
备注:现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。
## 查看 @vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
vue -V
## 安装或 升级你的 @vue/cli
npm install -g @vue/cli
## 执行创建命令
vue create vue_test
## 随后选择 3.x
> Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
## 下面基于webpack搭建项目成功后执行下面的命令就是成功创建Vue3项目!!!
? Please pick a preset: Default ([Vue 3] babel, eslint)
Vue CLI v5.0.8
✨ Creating project in F:\桌面\vue_test.
Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
added 864 packages in 35s
Invoking generators...
Installing additional dependencies...
added 103 packages in 7s
⚓ Running completion hooks...
Generating README.md...
Successfully created project vue_test.
Get started with the following commands:
$ cd vue_test
$ npm run serve
## 创建命令
npm create vue@latest
## 一些可选项
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
Scaffolding project in ./<your-project-name>...
Done.
## 在项目被创建后,通过以下步骤安装依赖并启动开发服务器
> cd <your-project-name>
> npm install
> npm run dev
总结:
指向的 JavaScript。## vite构建vue项目
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app --template vue
# bun
bunx create-vite my-vue-app --template vue
查看 create-vite 以获取每个模板的更多细节:vanilla,vanilla-ts, vue, vue-ts,react,react-ts,react-swc,react-swc-ts,preact,preact-ts,lit,lit-ts,svelte,svelte-ts,solid,solid-ts,qwik,qwik-ts。
setup 是 Vue3 中的一个新配置项,值是一个函数,它是 Composition API “表演的舞台”,组件中所用到的:数据、方法、计算属性、监视……等,均配置在 setup 当中。
特点如下
setup 的返回值
//返回一个对象!!!
return {
name,
age,
tel,
changeName,
changeAge,
showTel,
};
//一个渲染函数
return () => "哈哈";
涉及到的面试问题
(data、methods……) 和 setup 能不能同时存在?
能,可以在一个 Vue 3 组件中同时使用 Options API 和 Composition API。但是,它们的使用场景是不同的。一般来说,如果你想要定义一些基础的响应式数据和钩子函数,使用 Options API 就足够了。而当你需要更复杂的逻辑或希望更好地组织你的代码时,Composition API 会是一个更好的选择。
setup 中定义的数据,在 data 中能否获得?
能,setup 这个钩子执行的时期比 data 早,当 data 中数据解析完毕时,setup 中数据早已经解析完毕了!!!但是注意的是 setup 不能再去读取 data 中的数据了
官网的 setup 语法糖
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的
语法,它具有更多优势:
要启用该语法,需要在 代码块上添加 setup attribute:
<script setup>console.log('hello script setup')</script>
里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 只在组件被首次引入的时候执行一次不同,
中的代码会在每次组件实例被创建的时候执行。
Vue3 项目注意点
Vue3 中可能会遇到两个 script 标签,一个是用来配置组件名称的,另一个是用来配置 setup 的。
<script lang="ts">
export default {
name:"Person123"
}
</script>
<script lang="ts" setup>
...
</script>
和上面这样写可能有点麻烦了!我们可以进行如下配置:
npm i vite-plugin-vue-setup-extend -D
vite.config.ts
中进行配置import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueSetupExtend from "vite-plugin-vue-setup-extend";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), VueSetupExtend()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
<script lang="ts" setup name="Person456">
...
</script>
ref ==============> 可以定义:基本类型、对象类型的响应式数据
reactive ===========> 只能定义:对象类型是响应式数据
ref 对比 reactive
宏观角度:
- ref 用来定义:基本数据类型、对象数据类型
- reactive 用来定义:对象数据类型
区别:
- ref 创建的变量必须使用 .value (可以使用 volar 插件自动添加 .value)
- reactive 重新分配一个新对象,会失去响应式 (可以使用 Object.assign 去整体替换)
使用原则:
- 若需要一个基本类型的响应式数据,必须使用 ref
- 若需要一个响应式对象,层级不深,ref、reactive 都可以
- 若需要一个响应式对象,且层级较深,推荐使用 reactive
reactive中一个注意点:(自动解包)
//当访问 obj.c 的时候,底层会自动读取value属性,因为c是在obj这个响应式对象里!!!
let obj=reactive({
a:1,
b:2,
c:ref(3)
})
toRefs 和 toRef 的作用
将一个响应式对象的每个属性,转换成为一个 ref 对象。两个基本相似,但是 toRefs 可以批量转换!
import {reactive,toRefs,toRef} from "vue"
//数据
let person=reactive({
name:"xiaoyu",
age:18
})
// let {name,age}=person;
// console.log(name,age);//产生出普通的值!
let {name,age}=toRefs(person);
console.log(name,age);
let name2=toRef(person,"name");
console.log(name2);
//像如下这种
ObjectRefImpl {_object: Proxy(Object), _key: 'name', _defaultValue: undefined, __v_isRef: true}__v_isRef: true_defaultValue: undefined_key: "name"_object: Proxy(Object) {name: 'xiaoyu', age: 18}dep: (...)value: (...)[[Prototype]]: Object
- ref 定义的数据
- reactive 定义的数据
- 函数返回一个值(getter 函数)
- 一个包含上述内容的数组
// 情况一:监视【ref】定义的 【基本类型】数据
let sum = ref(0);
const stopWatch = watch(sum, (newVal, oldVal) => {
console.log(newVal, oldVal);
//这个watch还返回一个函数,调用它可以解除监视!!!
if (newVal > 10) {
stopWatch();
}
});
console.log(stopWatch); //返回一个函数
/*
() => {
effect2.stop();
if (instance && instance.scope) {
remove(instance.scope.effects, effect2);
}
}
*/
监视【ref】定义的 【对象类型】数据:直接写数据名,监视的是对象的地址值,若想监视对象内部的属性变化,需要手动开启深度监视。
注意
- 若修改的是 ref 定义的对象中的属性,newVal 和 oldVal 都是新值,因为他们是同一个对象
- 若修改整个 ref 定义的对象,newVal 是新值,oldVal 是旧值,因为不是同一个对象了。
let person = ref({
name: "张三",
age: 18,
});
const changeName = () => {
person.value.name += "~";
};
const changeAge = () => {
person.value.age += 1;
};
const changePerson = () => {
person.value = { name: "李明", age: 20 };
};
//监视【ref】定义的 【对象类型】数据,监视的是对象的地址值,
// 若想监视对象内部的属性变化,需要手动开启深度监视。
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
let p = reactive({
name: "小雨",
age: 20,
});
const pName = () => {
p.name += "~";
};
const pAge = () => {
p.age += 1;
};
const pPerson = () => {
//这里不是真正意义上的修改整个人,而是进行了批量的修改!!!
Object.assign(p, { name: "小柔", age: 19 });
};
//监视【reactive】定义的 【对象类型】数据,默认开启了深度监视!!!
watch(p, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
注意点:
结论:监视的要是对象里面的属性,那么最好写函数式。注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视!
let p2 = reactive({
name: "xiaoyu",
age: 18,
car: {
c1: "奔驰",
c2: "宝马",
},
});
const p2Car = () => {
p2.car = { c1: "大奔驰", c2: "大宝马" };
};
//情况四:监视响应式对象中的某个属性,且该属性是基本数据类型,要写成函数式。
watch(
() => p2.name,
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
//下面这个情况就很诡异,能监视到对象里面的值变化,对象本身却不进行监视!
// watch(p2.car,(newVal,oldVal)=>{
// console.log(newVal,oldVal);
// })
//建议写成监视其中的属性任然是一个对象类型的,也推荐使用函数形式的!
watch(
() => p2.car,
(newVal, oldVal) => {
console.log(newVal, oldVal);
},
{ deep: true }
);
// 情况五:监视多个数据:
watch([() => p2.name, () => p2.car.c1], (newVal, oldVal) => {
console.log(newVal, oldVal);
});
- 都能监视响应式数据的变化,不同的是监听数据变化的方式不同
- watch:需要指明出监视的数据
- watchEffect:不用明确指出监听的数据(函数中用到了那些属性,那就是监视那些属性!)
作用:用于注册模板引用
- 用于普通 DOM 标签上,获取的是 DOM 节点
- 用于组件标签上,获取的是组件实例对象
这里有个说法:在获取组件实例实例身上的东西时,vue3 不允许直接获取,要求获取组件的本身主动暴露出来属性才行!!!如 defineExpose({a,b,c})
对于子组件 Person 中如下:
<template>
<div class="person">
<h1>中国</h1>
<h2 ref="title">北京</h2>
<button @click="showLog">点击输出h2这个元素</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,defineExpose} from "vue"
let title=ref()
let a=ref(0)
let b=ref(1)
let c=ref(2)
const showLog=()=>{
console.log(title.value);//获取的是DOM
}
defineExpose({a,b,c})
</script>
父组件中如下:
<script setup lang="ts" name="App">
import Person from './components/Person.vue';
import {ref} from "vue"
let title=ref()
let ren=ref()
const showLog=()=>{
console.log(title.value);//获取DOM
console.log(ren.value);//组价实例
}
</script>
<template>
<h2 ref="title">你好</h2>
<button @click="showLog">点我输出h2</button>
<Person ref="ren"/>
</template>
App.vue
<script setup lang="ts" name="App">
import Person from "./components/Person.vue";
import type { PersonInter, Persons } from "@/types/index";
import { reactive, ref } from "vue";
let person: PersonInter = { id: "sgbusjdbsj", name: "xiaoyu", age: 20 };
let personList = reactive<Persons>([
{ id: "sgbusjdbsjas", name: "xiaoyu", age: 18 },
{ id: "sgbusjdbsjsf", name: "xiaohh", age: 20 },
{ id: "sgbusjdbsjdf", name: "xiaogege", age: 10 },
]);
script>
<template>
<Person a="哈哈" b="嘿嘿" :list="personList" />
template>
Person.vue
<template>
<div class="person">
<h2>{{ list }}h2>
div>
template>
<script lang="ts" setup name="Person">
//其实这里的 宏函数 无需引入就能进行使用!!!
import { defineProps, withDefaults } from "vue";
import type { Persons } from "@/types/index";
//接收props,顺便使用!
// let x=defineProps(['a','b',"list"])
// console.log(x);
//接收list + 限制类型
// defineProps<{list:Persons}>()
//接收list + 类型限制 + 限制必要性 + 指定默认值
withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: "1", name: "xiao", age: 19 }],
});
script>
注意模块化的用法就行了!!!
// mouse.ts
import { ref, onMounted, onUnmounted } from "vue";
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0);
const y = ref(0);
// 组合式函数可以随时更改其状态。
function update(event: any) {
x.value = event.pageX;
y.value = event.pageY;
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
// 通过返回值暴露所管理的状态
return { x, y };
}
<template>
<div class="person">Mouse position is at: {{ x }}, {{ y }}div>
template>
<script lang="ts" setup name="Person">
import { useMouse } from "@/hooks/mouse";
const { x, y } = useMouse();
script>
<router-link >router-link>
<RouterLink>RouterLink>
- 路由组件通常存放在 pages 或者 views 文件夹中,一般组件通常存放在 components 文件夹。
- 通过点击导航,视觉效果上“消失了”的路由组件,默认是被销毁掉的,需要的时候再去挂载。
active-class 是 RouterLinkProps:链接在匹配当前路由时被应用到 class。
<RouterLink active-class="active" to="/home">主页RouterLink>
主页RouterLink>
history 模式
优点:URL 更加美观,不带有#,更接近于传统的网站 URL。
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 的错误。const router = createRouter({ history: createWebHistory(), });
hash 模式(后台管理系统喜欢用!!!)
优点:兼容性更好,因为不需要服务端处理路径。
缺点:URl 带有#不太美观,且在 SEO 优化方面相对较差。const router = createRouter({ history: createWebHashHistory(), });
传参
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">主页RouterLink>
主页RouterLink>
接收参数
import {useRoute} from "vue-router"
const route=useRoute()
//打印query参数
console.log(route.query)
路由占位:
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail/:id/:title/:content?',
//在占位后面加一个 ? 表示参数可传可不传
component:Detail
}
]
}
路由传参:
<RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">主页RouterLink>
<RouterLink :to="{name:"xiangqing",params:{id:news.id,title:news.title,content:news.content}}">主页RouterLink>
注意:
- 传递params参数时,若使用to的对象写法,必须使用name配置项,不能使用path。
- 传递params参数时,需要提前在规则中占位!
第一种写法:将路由收到的所有params参数作为props传递给组件。
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail/:id/:title/:content?',//在占位后面加一个 ? 表示参数可传可不传
component:Detail,
//第一种写法:将路由收到的所有params参数作为props传递给组件。
props:true
}
]
}
第二种写法:可以自己决定将什么作为props传给路由组件
{
name:'xinwen',
path:'/news',
component:Detail,
children:[
{
name:'xiangqing',
path:'detail',
component:Detail,
// 第二种写法:可以自己决定将什么作为props传给路由组件
props(route){
return route.query
}
//一种固定值的写法,用到的比较少!!!也是将参数传给组件身上。
props:{
a:'1',
b:'2'
}
}
]
}
<RouterLink replace >主页RouterLink>
编程式路由导航
import { useRouter } from "vue-router"
let router=useRouter()
router.push(
//这里和 router-link 中的 to 一个样!!!
)
重定向
//在路由规则中
{
path:'/',
redirect:"/home"
}
Pinia
符合直觉的
Vue.js 状态管理库
Pinia官网
安装
yarn add pinia
# 或者使用 npm
npm install pinia
在 main.ts中引入
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
先创建一个 Store(选项式)
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
在一个组件中使用该 store
<script setup>
import { useCounterStore } from '@/stores/counter'
//这下面三种方式用来更改状态!!!
const counter = useCounterStore()
//常规更改!
counter.count++
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
script>
<template>
<div>Current Count: {{ counter.count }}div>
template>
一个特殊的定义Store的方式(组合式)
为实现更多高级用法,你甚至可以使用一个函数 (与组件 setup() 类似) 来定义一个 Store
// stores/counter.js
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
我们得知道 Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字:
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})
这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。
defineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象。
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
在 Setup Store 中:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
script>
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
作为 action 的 increment 可以直接解构,getter 和 state 需要使用storeToRefs()
使用选项式API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
在 $reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。
const store = useStore()
store.$reset()
在 Setup Stores 中,您需要创建自己的 $reset() 方法:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
通过 store 的 $subscribe() 方法侦听 state 及其变化
const cartStore = useSomeStore()
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
},{ detached: true })
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离
可以使用watch监听整个 state
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
//
注意:子组件中不用编写任何东西,是不受到任何打扰的
【第二步】孙组件中使用inject
配置项接受数据。
我是孙组件
资产:{{ money }}
汽车:{{ car }}