个人学习 Vue 的手册.
开始前, 应该先搞定 前端迅速上手 里的 vue项目创建 和 vscode配置,
1
2
3
4
5
6
7
8
| export interface Person {
id: number
name: string
age: number
x?: string
}
type Persons = Array<Person>
|
router.ts
文件内容
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
| import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'root',
// 重定向到 /home
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// 懒加载
component: () => import('../views/AboutView.vue')
}
]
})
export default router
|
子路径嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 子路径
{
path: '/home2',
name: 'home2',
component: HomeView,
children: [
{
name: 'p1'
path: 'p1',
component: Detail
}
]
},
|
路由参数. 在路径 url 里传递参数.
title?
说明 title 可以不传- routerlink 写法为
:to="{name: 'p1', params:{id: 1, title:2}}"
1
2
3
4
5
6
7
8
9
10
11
12
13
| {
path: '/home2',
name: 'home2',
component: HomeView,
children: [
{
name: 'p1'
// 传递参数
path: 'p1/:id/:title?',
component: Detail
}
]
},
|
props
是配合上面路由参数使用的. 原因是有时候只配置了路由, 无法使用 <Component a="1">
传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 通过query传参的函数写法
props(route){
return route.query
// 返回params不如用属性 props: true
// return route.params
}
// 属性写法. 只能用来替代函数写法的 return route.params
props: true
// 对象写法
props: {
id:1
title:2
}
// 接收方
// defineProps(['id','title'])
|
1
2
3
4
5
| <!-- 传字符串 -->
<RouterLink replace :to="url" active-class="active-link">{{ index }}-{{ url }}</RouterLink>
<!-- 传对象 -->
<RouterLink :to="{name: 'name', query:{a:1,b:2}}" active-class="active-link">{{ index }}-{{ url }}</RouterLink>
|
- 可以传递 url, 其实就是 path 路径
- 传递对象的话, 可以写 router 里面的 name, 用 query 传递 url 参数
replace
属性, 使用户无法使用浏览器的返回按钮去到上一个页面
css 内容
1
2
3
| .active-link {
background-color: skyblue;
}
|
1
2
3
4
| import { useRouter } from "vue-router"
const router = useRouter()
// 这里的push可以传递RouterLink里的 to 值
router.push('/url1')
|
定义一个 store
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
56
| import { defineStore } from 'pinia'
import axios from 'axios'
import { computed, ref } from 'vue'
export const useDogStore = defineStore('dog', {
state() {
return {
urls: [
'https://images.dog.ceo/breeds/pekinese/n02086079_10613.jpg',
'https://images.dog.ceo/breeds/cockapoo/bubbles1.jpg'
]
}
},
// 用于把state计算一下,字符串拼接一下来用
getters: {
doubleUrls(state) {
return state.urls.concat(state.urls)
},
doubleUrls2: (state) => state.urls.concat(state.urls),
doubleUrls3(): Array<string> {
return this.urls.concat(this.urls)
}
},
actions: {
async addDog() {
const {
data: { message }
} = await axios.get('https://dog.ceo/api/breeds/image/random')
this.urls.push(message)
}
}
})
// 组合式
// ref() 就是 state 属性
// computed() 就是 getters
// function() 就是 actions
export const useDog2Store = defineStore('dog', () => {
let urls = ref([
'https://images.dog.ceo/breeds/pekinese/n02086079_10613.jpg',
'https://images.dog.ceo/breeds/cockapoo/bubbles1.jpg'
])
const doubleUrls = computed(() => urls.value.concat(urls.value))
const doubleUrls2 = computed(() => urls.value.concat(urls.value))
const doubleUrls3 = computed(() => urls.value.concat(urls.value))
async function addDog2() {
const {
data: { message }
} = await axios.get('https://dog.ceo/api/breeds/image/random')
urls.value.push(message)
}
return { urls, doubleUrls, doubleUrls2, doubleUrls3, addDog2 }
})
|
读取
1
2
3
4
5
| const dogStore = useDogStore()
// store的解构用storeToRefs, storeToRefs只会处理数据,而不会处理store里的函数
const {urls} = storeToRefs(dogStore)
<img v-for="url in dogStore.urls" :key="url" :src="url" mode="scaleToFill" />
|
修改
1
2
3
4
5
6
7
8
9
10
| // 1 直接修改
dogStore.urls = ['1','2']
// 2 patch方式修改对象,一次提交多个修改
counterStore.$patch({
urls: ['1','2']
b: 4
})
// 3 就是在store定义里, 用actions修改
|
订阅
1
2
3
4
5
6
7
| // 订阅
// mutate里有id和事件.没什么用.
// state则可以拿到数据
counterStore.$subscribe((mutate, state) => {
// 一旦数据发生了变化,就存起来
localStorage.setItem('counter', JSON.stringify(state.count))
})
|
- 父传子
使用props
. - 子传父 1:
父传子一个函数,子接收并带上参数来调用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 父
<子组件 :dataA="dataA" @click="fangfa">
function fangfa(value:number){
consolo.log(value)
}
// 子
// 接收特定key,例如字符串 <子组件 a="a" b="b">
defineProps(['dataA','fangfa'])
fangfa(1)
// 接收父组件的Person对象
defineProps<{dataA:Person}>()
// 可以不传
defineProps<{dataA?:Person}>()
// 默认值
withDefaults(defineProps<{dataA?:Person}>(),{
dataA: ()=> [{id: 1,name: "ken",age: 1}]
})
|
只能子传父 2:
- 自定义一个函数名 haha, 传入一个函数
- 子组件接收特定名称, 然后 emit 传回去
1
2
3
4
5
6
7
8
9
10
| // 父
<子组件 @haha="fangfa">
function fangfa(value:number){
consolo.log(value)
}
// 子
<button @click="emit('haha',a)"></button>
var a = ref(1)
const emit = defineEmits(['haha'])
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import mitt from 'mitt'
const emitter = mitt()
// 监听
emitter.on('foo', e => console.log('foo', e) )
// 触发
emitter.emit('foo', { a: 'b' })
// 去掉所有事件
emitter.all.clear()
unMounted(()=>{
// 销毁的时候去掉这个
emitter.off('foo')
})
|
关于 $event
- 如果是原始 html 对象, 例如 input . 那么
$event
就是 dom 元素, 需要去 target.value
获取值 - 如果是自己定义的事情或对象, 那么
$event
就是你传递的对象
1
2
3
4
5
6
7
8
9
10
11
| // 父
// <A-Input v-model="aa"/> 其实是下面的简写
<A-Input :modelValue="username" @update:modelValue="username = $event"></A-Input>
let username = ref('ken')
// 子
<input type="text" :value="modelValue" @input="emit('update:modelValue',(<HTMLInput>)$event.target.value)">
// 接收传入的值
defineProps(['modelValue'])
// 接受传递的事件
const emit = defineEmits(['update:modelValue'])
|
父传子的时候, 如果子没有使用 defineProps
接收. 就会到子组件的 $attr
里.
常见于把用户的配置通过 attrs
传递到下层组件. 所以常见场景是: 二次封装组件
1
2
3
4
5
6
7
8
9
| // 父
// {c:1,d:2} 等于 :c="1" :d="2"
<child :a="a" :b="func" v-bind="{c:1,d:2}">
// 子 原封不动传递一下
<sun v-bind:"$attr">
// 孙 直接可以接收到
defineProps(["a","func"])
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 父 给子一个ref
<child ref="z">
// 拿到子
let z = ref()
// 子暴露的都可以拿到
z.value.a = 2
// 自己的数据,也暴露出去
let q = ref(1)
defineExpose({q})
// 子 暴露出去a
a = ref(1)
defineExpose({a})
// 通过$parent拿到父暴露的q
console.log($parent.q)
|
祖辈向下一代传递
1
2
3
4
5
6
7
8
9
10
| // 祖先
let qian = ref(1)
provide('money',qian)
// 复杂的对象
provide('money',{qian,func})
// 子
let q = inject('money',5) // 默认值5,顺便用来做类型推断
// 复杂的对象
let {qian,func} = inject('money',{q:5,f:()=>{}})
|
- 循环遍历数组
(person, index) in Persons
- 循环遍历对象
v-for="(value, key, index) in object"
computed
- 肯定有返回值
- 依赖项不变, 就可以用缓存. 性能好
- 不能异步.
- 场景: 购物车结算, 字符串拼接
1
2
3
| let price = ref(0)//$0
let p = computed<string>(()=>{ return `$` + price.value })
|
watch
- 返回值是停止自己
- 每次都重新执行整体
- 可以异步
- 场景: 搜索框匹配内容, 做一些计算内容的限制
1
2
3
4
5
6
7
| const stopWatch = watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
if (newValue >= 10) {
// 停止
stopWatch()
}
}, { immediate: false, deep: false })
|
假设一个页面, 有一个热门推荐窗口.
那么用 slot
就可以传递不同的元素.
1
2
3
4
5
6
7
8
9
10
| // 父
<h3>今日推荐</h3>
<child>
<img src="xxx">
</child>
// 子
<template>
<slot>默认没有内容</slot>
</template>
|
具名插槽
给插槽一个名字, 对应插入内容.
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
| <!-- ParentComponent.vue -->
<template>
<ChildComponent>
<template v-slot:header>
<h2>这是标题</h2>
</template>
<template v-slot:footer>
<p>这是页脚</p>
</template>
</ChildComponent>
</template>
<!-- ChildComponent.vue -->
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
|
作用域插槽
: 子组件有数据 q={a:1}
. 而父组件需要定义如何展示. 就会用到 q.a
. 就需要作用域插槽解决这个问题.
常见于 ui 组件库.
- 你在编写一个 ui 组件库的 table 组件, 有一个配套的搜索栏.
- 这时候父组件就需要传入搜索的字段, 配置这个搜索框的样式.
- 而可以使用哪些字段来搜索, 是 table 组件控制的.
- 所以 table 子组件把可以用的字段通过
slot
传递给父组件.
setup
创建阶段onBeforeMounted
, onMounted
挂载阶段onBeforeUpdated
, onUpdated
更新阶段onBeforeUpdated
, onUnmounted
卸载阶段
1
2
3
| onMounted(()=>{
console.log(1)
})
|
shallowref({a:1,b:{c:2}})
: 包裹对象的第一层 a
响应式. 下级 b
不监测. 性能更好readonly({a:1,b:2})
无法修改const rawObj = toRaw(reactiveObj)
拿到 reactive
包装前的原对象.const nonReactiveObj = markRaw(copiedObj)
相当于深拷贝一个新对象出来.teleport
让一个组件渲染到不同的 dom 位置 (例如相对于 body 元素, 而不是当前位置)suspense
自定义一个 loading 或加载失败时展示内容. 是占位符 + 加载状态展示customRef
是 Vue 3 中的一个函数,用于创建一个自定义的 ref 对象。通过 customRef
,你可以定义一个自定义的 getter 和 setter 函数来管理 ref 对象的值,并且可以手动控制依赖追踪和触发更新。这使得你可以更灵活地处理一些复杂的情况,例如惰性计算、异步更新等。
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
| import { customRef } from 'vue';
function customComputedRef(getter, setter) {
let value = getter();
return customRef((track, trigger) => {
return {
get() {
track(); // 手动追踪依赖
return value;
},
set(newValue) {
setter(newValue);
value = newValue;
trigger(); // 手动触发更新
}
};
});
}
const myCustomRef = customComputedRef(
() => {
console.log('getter executed');
return 1;
},
(v) => {
console.log('setter executed', v);
}
);
console.log(myCustomRef.value); // 输出: "getter executed", 1
myCustomRef.value = 2; // 输出: "setter executed", 2
console.log(myCustomRef.value); // 输出: "getter executed", 2
|
iconfont
是阿里的图标库. 这里介绍用法.
安装插件
1
| pnpm install vite-plugin-svg-icons -D
|
配置 vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
| import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default defineConfig({
plugins: [
// 关键内容
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]'
})
],
})
|
配置 main.ts
1
2
| // svg插件
import 'virtual:svg-icons-register'
|
创建文件夹 src/assets/icons
, 去 iconfont 下载 svg 到这个文件夹.
例如 src/assets/icons/dollor.svg
使用方法:
1
2
3
4
5
6
7
8
9
| <svg class="svg">
<use xlink:href="#icon-dollor" fill="red"></use>
</svg>
<style scoped>
.svg {
width: 2rem;
}
</style>
|
封装一下 src/components/SvgIcon/index.vue
组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <template>
<svg :style="{ width: size, height: size }">
<use :xlink:href="prefix + name" :fill="fill"></use>
</svg>
</template>
<script lang="ts" setup>
defineOptions({
name: "svg-component"
})
const prefix = "#icon-"
withDefaults(defineProps<{ name?: string, size?: string, fill?: string }>(), {
// 默认图标
name: () => "info-circle",
// 大小
size: () => "1rem",
// 填充颜色
fill: () => ""
})
</script>
|
使用组件
1
2
3
| <svg-icon name="dollor" size="2rem" fill="red"></svg-icon>
import SvgIcon from "@/components/SvgIcon/index.vue";
|
vite
使用 .env
文件区分使用环境变量, 暴露在 import.meta.env
对象里.
已有的变量:
import.meta.env.MODE
: {string} 应用运行的 模式。import.meta.env.BASE_URL
: {string} 部署应用时候的基础 url, 默认 /
import.meta.env.PROD
: {boolean} 是否生产环境. NODE_ENV='production'
决定这个值import.meta.env.DEV
: {boolean} 是不是开发环境import.meta.env.SSR
: {boolean} 应用是否运行在 SSR 模式
使用场景
1
2
3
4
5
6
7
8
9
10
| # "dev":"vite" 所以等同于直接执行 vite 命令
# 读取 .env.development
pnpm dev
# 等于并行运行 vue-tsc --build --force && vite build
# 读取 .env.prodution
pnpm build
# 使用 --mode 指定读取的文件 .env.staging
vite build --mode staging
|
process.env.NODE_ENV
和 vite build --mode env
不是一个东西.
但是建议在 .env
文件里加入加上这个变量. 使得统一
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 示例如下
# .env
VITE_APP_TITLE = '我是名字'
# .env.development
NODE_ENV = 'development'
VITE_KEN = 'development'
VITE_SERVER_URL = 'http://127.0.0.1:5000/'
# .env.production
NODE_ENV = 'production'
VITE_KEN = 'production'
VITE_SERVER_URL = 'http://127.0.0.1:5000/'
|
现在 可以这样指定名字
1
2
3
4
| defineOptions({
name: 'Foo',
// ... 更多自定义属性
})
|
以前写 vue 的时候组件名会是文件名. 无法配置组件名称, 可以使用 vite-plugin-vue-setup-extend
.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import vueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [
vue(),
vueSetupExtend() // 加入
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
|
于是我们就可以像 xml 一样写 name 的值
1
2
3
4
5
| <script setup lang="ts" name="自定义组件明">
defineProps<{
msg: string
}>()
</script>
|