Vue3生态与进阶

目录


TypeScript支持

TypeScript配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

组件类型定义

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
<script setup lang="ts">
import { ref, computed, PropType } from 'vue'

interface User {
id: number
name: string
email: string
}

const props = defineProps({
// 基本类型
title: String,
count: {
type: Number,
required: true,
default: 0
},

// 数组类型
items: {
type: Array as PropType<string[]>,
default: () => []
},

// 对象类型
user: {
type: Object as PropType<User>,
required: true
},

// 联合类型
status: {
type: String as PropType<'pending' | 'success' | 'error'>,
default: 'pending'
},

// 函数类型
onChange: {
type: Function as PropType<(value: string) => void>,
default: null
}
})

const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()

const localCount = ref(props.count)

const userName = computed(() => props.user?.name || 'Unknown')
</script>

路由类型定义

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
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页'
}
},
{
path: '/user/:id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue'),
props: true,
meta: {
requiresAuth: true
}
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router

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
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface UserState {
id: number
name: string
email: string
role: 'admin' | 'user' | 'guest'
}

export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserState | null>(null)
const token = ref<string>('')

const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => userInfo.value?.role === 'admin')

async function login(credentials: { username: string; password: string }) {
const response = await api.login(credentials)
token.value = response.token
userInfo.value = response.user
return response
}

function logout() {
token.value = ''
userInfo.value = null
}

return {
userInfo,
token,
isLoggedIn,
isAdmin,
login,
logout
}
})

自定义指令

全局指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// directives/focus.js
// Vue3
const focus = {
mounted(el, binding) {
el.focus()

if (binding.value) {
el.style.outline = '1px solid ' + binding.value
}
}
}

// 注册
app.directive('focus', focus)

// 使用
// <input v-focus="'red'" />
1
2
3
4
5
6
// Vue2全局指令
Vue.directive('focus', {
inserted(el) {
el.focus()
}
})

局部指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组件内指令
export default {
directives: {
focus: {
mounted(el) {
el.focus()
}
},
'background-color': {
mounted(el, binding) {
el.style.backgroundColor = binding.value
}
}
}
}

指令钩子

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
// Vue3指令钩子
const myDirective = {
// 绑定前调用
created(el, binding, vnode, preVnode) {
console.log('created')
},

// 元素绑定时调用
beforeMount(el, binding, vnode) {
console.log('beforeMount')
},

// 元素挂载后调用
mounted(el, binding, vnode) {
console.log('mounted')
},

// 组件更新前调用
beforeUpdate(el, binding, vnode) {
console.log('beforeUpdate')
},

// 组件更新后调用
updated(el, binding, vnode, preVnode) {
console.log('updated')
},

// 指令卸载前调用
beforeUnmount(el, binding) {
console.log('beforeUnmount')
},

// 指令卸载后调用
unmounted(el, binding) {
console.log('unmounted')
}
}

常用指令示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 点击外部指令
const clickOutside = {
mounted(el, binding) {
el._clickOutside = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickActive)
}
}

// 使用
// <div v-click-outside="handleClickOutside"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 防抖指令
const debounce = {
mounted(el, binding) {
const { value, arg } = binding
const delay = parseInt(arg) || 500

let timer = null
el.addEventListener('click', () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
value()
}, delay)
})
}
}

// 使用
// <button v-debounce:500="handleClick">防抖点击</button>

渲染函数

渲染函数基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Vue3 - h函数
import { h, ref } from 'vue'

export default {
setup() {
const count = ref(0)

// 返回渲染函数
return () => h('div', {
class: 'counter',
onClick: () => count.value++
}, [
h('span', `count: ${count.value}`)
])
}
}

组件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 渲染子组件
import { h, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

export default {
setup() {
const show = ref(true)

return () => h('div', [
show.value
? h(ChildComponent, {
msg: 'Hello',
onUpdate: (val) => console.log(val)
})
: null
])
}
}

条件渲染

1
2
3
4
5
6
7
8
9
10
// if/else条件
return () => {
if (count.value > 0) {
return h('div', 'count > 0')
} else if (count.value < 0) {
return h('div', 'count < 0')
} else {
return h('div', 'count = 0')
}
}

列表渲染

1
2
3
4
5
6
// 列表渲染
return () => h('ul', [
items.value.map(item =>
h('li', { key: item.id }, item.name)
)
])

slots和children

1
2
3
4
5
6
7
8
9
10
// 使用slots
export default {
setup(props, { slots }) {
return () => h('div', { class: 'wrapper' }, [
slots.header?.(),
h('main', slots.default?.()),
slots.footer?.()
])
}
}

JSX支持

1
2
3
4
5
6
7
8
9
10
// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
plugins: [
vue({
jsx: true
})
]
}
1
2
3
4
5
6
7
8
9
// Hello.jsx
export default function Hello({ name }) {
return (
<div class="hello">
<h1>Hello {name}</h1>
<p>Welcome to Vue 3!</p>
</div>
)
}

