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
| { "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
| 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
| 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
|
const focus = { mounted(el, binding) { el.focus()
if (binding.value) { el.style.outline = '1px solid ' + binding.value } } }
app.directive('focus', focus)
|
1 2 3 4 5 6
| 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
| 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) } }
|
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) }) } }
|
渲染函数
渲染函数基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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
| 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
| 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
| import vue from '@vitejs/plugin-vue'
export default { plugins: [ vue({ jsx: true }) ] }
|
1 2 3 4 5 6 7 8 9
| 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
| 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
| 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
| 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
{ "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
| 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'
|
自定义标题锚点
标题