第八章:实战项目案例
8.1 项目一:Todo 应用(Vue 3 + Pinia)
项目初始化
bash
npm create vite@latest todo-app -- --template vue
cd todo-app
npm install pinia
npm install -D @vitejs/plugin-vue
项目结构
todo-app/
├── src/
│ ├── components/
│ │ ├── TodoList.vue
│ │ ├── TodoItem.vue
│ │ └── TodoForm.vue
│ ├── stores/
│ │ └── todo.js
│ ├── App.vue
│ └── main.js
└── vite.config.js
Store 实现
javascript
// src/stores/todo.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTodoStore = defineStore('todo', () => {
const todos = ref([
{ id: 1, text: '学习 Vite', completed: false },
{ id: 2, text: '学习 Pinia', completed: false }
])
const addTodo = (text) => {
todos.value.push({
id: Date.now(),
text,
completed: false
})
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
const deleteTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
const completedCount = computed(() =>
todos.value.filter(t => t.completed).length
)
const pendingCount = computed(() =>
todos.value.length - completedCount.value
)
return { todos, addTodo, toggleTodo, deleteTodo, completedCount, pendingCount }
})
组件实现
vue
<!-- src/components/TodoList.vue -->
<template>
<div class="todo-list">
<div class="stats">
<span>总计: {{ todos.length }}</span>
<span>已完成: {{ completedCount }}</span>
<span>未完成: {{ pendingCount }}</span>
</div>
<div v-for="todo in todos" :key="todo.id" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</div>
</div>
</template>
<script setup>
import { useTodoStore } from '../stores/todo'
import { storeToRefs } from 'pinia'
const todoStore = useTodoStore()
const { todos, completedCount, pendingCount } = storeToRefs(todoStore)
const { toggleTodo, deleteTodo } = todoStore
</script>
<style scoped>
.todo-list {
max-width: 500px;
margin: 0 auto;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 8px;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.completed {
text-decoration: line-through;
color: #999;
}
button {
margin-left: auto;
padding: 4px 12px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
8.2 项目二:博客系统(React + React Router)
项目初始化
bash
npm create vite@latest blog -- --template react
cd blog
npm install react-router-dom
npm install -D @vitejs/plugin-react
路由配置
javascript
// src/router.jsx
import { createBrowserRouter } from 'react-router-dom'
import Layout from './Layout'
import Home from './pages/Home'
import Post from './pages/Post'
import About from './pages/About'
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'post/:id', element: <Post /> },
{ path: 'about', element: <About /> }
]
}
])
使用 Vite 的 import.meta.glob
javascript
// src/pages/Home.jsx
import { useState, useEffect } from 'react'
// 动态导入所有 Markdown 文件
const modules = import.meta.glob('../posts/*.md', {
as: 'raw',
eager: true
})
function Home() {
const [posts, setPosts] = useState([])
useEffect(() => {
const postsData = Object.entries(modules).map(([path, content]) => ({
id: path.split('/').pop().replace('.md', ''),
title: extractTitle(content),
excerpt: content.slice(0, 200) + '...'
}))
setPosts(postsData)
}, [])
function extractTitle(content) {
const match = content.match(/^# (.+)$/m)
return match ? match[1] : '无标题'
}
return (
<div>
<h1>我的博客</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<a href={`/post/${post.id}`}>阅读更多</a>
</article>
))}
</div>
)
}
export default Home
8.3 项目三:组件库开发
项目结构
my-ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ ├── Button.vue
│ │ │ └── style.css
│ │ └── Input/
│ │ ├── index.js
│ │ └── Input.vue
│ └── index.js
├── vite.config.js
└── package.json
Vite 配置(库模式)
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.js'),
name: 'MyUI',
formats: ['es', 'umd'],
fileName: (format) => `my-ui.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
},
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'my-ui.css'
return assetInfo.name
}
}
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/styles/variables.scss";`
}
}
}
})
组件实现
vue
<!-- src/components/Button/Button.vue -->
<template>
<button
:class="['btn', `btn-${type}`]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup>
defineProps({
type: {
type: String,
default: 'primary',
validator: (v) => ['primary', 'success', 'danger'].includes(v)
},
disabled: Boolean
})
defineEmits(['click'])
</script>
<style scoped>
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #42b983;
color: white;
}
.btn-success {
background: #67c23a;
color: white;
}
.btn-danger {
background: #f56c6c;
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
8.4 项目四:PWA 应用
配置 PWA
bash
npm install -D vite-plugin-pwa
javascript
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
manifest: {
name: 'My PWA App',
short_name: 'PWA',
description: 'A Vite PWA App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365
}
}
}
]
}
})
]
})
更新通知
javascript
// src/main.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload()
})
}
8.5 项目五:Chrome 扩展
项目结构
chrome-extension/
├── public/
│ ├── manifest.json
│ └── icon.png
├── src/
│ ├── popup/
│ │ ├── popup.html
│ │ └── popup.js
│ ├── background/
│ │ └── background.js
│ └── content/
│ └── content.js
└── vite.config.js
Vite 配置
javascript
// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
outDir: 'dist',
rollupOptions: {
input: {
popup: resolve(__dirname, 'src/popup/popup.html'),
background: resolve(__dirname, 'src/background/background.js'),
content: resolve(__dirname, 'src/content/content.js')
},
output: {
entryFileNames: '[name].js',
assetFileNames: '[name].[ext]'
}
}
}
})
Manifest
json
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"permissions": ["storage", "activeTab"]
}
本章小结
通过五个实战项目,覆盖了:
- ✅ Vue 3 + Pinia 状态管理
- ✅ React + Router 路由系统
- ✅ 组件库开发与发布
- ✅ PWA 离线应用
- ✅ Chrome 扩展开发
每个项目都是真实可运行的,可以作为学习和参考的基础。