my-vue-app/
├── public/ # 静态资源
│ └── favicon.ico
├── src/
│ ├── assets/ # 资源文件(图片、样式等)
│ ├── components/ # 通用组件
│ │ ├── common/ # 基础组件
│ │ └── business/ # 业务组件
│ ├── composables/ # 组合式函数
│ ├── directives/ # 自定义指令
│ ├── layouts/ # 布局组件
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia状态管理
│ ├── types/ # TypeScript类型定义
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── .env # 环境变量
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
<script setup lang="ts">
// 1. 导入
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MyComponent from '@/components/MyComponent.vue'
// 2. Props和Emits
interface Props {
title: string
}
const props = defineProps<Props>()
const emit = defineEmits<{ submit: [value: string] }>()
// 3. 响应式数据
const count = ref(0)
const state = reactive({ name: '' })
// 4. 计算属性
const double = computed(() => count.value * 2)
// 5. 方法
function handleClick() {
count.value++
}
// 6. 生命周期
onMounted(() => {
console.log('mounted')
})
</script>
<template>
<!-- 模板内容 -->
</template>
<style scoped>
/* 样式 */
</style>
const routes = [
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)
</script>
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.name]">
{{ item.name }}
</div>
</template>
// 使用vue-virtual-scroller
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
</template>
<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
const searchText = ref('')
const debouncedSearch = useDebounceFn(() => {
console.log('搜索:', searchText.value)
}, 500)
const throttledScroll = useThrottleFn(() => {
console.log('滚动')
}, 200)
</script>
// stores/modules/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const token = ref<string | null>(null)
// Getters
const isLoggedIn = computed(() => !!token.value)
// Actions
async function login(credentials: LoginCredentials) {
const response = await api.login(credentials)
user.value = response.user
token.value = response.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isLoggedIn, login, logout }
})
// utils/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 跳转到登录页
}
return Promise.reject(error)
}
)
export default request
// api/user.ts
import request from '@/utils/request'
import type { User, LoginCredentials } from '@/types'
export const userApi = {
login(credentials: LoginCredentials) {
return request.post<{ user: User; token: string }>('/login', credentials)
},
getProfile() {
return request.get<User>('/user/profile')
},
updateProfile(data: Partial<User>) {
return request.put<User>('/user/profile', data)
}
}
// composables/useErrorHandler.ts
import { ref } from 'vue'
export function useErrorHandler() {
const error = ref<Error | null>(null)
function handleError(e: unknown) {
if (e instanceof Error) {
error.value = e
console.error('错误:', e.message)
} else {
error.value = new Error('未知错误')
}
}
function clearError() {
error.value = null
}
return { error, handleError, clearError }
}
# .env.development
VITE_API_URL=http://localhost:3000/api
VITE_APP_TITLE=开发环境
# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=生产环境
// 使用环境变量
const apiUrl = import.meta.env.VITE_API_URL
const appTitle = import.meta.env.VITE_APP_TITLE
// MyComponent.spec.ts
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
describe('MyComponent', () => {
it('renders properly', () => {
const wrapper = mount(MyComponent, {
props: { title: 'Hello' }
})
expect(wrapper.text()).toContain('Hello')
})
it('increments count on click', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('button').trigger('click')
expect(wrapper.vm.count).toBe(1)
})
})
// stores/todo.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
completed: boolean
}
export const useTodoStore = defineStore('todo', () => {
const todos = ref<Todo[]>([])
const filter = ref<'all' | 'active' | 'completed'>('all')
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
const activeCount = computed(() => {
return todos.value.filter(t => !t.completed).length
})
function addTodo(text: string) {
todos.value.push({
id: Date.now(),
text,
completed: false
})
}
function removeTodo(id: number) {
const index = todos.value.findIndex(t => t.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function clearCompleted() {
todos.value = todos.value.filter(t => !t.completed)
}
return {
todos,
filter,
filteredTodos,
activeCount,
addTodo,
removeTodo,
toggleTodo,
clearCompleted
}
})
<!-- TodoApp.vue -->
<template>
<div class="todo-app">
<h1>待办事项</h1>
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加待办事项"
>
<div class="filters">
<button @click="store.filter = 'all'">全部</button>
<button @click="store.filter = 'active'">未完成</button>
<button @click="store.filter = 'completed'">已完成</button>
</div>
<ul class="todo-list">
<li v-for="todo in store.filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="store.toggleTodo(todo.id)"
>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="store.removeTodo(todo.id)">删除</button>
</li>
</ul>
<div class="footer">
<span>剩余 {{ store.activeCount }} 项</span>
<button @click="store.clearCompleted">清除已完成</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTodoStore } from '@/stores/todo'
const store = useTodoStore()
const newTodo = ref('')
function addTodo() {
if (newTodo.value.trim()) {
store.addTodo(newTodo.value)
newTodo.value = ''
}
}
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.completed {
text-decoration: line-through;
color: #999;
}
.filters button {
margin: 0 5px;
}
</style>
npm run build
npm run preview
# 安装Vercel CLI
npm i -g vercel
# 部署
vercel
# 安装Netlify CLI
npm i -g netlify-cli
# 部署
netlify deploy --prod
恭喜你完成了Vue3的学习!现在你已经掌握了: