个人博客网站系统这个方向,看起来像是一道到处都有的课程设计题,但真正想把它做成“自己愿意天天打开”的产品,而不是一个能跑就行的小 Demo,中间涉及的东西其实远超预期。用 Node.js 提供接口,用 Vue 搭页面,再用 ElementUI 解决后台管理界面的大量重复劳动,这套组合之所以常被拿来当入门全栈项目,是因为每一层都有足够的挑战,但又没有难到让人一上来就放弃。
我最初做这个博客项目时,脑子里只想着“写文章、看文章”两件事,结果写着写着一堆问题排队上门:Node.js 装哪个版本、npm 命令在 PowerShell 里为什么被禁止运行、Vue 路由为什么一刷新就 404、ElementUI 分页组件翻页之后数据怎么接、文章列表和标签之间到底谁先加载……这篇文章我会按自己的实操顺序,把这套个人博客网站系统的设计和排坑过程完整过一遍。适合想把前后端打通、想弄明白各层配合关系的人参考,也适合那些刚装好 Node.js 和 Vue 工具链、还在第一步挣扎的新手。
1. 博客网站从需求拆解开始:别把文章、后台和登录揉成一团
网上很多个人博客代码把文章路由、管理员页面、富文本编辑组件塞在同一个 Vue 视图里,页面一复杂,数据流立刻变得很乱。我把需求拆成两个角色去看:访客只关心能不能顺滑地阅读内容,作者则关心能不能快速写、改、删文章。这两类人的操作路径完全不一样,页面结构也应该分开。
1.1 访客视角和作者视角是两套完全不同的页面
访客端我会保留这些基础入口:首页的文章流、文章详情页、分类页、标签归档页、内容页,外加一个极简“关于我”。作者端则需要先登录,然后进入 Dashboard,能看到文章列表、新建/编辑文章、分类管理、标签管理,如果还接了留言功能,再补一个评论管理页。
先拿一张表格把这套博客的核心页面和职责理出来:
| 模块 | 页面 | 核心职责 | 是否包含在最小版本 |
|---|---|---|---|
| 访客端 | 首页/文章列表 | 展示标题、摘要、封面图与发布时间 | 是 |
| 访客端 | 文章详情 | 正文渲染、上一篇/下一篇与目录 | 是 |
| 访客端 | 分类/标签 | 按维度聚合内容,方便读者继续找文章 | 视内容量而定 |
| 后台 | 登录页 | 校验作者身份,签发访问凭证 | 是 |
| 后台 | Dashboard/文章管理 | 表格化展示文章,支持分页搜索和删除 | 是 |
| 后台 | 编辑页 | Markdown 或富文本写作,设置分类和标签 | 是 |
我建议把“文章列表页”作为第一个完成的页面,它会牵涉到分页、类型字段、时间格式化、接口异常处理这些基础问题。先把这条路打通,后面加分类、加标签、加用户登录都会顺手很多。
也有人问我,为什么不直接用现成的 WordPress 或者静态博客生成器?我的看法是,如果目标是“尽快拥有一个个人博客”,直接选成熟工具确实更省心,但如果你想理解“浏览器发起请求后,后端如何把数据库里的文章返回给页面”,用 Node.js 自己写接口是性价比最高的方式。Vue 和 ElementUI 的价值则在于,当博客内容从“三五篇文章”涨到“几百篇”以后,后台的操作效率明显比一条条手写 HTML 高出一截。
1.2 Node.js + Vue + ElementUI 的组合为什么适合博客站
Node.js 负责给前端提供数据接口,Vue 负责把数据和组件渲染成可交互的界面,ElementUI 则提供现成的布局、表格、表单、弹窗和消息提示。对个人博客这种访问量不大、但管理功能五脏俱全的项目,这个组合比 React 加一堆重型中间件更直接。
Vue 在国内生态里有一大优势:周边资料非常多。无论是 vue-router 的路由跳转,还是 ElementUI 组件的配置方式,遇到问题都能快速找到对应场景,这对单独做项目的人来说很重要。另一个优势是 Vue 的模板语法上手曲线平滑,新手能先把视图写出来,再慢慢理解响应式原理,不必一上来就啃过重的状态管理工具。
我最终采用的结构是纯前后端分离:前端 Vue 单独维护,调用 /api 开头的接口;后端 Node.js 提供接口,并定时备份数据库。部署到服务器后,再把前端构建出的 dist 目录交给 Node.js 进程托管,这样既方便本地联调,又不会让开发环境依赖关系混乱。你在照着自己做时,也可以固定采用这层边界:前端不直接连接数据库,后端只返回 JSON,不掺和页面渲染。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Node.js 环境配置里最影响心情的几件小事
个人博客项目真正开始前,首先要跨过 Node.js 安装和环境配置这一关。很多报错不是因为代码写得不对,而是因为开发机上的 Node.js 版本、npm 脚本执行策略、终端类型三者不匹配导致的。
2.1 先装一个能切换版本的 Node.js,再谈项目
很多人直接从 Node.js 官网下载最新版,闷头安装后才发现某些依赖不支持最新版本,或者老项目用的还是旧版 API。建议先安装一个版本管理工具,比如 Windows 下常用的 nvm-windows,或者全平台通用的 fnm。这样你可以给不同项目维护不同的 Node.js 版本。
以 nvm-windows 为例,大致流程是:
- 去 nvm-windows 的 GitHub 仓库下载安装包,例如
nvm-setup.exe。 - 安装时注意,Node.js 的安装路径最好选择无中文无空格的目录,例如
C:\dev\nvm,避免以后出现一些“玄学”问题。 - 安装完成后重新打开终端,执行
nvm -v确认版本管理工具可用。 - 查看远程版本:
nvm list available。 - 安装一个 LTS 版本,例如
nvm install 20.11.1,然后nvm use 20.11.1。
选择 LTS 版本而不是 Current 版本的好处是稳定。个人博客不是前沿技术试验场,不需要依赖最新特性,反而更在意 npm install 时会不会突然遇到某个包与新版运行时不兼容。项目根目录最好再加一个 .nvmrc 文件,里面写清楚当前项目使用的 Node 版本号,以后换电脑或者交给别人接手时能快速复现环境。
2.2 npm.ps1 报“禁止运行脚本”:不是 Node.js 坏了
如果你安装完 Node.js 后,在终端执行 npm -v 或 npm install,屏幕上却弹出这样一段错误:
code复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,
因为在此系统上禁止运行脚本。
这还是很多人的第一道坑。这段报错并不是说 Node.js 没装好,而是 PowerShell 的执行策略默认禁止运行 .ps1 脚本。npm 在 Windows 下的启动器恰好就是 npm.ps1,你的命令本身没有错,错的是执行策略拦下了它。
解决办法是在 PowerShell 里放开当前用户的执行策略。以管理员身份打开 PowerShell,然后执行:
powershell复制Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
它会询问你是否要更改执行策略,输入 Y 回车即可。之后重新打开一个终端,npm -v 就能正常打印版本号了。
RemoteSigned 并不是完全放开限制,它只允许运行本机创建的脚本和带有可信签名的远程脚本,比 Unrestricted 安全得多。如果你只是想临时跑一条命令,不愿意改系统配置,也可以直接用 Command Prompt 或 Git Bash 来执行 npm 命令,它们不受 .ps1 执行策略影响。
2.3 安装成功之后,还要检查这两条命令
装完 Node.js 后,不要急着建项目。先打开终端,依次执行:
bash复制node -v
npm -v
如果你看到版本号,比如 v20.11.1 和 10.2.4,说明运行环境已经可用。接着可以顺手看一眼 npm 的默认镜像源,因为在个人博客搭建过程中,依赖安装速度直接影响心情。可以执行:
bash复制npm config get registry
如果返回的不是国内镜像源,后续某个大依赖包下载很可能会卡住。你可以通过项目级配置或者全局配置做调整,但需要注意镜像源本身的可信度,尽量选择公开、广泛使用的镜像服务。依赖源稳定后,再创建项目目录并执行初始化:
bash复制mkdir blog-project
cd blog-project
npm init -y
到这里,Node.js 安装及环境配置这部分才算真正结束。很多教程会直接把这一步骤略过,但实际项目里我遇到过太多“代码一模一样,环境不同跑不起来”的情况。操作系统位数、安装路径是否含空格、终端类型、是否开了代理、执行策略限制,每一个变量都可能制造不同报错。
3. Vue 前台页面 + ElementUI 后台管理:路由和组件的落地写法
环境就绪后,下一步是选择 Vue 版本。需要先说明一件事:ElementUI 这个组件库主要是给 Vue 2 时代准备的,Vue 3 对应的生态已经变成 Element Plus。很多课程或老项目里仍然会写“Vue + ElementUI”,这时候通常意味着项目使用的是 Vue 2.7 和 ElementUI 2.15.x。如果你从零开始并且没有历史包袱,更建议直接用 Vue 3 + Element Plus,组件名和用法大部分相近;但如果你是为了匹配别人现成的项目,那还要以目标项目的技术栈为准。
下面说的内容,我会按 Vue 2.7 + ElementUI 2.x 这套老组合来讲,因为它的历史教材最多,并且很多人拿到的项目就是这个组合。核心写法放到 Element Plus 里同样能迁移,差别主要在插槽名称和事件名细节上。
3.1 目录结构与路由拆分
一个适合博客系统的前端目录会先把“访客端”和“后台管理”拆到不同视图文件夹下:
text复制src
├── api
│ ├── article.js
│ ├── auth.js
│ └── request.js
├── router
│ └── index.js
├── views
│ ├── front
│ │ ├── Home.vue
│ │ ├── ArticleDetail.vue
│ │ └── Archive.vue
│ └── admin
│ ├── Login.vue
│ ├── Layout.vue
│ ├── ArticleManage.vue
│ └── ArticleEdit.vue
└── App.vue
路由设计上,我会给所有后台页面加一个统一的前缀 /admin,并在路由配置里给这些页面套上一层 Layout.vue。Layout.vue 内部使用 ElementUI 的 el-container 搭建侧边菜单与顶部导航,文章管理、分类管理、标签管理都作为子路由渲染到主要内容区。
路由配置大概长这样:
javascript复制// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
component: () => import('@/views/front/Home.vue')
},
{
path: '/article/:id',
component: () => import('@/views/front/ArticleDetail.vue')
},
{
path: '/admin/login',
component: () => import('@/views/admin/Login.vue')
},
{
path: '/admin',
component: () => import('@/views/admin/Layout.vue'),
redirect: '/admin/articles',
meta: { requiresAuth: true },
children: [
{
path: 'articles',
component: () => import('@/views/admin/ArticleManage.vue')
},
{
path: 'article/new',
component: () => import('@/views/admin/ArticleEdit.vue')
},
{
path: 'article/:id/edit',
component: () => import('@/views/admin/ArticleEdit.vue')
}
]
}
]
路由使用 meta.requiresAuth 标记之后,再配合 beforeEach 导航守卫检查登录状态。未登录的用户访问后台时,直接跳转到 /admin/login。这里我建议不要把“是否登录”这个状态写得过于复杂,用 Vuex 或简单模块化 store 存一个 token,再在请求拦截器里带上 Authorization 请求头就够了,博客项目早期不需要引入太庞大的权限模型。
3.2 分页组件和文章列表的联动
ElementUI 后台管理页中,出现频率最高的组件一定是表格加分页。文章数量少时你可能觉得分页是多余的,但一旦开始按月归档,后台一次性渲染几十条文章记录,表格卡顿不说,查找文章也很费劲。
先看我在文章管理页里常用的一组结构:
html复制<el-table :data="articleList" v-loading="loading">
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="categoryName" label="分类" width="120" />
<el-table-column prop="publishTime" label="发布时间" width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.publishTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="goEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:current-page="query.page"
:page-size="query.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
分页组件通常暴露 current-change 和 size-change 两个事件。我习惯把分页参数统一放在一个 query 对象里:
javascript复制data() {
return {
query: { page: 1, pageSize: 10 }
}
},
methods: {
async fetchList() {
this.loading = true
try {
const res = await this.$api.getArticleList(this.query)
this.articleList = res.data.list
this.total = res.data.total
} finally {
this.loading = false
}
},
handlePageChange(value) {
this.query.page = value
this.fetchList()
},
handleSizeChange(value) {
this.query.pageSize = value
this.query.page = 1
this.fetchList()
}
}
不要把页码变化事件里改成直接调用一次 fetchList,然后又在 fetchList 里操作 DOM 去重置分页状态,那会让状态源不统一。分页组件的 current-page 只应该绑定 query.page,所有改动最终都回到事件方法里同步更新数据。
3.3 后台标签筛选、时间线归档这类功能的 ElementUI 用法
个人博客还有一个高频需求,就是写文章时需要给文章选择分类和标签。我最早用的方案是复选框列表,但标签一多,页面就特别占空间。后来换成了 ElementUI 的下拉多选组件,后台编辑页清爽很多。基础写法如下:
html复制<el-select
v-model="article.tags"
multiple
collapse-tags
placeholder="选择标签"
clearable
>
<el-option
v-for="tag in tagList"
:key="tag.id"
:label="tag.name"
:value="tag.id"
/>
</el-select>
选择后你得到的是标签 ID 数组。我建议提交给后端时,直接以 tagIds: [1, 2, 3] 的形式传,后端在关联表里做批量写入。如果后台做文章筛选时也需要“下拉多选全选”,可以在选项里额外加一个“全选”的 option,选中后手动把全部标签的 ID 填入 v-model。不过要注意,这种“全选”并不是组件原生提供的语义,需要在前端代码里主动判断。
访客端的归档页面也可以用 ElementUI 的时间线组件来做,按月份展示文章标题,比纯表格亲切很多。ElementUI 时间线的基础用法是:
html复制<el-timeline>
<el-timeline-item
v-for="item in archivedPosts"
:key="item.id"
placement="top"
>
<template slot="timestamp">
{{ item.publishTime }}
</template>
<el-card>
<h4>{{ item.title }}</h4>
<p>{{ item.summary }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
如果直接给 el-timeline-item 传 timestamp 属性,时间文案会按默认格式显示。要做自定义样式,比如把时间格式化成年月日、加图标、加链接,就可以在内部使用 timestamp 插槽,上面这段就是 Vue 2 里具名插槽的写法。换到 Vue 3 / Element Plus 后,插槽写法改成 #timestamp 即可。
3.4 作为开发者你还要留意组件库里的“陷阱”
ElementUI 给人省时间,但它不是银弹。表格组件的 prop 字段名必须与接口返回字段完全一致,如果后端返回的是 publish_time,而你的代码里绑定了 publishTime,表格这一列就会空白。为了省掉这种低级问题,个人项目里我会让后端接口直接返回驼峰风格的字段,或者在 Axios 响应拦截器里做统一的字段格式转换,而不是在前端每个页面里临时处理。
还有一点是,后台编辑文章的富文本编辑器最好和 Markdown 预览分成两个步骤。文章编辑页涉及标题、分类、标签、封面图、正文、发布状态等多个字段,我建议把“保存草稿”和“发布”做成两个按钮,发布状态用布尔值或状态字段控制,不要让草稿直接暴露在访客端首页。
4. Node.js 后端接口设计:把文章接口写得像一份产品文档
后端部分,我选择了 Express 作为 Web 框架。它足够轻量,适合个人博客这种路由和中间件都不算复杂的项目。数据库我用 MySQL,因为文章、分类、标签、评论这些数据天然有外键关系,如果用文档型数据库,反而需要手动维护不少一致性逻辑。
4.1 先约定接口返回结构,前端才不会四处救火
前后端分离后,最大的矛盾往往不是“接口不行”,而是“接口结构没约定”。我建议从第一个接口开始就统一返回格式,比如:
json复制{
"code": 0,
"message": "ok",
"data": {}
}
其中 code 为 0 表示成功,非 0 表示业务失败。前端请求封装里判断 code,如果是 0 就返回 data,否则直接弹出 message。这样接口无论成功失败,前端都能以同一套逻辑处理,不至于每个页面都写一份 if (res.code === 200)。
4.2 登录注册、文章列表和详情接口的最小实现
登录接口通常会做成这样:
javascript复制router.post('/auth/login', async (req, res) => {
const { username, password } = req.body
const user = await db.queryOne('SELECT * FROM user WHERE username = ?', [username])
if (!user) {
return res.json({ code: 1, message: '用户不存在' })
}
const valid = bcrypt.compareSync(password, user.password_hash)
if (!valid) {
return res.json({ code: 1, message: '密码错误' })
}
const token = jwt.sign(
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
)
res.json({ code: 0, data: { token, username: user.username } })
})
密码绝不能以明文形式存储,入库前要用 bcryptjs 这类算法做哈希。不推荐自己设计一套字符串拼接或简单加密,因为密码安全不适合在个人项目里 DIY。
文章列表接口会稍微复杂一点。它需要支持分页、关键字搜索、按分类筛选、按标签筛选,以及只查询已发布状态。如果把全部逻辑都堆在一条 SQL 里,后期几乎无法维护。我会把条件拼接的部分抽象成 buildArticleWhere 这样的函数,只负责拼 SQL 和查询参数。核心代码如下:
javascript复制router.get('/articles', async (req, res) => {
const page = parseInt(req.query.page) || 1
const pageSize = parseInt(req.query.pageSize) || 10
const offset = (page - 1) * pageSize
const where = ['a.status = 1']
const params = []
if (req.query.categoryId) {
where.push('a.category_id = ?')
params.push(req.query.categoryId)
}
if (req.query.keyword) {
where.push('(a.title LIKE ? OR a.summary LIKE ?)')
params.push(`%${req.query.keyword}%`, `%${req.query.keyword}%`)
}
const whereSql = where.join(' AND ')
const list = await db.query(
`SELECT a.id, a.title, a.summary, a.cover, a.publish_time,
c.name AS category_name
FROM article a
LEFT JOIN category c ON a.category_id = c.id
WHERE ${whereSql}
ORDER BY a.publish_time DESC
LIMIT ?, ?`,
[...params, offset, pageSize]
)
const totalRows = await db.query(
`SELECT COUNT(*) AS total FROM article a WHERE ${whereSql}`,
params
)
res.json({
code: 0,
data: {
list,
total: totalRows[0].total
}
})
})
分页参数的默认值必须做防御处理,因为前端可能第一次不传任何分页参数。直接用 req.query.page 拿到的可能是字符串 "1",所以需要用 parseInt 转换,还要避免 NaN 进入 SQL。列表页返回的 total 是总条数,前端据此计算总页数并渲染分页器,不能靠前端猜。
4.3 联调阶段最该注意的三个小问题
联调通常发生在本地,有时候 Vue 开发服务器跑在 8080,Node.js 接口跑在 3000,浏览器直接请求 3000 端口会触发跨域。最简单的方案是在 Vue 的 vue.config.js 里配代理,而不是在后端强行开 CORS,比如:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}
第二个常见问题是上传图片。博客文章难免有封面图,如果你把图片以二进制存入 MySQL,数据库体积会迅速膨胀,而且读写性能很差。我建议把图片保存在服务器的某个上传目录或云存储桶里,数据库只保存最终的访问 URL,接口返回文章列表时直接返回完整 URL 或路径,前端拼接后即可展示。
第三个问题是统一异常处理。很多人写接口时只顾着业务正常路径,查询失败后整个 Node 进程可能直接抛异常。Express 需要有一个全局错误处理中间件。给所有异步路由包一层 try-catch 很繁琐,可以写一个小的 asyncHandler 包装函数,或者至少做到:所有登录校验、文件上传这类可能出错的操作都返回统一错误结构,而不是让页面看到一行未知的堆栈。
5. 从开发机搬到服务器:前端构建文件与 Node.js 服务的配合
开发完成只是第一步,真正让个人博客可被外部访问,还需要处理前端构建、静态资源托管、Node.js 进程守护这几件事。我自己的经验是,这一步踩的坑常常比写业务逻辑还要多,但也是理解“前后端如何成为一个网站”的关键。
5.1 前端资源交给后端托管,省掉跨域烦恼
本地开发时前后端分离是效率高,但生产服务器上,我更倾向于让 Node.js 服务直接把 Vue 构建后的静态文件一起托管。先在前端项目里执行:
bash复制npm run build
构建完成后会生成 dist 文件夹,里面有 index.html、js、css 等静态资源。在 Node.js 项目里,把它作为静态资源目录暴露出来:
javascript复制const express = require('express')
const path = require('path')
const app = express()
// 接口路由
app.use('/api', require('./routes'))
// 托管前端构建产物
const distPath = path.join(__dirname, '../dist')
app.use(express.static(distPath))
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`server is running at http://localhost:${port}`)
})
这样用户访问 3000 端口时会直接看到 Vue 首页,访问 /api/... 时才走接口逻辑。由于页面和接口处于同源下,前面提到的跨域问题在生产环境也就不存在了。
5.2 history 路由刷新 404 的根源与解决
如果你的 Vue 路由用了 HTML5 History 模式,也就是地址栏显示 /article/1 而不是 /#/article/1,刷新页面时会看到一个 404。原因是后端只识别到 / 请求,不知道 /article/1 应该返回哪个静态文件。浏览器请求 http://yourdomain/article/1 时,后端没有对应资源,就默认返回 404。
解决办法是在静态资源托管之后加一条兜底规则,把所有非 API 请求都指回 index.html:
javascript复制app.get(/^\/(?!api).*/, (req, res) => {
res.sendFile(path.join(distPath, 'index.html'))
})
需要注意的是,这条规则必须放在 API 路由之后,否则它会先拦截 /api/articles 之类的请求。如果你用的是 Nginx 做反向代理,也可以在 Nginx 里配置类似 try_files $uri $uri/ /index.html; 的规则,两种方案都能解决刷新 404。
5.3 Node 进程守护和数据备份习惯
普通命令启动的 Node.js 进程会在 SSH 终端关闭后随之退出,所以生产环境不能直接 node app.js。我会使用 PM2 做进程守护,配置文件如下:
javascript复制// ecosystem.config.js
module.exports = {
apps: [
{
name: 'blog-server',
script: './src/app.js',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '300M'
}
]
}
启动命令:
bash复制pm2 start ecosystem.config.js
pm2 save
pm2 startup
PM2 会在进程崩溃后自动重启。不过它解决的是“进程死掉后复活”,解决不了“数据被误删或者服务器硬盘坏掉”。个人博客仍然要记得备份数据库。如果是 MySQL,可以定时执行 mysqldump,把备份文件放到另一台机器或对象存储里。
6. 稳定上线之后我会继续加的部分
博客系统跑通后,有些人会急着给它加花哨功能,但我的建议是,按“对读者价值最大”的顺序来迭代。你的主流程稳定性比功能数量重要得多。
6.1 阅读体验:Markdown 渲染、代码高亮和目录
一开始文章正文可能只是普通段落文本,但技术类博客如果不用 Markdown 渲染,代码块和列表会非常难维护。我后续会引入 Markdown 解析组件,把后台写作内容改成 Markdown 来源,前端渲染成 HTML,再给代码块套一个代码高亮主题。这个改动会影响编辑页、详情页和样式文件,所以最好在项目早期就确定下来,而不是等写了几百篇文章后再换。
6.2 搜索、评论通知和访客统计
当文章数量增加以后,基础搜索功能会变成一个刚需。最简单的方式是用数据库的关键字查询,文章量更大了再考虑更专业的搜索引擎方案,个人博客这个量级,其实没有必要提前引入重组件。评论功能如果需要加,一定要先考虑垃圾评论过滤和作者通知机制,否则只是给自己添工作量。
6.3 文章里嵌入视频或地图时,需要绕开的坑
如果博客会写影音记录或者旅行记录,还会牵扯到两类特殊资源。一是页面里播放 m3u8 格式的视频流时,原生 HTML 的 <video> 标签通常无法直接播放,需要引入 hls.js 或 video.js 做一层封装转换,并且处理移动端兼容。二是在文章页嵌入腾讯地图或高德地图时,要注意地图实例的生命周期。有些开发者遇到“首次打开正常,第二次进入一片空白”,多半是前一个页面销毁时地图实例没有释放,需要在组件的销毁钩子里主动清理地图对象;地图初始化也要等容器 DOM 完全渲染后再执行,否则拿不到宽高信息。
我在迭代这套个人博客系统时最深的感受是:功能可以慢慢加,但数据格式和代码结构要早定。前后端接口返回字段一旦铺开使用,后期改成本成倍上升;ElementUI 这类组件库给了你现成的页面零件,但如何组织这些零件,仍然取决于你对业务边界的理解。如果你也在从头写一套个人博客,先不要想着一步到位,把文章列表、文章详情和登录后台这条主线跑通,再一层一层往上叠加,整套系统的可靠性会高很多。