Vue3动画

Transition组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<button @click="show = !show">切换</button>

<transition name="fade">
<div v-if="show" class="box">内容</div>
</transition>
</div>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

Transition CSS类

1
2
3
4
5
6
7
v-enter-from      -> 元素进入前的状态
v-enter-active -> 元素进入中的状态
v-enter-to -> 元素进入后的状态

v-leave-from -> 元素离开前的状态
v-leave-active -> 元素离开中的状态
v-leave-to -> 元素离开后的状态

过渡模式

1
2
3
4
5
6
7
8
9
<!-- 过渡模式 - 新元素先进入再移除旧元素 -->
<transition name="fade" mode="out-in">
<component :is="currentView"></component>
</transition>

<!-- 过渡模式 - 元素同时过渡 -->
<transition name="slide" mode="in-out">
<component :is="currentView"></component>
</transition>

列表过渡

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>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</transition-group>
</template>

<style>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}

.list-move {
transition: transform 0.3s ease;
}
</style>

JavaScript钩子

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
<template>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<div v-if="show">内容</div>
</transition>
</template>

<script setup>
function beforeEnter(el) {
el.style.opacity = 0
el.style.transform = 'translateX(30px)'
}

function enter(el, done) {
el.offsetHeight // 触发重绘
el.style.transition = 'all 0.3s ease'
el.style.opacity = 1
el.style.transform = 'translateX(0)'
el.addEventListener('transitionend', done)
}

function afterEnter(el) {
el.style.opacity = ''
el.style.transform = ''
}

function leave(el, done) {
el.style.transition = 'all 0.3s ease'
el.style.opacity = 0
el.style.transform = 'translateX(-30px)'
el.addEventListener('transitionend', done)
}
</script>

Animate.css结合

1
2
3
4
5
6
7
8
9
<template>
<transition
name="custom-classes-transition"
enter-active-class="animate__animated animate__bounce"
leave-active-class="animate__animated animate__hinge"
>
<div v-if="show">内容</div>
</transition>
</template>

SSR与Nuxt

Nuxt3简介

Nuxt是一个基于Vue.js的全栈框架,提供了SSR、SSG、ISR等渲染模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建Nuxt3项目
npx nuxi init my-nuxt-app

# 安装依赖
cd my-nuxt-app
npm install

# 启动开发服务器
npm run dev

# 构建生产版本
npm run build

# 预渲染静态站点
npm run generate

Nuxt3目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my-nuxt-app/
├── .nuxt/ # Nuxt生成的文件
├── app.vue # 应用入口
├── nuxt.config.ts # 配置文件
├── pages/ # 页面目录
│ ├── index.vue # 首页
│ └── user/
│ └── [id].vue # 动态路由
├── components/ # 组件目录
├── composables/ # 组合式函数
├── layouts/ # 布局
├── middleware/ # 中间件
├── plugins/ # 插件
├── public/ # 静态资源
├── server/ # 服务端API
└── types/ # 类型定义

Nuxt3页面

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
<!-- pages/index.vue -->
<template>
<div>
<h1>首页</h1>
<NuxtLink to="/about">关于页面</NuxtLink>

<!-- 异步数据 -->
<div v-if="pending">加载中...</div>
<div v-else>
<div v-for="post in posts" :key="post.id">
{{ post.title }}
</div>
</div>
</div>
</template>

<script setup>
// 自动导入
const { data: posts, pending, error } = await useFetch('/api/posts')

// SEO
useHead({
title: '首页',
meta: [
{ name: 'description', content: '首页描述' }
]
})
</script>

Nuxt3服务端API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10

return {
posts: [
{ id: 1, title: '文章1' },
{ id: 2, title: '文章2' }
],
total: 100,
page,
limit
}
})

Nuxt3状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
// composables/useCounter.ts
export const useCounter = () => {
const count = useState('count', () => 0)

const increment = () => {
count.value++
}

return {
count: readonly(count),
increment
}
}

SSG与VitePress

VitePress简介

VitePress是VuePress的继任者,使用Vite构建,提供了极快的开发体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建项目
npm install -D vitepress

# 创建文档
mkdir docs
echo '# Hello VitePress' > docs/index.md

# package.json添加脚本
{
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}

VitePress配置

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
// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
title: 'Vue文档',
description: 'Vue学习文档',

themeConfig: {
nav: [
{ text: '指南', link: '/guide/' },
{ text: 'API', link: '/api/' },
{ text: 'GitHub', link: 'https://github.com/vuejs/core' }
],

sidebar: [
{
text: '入门',
items: [
{ text: '安装', link: '/guide/install' },
{ text: '快速开始', link: '/guide/quickstart' }
]
}
],

socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/core' }
]
}
})

Markdown扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 自定义容器
::: tip
这是一个提示
:::

::: warning
这是一个警告
:::

::: danger
这是一个危险警告
:::

# 代码块高亮
```typescript
const hello = 'world'

自定义标题锚点

标题