以下是一篇基于 Vue 3 完整的技术教程,教你如何从 0 到 1 构建一个高性能的个人博客+在线投票平台,涵盖需求分析、技术选型、架构设计、前端实现、性能优化、部署运维等方面。
随着个人品牌建设和社区互动需求的增长,越来越多开发者希望拥有一套既能发布技术文章,又能与用户进行实时互动(如投票、点赞、评论)的高性能平台。本项目目标是:
模块 | 功能点 |
|---|---|
博客模块 | 发布/编辑/删除文章、Markdown 渲染、文章归档、分页、搜索 |
投票模块 | 创建投票、选项管理、多选/单选模式、截止时间控制、实时结果展示 |
通用模块 | 用户注册登录(JWT)、权限校验、评论与点赞、文件上传 |
性能运维 | SSR 或预渲染、静态资源 CDN、离线缓存、监控预警 |
┌────────────┐ ┌──────────────┐ ┌───────────┐
│ Browser │ ←→ │ Nginx (SSR) │ ←→ │ Node/Express │
└────────────┘ └──────────────┘ └───────────┘
│ │ │
▼ ▼ ▼
Frontend(Vue3) Static Assets REST API / GraphQLnpm init vite@latest vue-blog-vote -- --template vue-ts
cd vue-blog-vote
npm install在 vite.config.ts 中配置别名与 CDN 前缀:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: { '@': '/src' }
},
base: process.env.NODE_ENV === 'production' ? 'https://cdn.example.com/' : '/'
});使用 Vue Router 4,定义公共布局与私有布局:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import PublicLayout from '@/layouts/PublicLayout.vue';
import AdminLayout from '@/layouts/AdminLayout.vue';
const routes = [
{ path: '/', component: PublicLayout, children: [
{ path: '', name: 'Home', component: () => import('@/views/Home.vue') },
{ path: 'article/:id', name: 'Article', component: () => import('@/views/Article.vue') },
{ path: 'vote/:id', name: 'Vote', component: () => import('@/views/Vote.vue') },
]},
{ path: '/admin', component: AdminLayout, children: [
{ path: 'login', component: () => import('@/views/admin/Login.vue') },
{ path: 'dashboard', component: () => import('@/views/admin/Dashboard.vue') },
]}
];
export const router = createRouter({
history: createWebHistory(),
routes
});<!-- src/views/Home.vue -->
<template>
<n-card title="文章列表">
<n-list>
<n-list-item v-for="post in posts" :key="post.id">
<router-link :to="{ name: 'Article', params: { id: post.id } }">
{{ post.title }}
</router-link>
</n-list-item>
</n-list>
<n-pagination
:page="page"
:page-count="totalPages"
@update:page="fetchPosts"
/>
</n-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useApi } from '@/composables/useApi';
const page = ref(1), totalPages = ref(1), posts = ref([]);
const { get } = useApi();
async function fetchPosts() {
const res = await get('/api/posts', { page: page.value });
posts.value = res.data.items;
totalPages.value = res.data.totalPages;
}
onMounted(fetchPosts);
</script><!-- src/views/Vote.vue -->
<template>
<n-card :title="vote.title">
<n-radio-group v-model="selected" :options="vote.options" :disabled="expired" />
<n-button @click="submit" :disabled="expired">提交投票</n-button>
<div v-if="expired || hasVoted">
<n-progress v-for="opt in vote.options" :key="opt.value"
:label="`${opt.label} (${opt.count}票)`"
:percentage="(opt.count/total*100).toFixed(1)" />
</div>
</n-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useApi } from '@/composables/useApi';
interface Vote {
id: string;
title: string;
options: { label: string; value: string; count: number }[];
deadline: string;
}
const api = useApi();
const vote = ref<Vote>({ id:'', title:'', options:[], deadline:'' });
const selected = ref<string[]>([]);
const hasVoted = ref(false);
async function fetchVote() {
const res = await api.get(`/api/votes/${route.params.id}`);
vote.value = res.data;
}
async function submit() {
await api.post(`/api/votes/${vote.value.id}/submit`, { choices: selected.value });
hasVoted.value = true;
await fetchVote(); // 更新结果
}
onMounted(fetchVote);
</script>使用 Pinia + Axios 封装全局 API:
// src/composables/useApi.ts
import axios from 'axios';
export function useApi() {
const instance = axios.create({ baseURL: import.meta.env.VITE_API_BASE });
instance.interceptors.request.use(cfg => {
const token = localStorage.getItem('token');
if (token) cfg.headers.Authorization = `Bearer ${token}`;
return cfg;
});
return {
get: <T>(url: string, params?: any) => instance.get<T>(url, { params }),
post: <T>(url: string, data?: any) => instance.post<T>(url, data),
// … put, delete
};
}// 动态 import 已自动分包,Vite 默认在生产环境生成多个 chunk
component: () => import('@/views/About.vue');文章列表或长投票选项可用 vue-virtual-scroll-list 减少 DOM 数量。
借助 Vite PWA 插件 提供离线浏览与资源缓存。
以 Node.js + Express + MongoDB 为例,简单说明用户注册、文章列表、投票提交接口。
// routes/posts.js
router.get('/', async (req, res) => {
const { page=1 } = req.query;
const items = await Post.find()
.sort({ createdAt: -1 })
.skip((page-1)*10)
.limit(10);
const total = await Post.countDocuments();
res.json({ items, totalPages: Math.ceil(total/10) });
});本文从技术选型、架构设计、核心功能到性能优化、部署运维,全流程示范了如何基于 Vue 3 打造高性能的个人博客+在线投票平台。以为为扩展思考方向: