如果你最近在找用 Node.js + Vue + ElementUI 做的网站课题,多半会看到“中国传统文化宣传网站”这个名字。它听起来像是一个纯静态展示站,真做起来才发现要同时处理栏目体系、文章发布、图片素材、视频播放和后台管理这一大串事。正好我前后带学生做过几个类似的完整项目,也踩过不少和 Node 环境、npm 脚本、ElementUI 组件有关的坑,这里干脆把从技术选型到部署落地的完整思路写下来。
这篇内容不是那种只贴几个页面的“演示型记录”,而是会从需求源头说清楚:为什么要用这套技术栈、文化类内容如何设计成数据模型、前台哪些组件最常用、后台内容管理有哪些绕不开的细节、视频资源播放为什么会碰见 m3u8、以及 Node.js 新手最容易卡住的安装和环境问题。适合正在做课程设计或毕业设计的人参考,也适合想用 Vue 快速搭一个内容型网站的开发者拿来当底稿。
1. 传统文化专题站的技术选型逻辑与工程基础
1.1 传统文化宣传站到底需要哪些功能模块
很多人以为文化宣传网站就是“几张好看的图片加一点介绍文字”,但真拿去答辩或者上线,需求会立刻变清晰。一个能持续更新的传统文化网站,至少要覆盖前台展示和后台维护两个闭环。
前台部分通常会包含几类内容:传统节日版块,里面要有节日由来、民俗活动、相关诗词;二十四节气单元,天然适合用时间线来展示;非遗项目板块,涉及传承人、工艺过程、图片和视频资料;还有资讯或者活动公告,用来发布线下展览、文化讲座等信息。如果资料足够,还可以加关键字搜索、热门内容推荐。
后台部分则需要有人能往里录入文章、上传封面图、维护轮播 Banner、管理栏目分类。也就是说,这不只是一个静态宣传页,而是一个“内容型站点”。这也是为什么我倾向于用 Vue 这种组件化框架而不是直接堆 HTML 模板——同样是做传统文化主题,组件化之后,新增栏目、替换轮播图、调整卡片布局,都不需要动到整页代码。
1.2 为什么用 Node.js + Vue + ElementUI,而不是别的组合
用 Node.js 不一定是为了性能,更多是为了让整个项目保持同一种语言栈。传统文化宣传站的数据量不算大,但对接口开发速度、前端联调效率、本地部署自由度都有要求,Node 生态里一个 Express 应用就能把文章接口、上传接口、静态资源服务全部承担起来。
Vue 的优势在于组件化和渐进式接入。一个展示型官网拆成导航栏、轮播图、文章卡片、视频播放器、分页列表这些独立组件后,后续维护非常方便。ElementUI 则是 Vue 生态里最成熟的中后台组件库之一,尤其适合快速搭建管理后台,表格、表单校验、弹窗、分页、日期选择这些高频交互都有现成组件。
需要特别提醒:ElementUI 官方适配的是 Vue 2.x,如果项目打算直接用 Vue 3,就要换成 Element Plus。标题里写的是 ElementUI,那么技术方案里对应的就是 Vue 2 + ElementUI 2.x 这一套经典组合。Vue 2 虽然已经进入维护尾声,但对于以“可实现、可演示、可扩展”为目标的课程设计和中小型网站来说,依然是久经考验的稳定选择。
1.3 前后端目录结构与数据表设计
这类项目我习惯用下面的目录结构,把服务端和前端分开,但最终打包后又能放到同一个 Node 服务里运行:
code复制culture-website/
├─ server/
│ ├─ app.js # Express 入口
│ ├─ routes/ # 接口路由
│ ├─ uploads/ # 上传的图片、视频文件
│ └─ config/
├─ web/ # Vue 前端工程
│ ├─ src/
│ │ ├─ api/ # axios 请求封装
│ │ ├─ router/
│ │ ├─ views/ # 前台页面 + 后台管理页面
│ │ └─ components/
│ └─ package.json
数据表不要设计得太过复杂,能支撑内容发布即可。核心表可以参考下面的结构:
| 表名 | 主要字段 | 作用 |
|---|---|---|
| category | id, name, parent_id, sort | 栏目分类,支持父级与子级 |
| article | id, category_id, title, cover, summary, content, status, is_deleted, created_at, updated_at | 图文内容主体 |
| banner | id, image, title, link_url, sort, status | 首页轮播图 |
| video | id, title, cover, video_url, category_id, duration, status | 视频资料 |
| admin_user | id, username, password, nickname | 后台登录账号 |
在实际开发中,我还会给 article 表留一个 is_deleted 字段做软删除,后台误删内容时还能恢复,演示的时候也不会因为删了数据就手忙脚乱。这算是内容管理系统里很基础但很重要的习惯。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前台文化内容展示:从轮播到分页的组件落地细节
2.1 ElementUI 组件选型:全量引入还是按需引入
前台页面里,ElCarousel、ElCard、ElTag、ElPagination、ElDialog、ElTimeline 这些组件几乎都会被用到。对于课程设计和中小项目,我建议开发阶段先全量引入 ElementUI,配置省心,不会因为漏掉组件而到处报错。
入口文件里的引入方式:
javascript复制import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
如果项目对首屏体积要求高,等所有页面开发完再改成按需引入也来得及。按需引入时需要配合 babel-plugin-component,在 .babelrc 或 babel.config.js 里做配置。对这个项目来说,真正上线后文化类图片往往是大头,组件库那点体积反而没那么敏感,所以不要一上来就被“按需引入”这个优化点卡住。
2.2 首页轮播图和栏目卡片的组织方式
传统文化主题的首页不适合放太满,轮播图区域一般是核心视觉入口。用 ElementUI 的 el-carousel 可以很快实现:
html复制<el-carousel height="420px" :interval="5000">
<el-carousel-item v-for="item in banners" :key="item.id">
<div class="banner-text">
<h2>{{ item.title }}</h2>
<p>{{ item.subtitle }}</p>
</div>
</el-carousel-item>
</el-carousel>
很多文化类网站的问题在于——图片素材风格不统一,轮播图看起来像临时拼凑的。建议在后台维护 Banner 时统一图片尺寸规范,比如宽度不低于 1920 像素,核心文字不要出现在画面边缘。代码层面的实现其实不难,真正影响观感的是素材规范。
首页栏目板块可以直接用 el-card 组合布局。每个版块用 v-for 循环渲染栏目数据,每个卡片放封面、标题、摘要,点击后跳文章列表或详情。到这里已经能感受到 Vue 组件化的好处:不用每个页面手写重复的结构,数据一变页面就跟着变。
2.3 el-pagination 分页到底怎么和 Node 接口配合
传统节日、非遗项目下面的文章数量一旦多起来,前端就不能一次性把所有数据拉下来。el-pagination 是 ElementUI 里被问得最多的组件之一,很多人的疑问是“为什么页码变了但数据没变”,原因通常出在分页事件没有真正重新请求接口。
前端典型写法:
html复制<el-pagination
background
layout="prev, pager, next, total"
:total="total"
:page-size="pageSize"
:current-page.sync="currentPage"
@current-change="loadArticleList"
/>
对应的事件处理:
javascript复制async loadArticleList() {
const params = {
page: this.currentPage,
pageSize: this.pageSize,
categoryId: this.activeCategoryId
}
const res = await getArticleList(params)
this.articleList = res.data.list
this.total = res.data.total
}
后端接口则要用 LIMIT 和 OFFSET 来做真正的分页,而不是把全量数据返回前端再截取:
javascript复制const page = parseInt(req.query.page) || 1
const pageSize = parseInt(req.query.pageSize) || 10
const offset = (page - 1) * pageSize
const [list] = await db.query(
`SELECT id, title, cover, summary FROM article
WHERE category_id = ? AND status = 1 AND is_deleted = 0
ORDER BY id DESC
LIMIT ? OFFSET ?`,
[categoryId, pageSize, offset]
)
很多人容易忽略一个细节:total 不能用当前列表的长度,应该单独查一次总数,否则数据超过一页后分页就乱套了。只要保证 total 是 COUNT(*) 的结果,组件显示和跳转逻辑才会稳定。
2.4 el-timeline 时间线在节气页里的自定义玩法
二十四节气页面特别适合时间线组件,但默认的时间线展示太单调,人家问的“ElementUI 的时间线如何插槽自定义 timestamp”其实就是想把时间戳区域改成更好看的内容,比如加上农历、节气解释、配图。
Vue 2 + ElementUI 2.x 里,可以通过 slot 具名插槽来自定义时间戳内容:
html复制<el-timeline>
<el-timeline-item
v-for="item in solarTerms"
:key="item.id"
placement="top"
>
<template slot="timestamp">
<div class="custom-term-date">
<span class="term-icon">{{ item.icon }}</span>
<span class="term-name">{{ item.name }}</span>
<span class="term-date">{{ item.date }}</span>
</div>
</template>
<el-card>
<p>{{ item.description }}</p>
<img :src="item.image" alt="节气图片">
</el-card>
</el-timeline-item>
</el-timeline>
这里最需要注意的坑是:ElementUI 2.x 的时间线自定义插槽必须放在 el-timeline-item 内部,如果放到组件外层就没有效果。自定义时间戳后,时间线和内容卡片之间的对齐位置需要微调 CSS,比较稳妥的做法是给自定义区域设置固定宽度,比如 160 到 200 像素之间。
2.5 路由跳转和参数传递的稳妥做法
从首页卡片跳转文章详情,很多初学者都会写 this.$router.push({ name: 'detail', params: { id: item.id } })。这种写法在页面内跳转没问题,但如果用户刷新了详情页,params 里的参数可能会丢失,页面就白屏了。
对内容型网站来说,更稳的是用 query:
javascript复制this.$router.push({
path: '/article/detail',
query: { id: item.id }
})
详情页再通过 this.$route.query.id 获取参数。这样即使刷新页面,URL 里仍然带着 id,不会找不到数据。经验之谈:项目里凡是要被分享、收藏、刷新后还在的页面,都优先考虑 query 或者把 id 拼进路由路径里。
3. 后台内容管理:富文本、图片上传与多选全选的实操改造
3.1 后台界面的布局不一定很复杂,但交互要成体系
管理后台不需要做得花哨,功能完整最重要。推荐用 ElementUI 的 el-container 搭整体结构:左侧菜单放分类管理、文章管理、轮播图管理、视频管理,右侧主区域放对应的内容列表和编辑表单。
我见过很多半成品后台,文章列表能展示,但点“编辑”之后弹出的表单只有标题和正文,其他字段全靠数据库手工改,这在演示时非常露怯。后台至少要形成“列表——新增——编辑——保存——删除/软删除”的完整闭环,哪怕栏目只有几个,也要让内容管理员能自己操作,而不需要开发人员每次去数据库里调整。
3.2 el-table 行选中与 el-select 多选全选的高级写法
后台文章管理里,常见的需求是批量选择文章后统一修改分类或下架。el-table 自带单选和多选能力,给表格加一列 type="selection" 即可:
html复制<el-table :data="articleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="categoryName" label="分类" />
<el-table-column label="状态">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? '已发布' : '已下架' }}
</el-tag>
</template>
</el-table-column>
</el-table>
另一个高频需求是用 el-select 做分类筛选,并且要求支持多选。通常还会配一个“全选”的快捷选项。最直接的方式是在 el-select 里加一个单独的回调按钮,不要试图把“全选”和普通分类混在一个多选下拉里,否则反选和取消全选的逻辑会写到你怀疑人生。
我自己的实现方案是:在筛选区域放一个“全选”按钮,点击后把当前所有分类 id 放进选中数组;再放一个“清空”按钮。这样比在下拉菜单里塞全选选项更直观,也不会干扰 el-select 本身的交互逻辑。
javascript复制selectAllCategories() {
this.filterForm.categories = this.allCategories.map(item => item.id)
},
clearCategories() {
this.filterForm.categories = []
}
3.3 富文本编辑器的图片上传,别用 base64 硬扛
后台录入传统节日、非遗项目的内容时,正文里会插入大量图片。许多学生喜欢直接把图片转成 base64 塞进富文本内容里。少量截图没问题,但一张高清书法作品或者古建筑照片经过 base64 编码后,体积会膨胀大约三分之一,内容表很快就变成几十 MB 的怪物,页面加载也会明显变慢。
正确做法是单独做图片上传接口。比如使用 multer 处理文件上传:
javascript复制const multer = require('multer')
const path = require('path')
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'uploads/')
},
filename(req, file, cb) {
const ext = path.extname(file.originalname)
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1e9) + ext
cb(null, uniqueName)
}
})
const upload = multer({ storage })
app.post('/api/upload/image', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ code: 400, msg: '上传失败' })
}
const url = `/uploads/${req.file.filename}`
res.json({ code: 200, data: { url } })
})
前端配合富文本编辑器,把上传按钮指向这个接口,拿到返回的 URL 后再插入正文中。这样做的好处非常明显:数据库只存链接,图片文件单独存放在 uploads 目录,后续要做 CDN 加速或者备份迁移都非常方便。
还有一点需要关注:富文本内容如果直接来自第三方编辑器或复制的网页,可能携带危险的 HTML 标签。后端保存前要做一个基础过滤,至少要把 <script>、<iframe>、<embed> 这些标签清理掉,否则后台一旦被非技术人员使用,就是个潜在隐患。
3.4 封面图、轮播图的裁剪与压缩规范
前台展示效果不好的原因,很多时候不在代码而在图片。封面图尺寸不一致,卡片列表就会参差不齐。ElementUI 的卡片本身能限制宽度,但图片如果原始比例差异过大,整个页面还是会被撑得很乱。
项目里我习惯做两件事。第一,上传接口里对图片进行压缩处理,直接用 sharp 这个 Node 图像处理库,把上传的图片统一转成 WebP 格式并限制最大宽度。第二,在后台表单里明确提示“封面图建议尺寸 800x450”,前端可以对图片做 object-fit: cover 兜底,避免图片变形。这样做之后,首页会干净非常多,这也是实际运营中很容易忽略的细节。
4. 图文之外的资源展示:m3u8 视频播放里那些绕不过去的坑
4.1 为什么文化类视频资料经常以 m3u8 形式出现
传统文化宣传网站里,非遗工艺视频、纪录片、戏曲片段是很有价值的内容。但视频文件普遍体积大,如果直接放一个 MP4 上去,用户加载慢,服务器带宽也吃不消。很多视频平台会把原始视频转码成 HLS 流,也就是 m3u8 索引文件加一堆 ts 视频切片。播放器根据 m3u8 文件里的地址逐个加载切片,可以实现边下边播、拖动进度更平滑。
原生 <video> 标签在 iOS Safari 上能直接播 m3u8,但在 Windows 上的 Chrome、Edge 浏览器里并不支持。这就是“vue 播放 m3u8”这个问题特别常见的原因。要在 Vue 项目里兼容这些浏览器,需要借助 hls.js 这个库,它能把 m3u8 流转换并喂给原生 video。
4.2 用 hls.js 在 Vue 里播 m3u8
安装 hls.js:
bash复制npm install hls.js
组件里的核心逻辑:
vue复制<template>
<video
ref="videoPlayer"
class="culture-video"
controls
:poster="videoInfo.cover"
></video>
</template>
<script>
import Hls from 'hls.js'
export default {
props: {
videoInfo: {
type: Object,
required: true
}
},
mounted() {
this.playM3u8()
},
methods: {
playM3u8() {
const video = this.$refs.videoPlayer
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 等原生支持的场景
video.src = this.videoInfo.videoUrl
} else if (Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(this.videoInfo.videoUrl)
hls.attachMedia(video)
this.hls = hls
} else {
this.$message.error('当前浏览器不支持 HLS 播放')
}
}
},
beforeDestroy() {
if (this.hls) {
this.hls.destroy()
}
}
}
</script>
这里有一个容易踩的坑:hls 实例一定要在组件销毁时调用 destroy(),否则页面切换后播放器仍然占用资源,再次进入页面会有异常。另一个坑是 hls.js 处理 m3u8 和 ts 文件时对跨域要求比较高,如果接口和视频文件不在同一个域名下,服务端必须设置正确的 Access-Control-Allow-Origin 响应头,并且允许相应的请求方法。
4.3 自动播放策略、封面图与弹层销毁
文化宣传网站首页有时想放一个背景视频自动播放。但浏览器自动播放策略很统一:带声音的视频不能自动播放,除非用户已经和页面有过交互。如果一定要自动播放,常见解法是给 video 加 muted 属性和 playsinline,静音循环播放,用户点击后才开启声音。这个限制和 Vue、ElementUI 都没关系,是浏览器层面的硬性规定,设计需求的时候就要提前考虑。
封面图不要默认依赖视频的某一帧。m3u8 加载需要时间,如果 poster 没有设置,用户看到的可能是一段黑屏或浏览器默认画面。我会在视频组件里单独用一张经过压缩的封面图,同时在 video 标签上绑定 poster 属性。
如果视频是在 el-dialog 弹窗里打开的,建议给弹窗加 destroy-on-close 属性,关闭弹窗时销毁内部组件。否则视频可能没有真正暂停,声音会一直响,非常尴尬。
4.4 视频文件放哪里,服务器该怎么配
如果只是课程设计,视频文件可以直接放在服务器的 uploads 目录下,前端通过路由访问。但如果视频文件比较多,更稳妥的方案是使用对象存储,把 m3u8、目录和 ts 切片都放到对象存储或 CDN 上,后台只维护文件地址。
视频放自己服务器时,不要放在 Vue 工程的 public 目录里然后打成前端包,否则每次更新前端代码都要连同几百 MB 的视频重新打包。更好的做法是把视频目录映射成 Node 服务的一个静态目录:
javascript复制app.use('/media', express.static(path.join(__dirname, 'media')))
这样上传的视频都进 media 目录,前端视频地址就是 http://服务器地址:端口/media/xxx.m3u8,与前端代码互不干扰,备份和迁移时也只需要单独处理 media 目录。
5. Node.js 环境搭建与 npm 高频报错处理实录
5.1 Node.js 下载、LTS 版本选择与环境变量配置
很多人第一次接触 Node.js,第一件事就去官网下载最新版。但“最新版”不一定是“最合适版”。Node.js 官网会把版本分为 LTS 和 Current 两类,LTS 是长期维护版,稳定性更好,适合实际项目开发。做 Vue 2 项目时,Node 16 或 18 的 LTS 版本基本都能顺利跑起来;如果用了更新的工程化工具,可能需要 Node 18 以上。建议先查一下自己要用的 Vue CLI 或 Vite 版本要求,再决定装哪个 Node。
安装时有一个容易忽略的点:Windows 安装包会问是否自动把 Node 加入 PATH,这个勾选一定要保留。安装完成后打开命令行工具检查:
bash复制node -v
npm -v
如果弹出“node 不是内部或外部命令”,大概率是环境变量没配好。需要到“系统属性 -> 环境变量 -> Path”里确认有没有 Node.js 安装目录,没有就手动加进去。安装目录不要选择带中文或带空格过多的路径,虽然不一定出错,但很多第三方工具在解析路径时会很脆弱。
5.2 npm.ps1 无法加载脚本这个报错,到底是什么原因
在 Windows 上使用 npm 时,很多同学会在 PowerShell 里遇到下面这串错误:
code复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
这不是 Node 本身坏了,而是 PowerShell 的脚本执行策略默认禁止运行 .ps1 脚本。解决方案有两种。
第一种,用管理员身份打开 PowerShell,执行:
powershell复制Set-ExecutionPolicy RemoteSigned
执行策略改成 RemoteSigned 后,本地创建的脚本可以运行,从互联网下载的脚本需要有签名,这是一个相对安全的策略。执行时会提示确认,输入 Y 回车即可。
第二种,如果你只是偶尔用一次,也可以直接用 CMD,或者使用 npm.cmd 命令:
bash复制npm.cmd -v
这完全不是一个需要纠结很久的疑难杂症,理解了原理,碰到类似报错就不会慌。
5.3 下载依赖慢、装不上、版本冲突怎么处理
如果使用了默认 npm 源,安装依赖速度可能让人崩溃。npm 是国外源,国内网络环境拉取大包时经常超时。可以换成国内的镜像源:
bash复制npm config set registry https://registry.npmmirror.com
npm config get registry
执行完 get 命令如果能回显刚才设置的地址,就说明镜像源配置成功。
还有一种情况是项目已经存在 package-lock.json,但安装依赖时出现各种奇怪报错。这时候可以试试清理缓存并重新安装:
bash复制npm cache clean --force
rm -rf node_modules package-lock.json
npm install
注意:删除 package-lock.json 再重新生成,会让依赖版本重新锁定,如果项目是团队协作的,建议谨慎操作。不过对个人项目来说,这是处理依赖环境紊乱比较有效的办法。在 npm 里出现“peerDependencies”冲突时,也常常是某个依赖版本对 Vue 或 Node 版本有硬性要求,先查版本匹配比强行 --force 安装更稳妥。
5.4 安装 Vue、启动开发服务器和调试面板的细节
全局安装 Vue CLI 时,如果权限不足可以加 --force,但全局安装本身并不推荐都这样做,用 npx 按需运行其实更干净。Vue 2 项目的创建方式:
bash复制npm install -g @vue/cli
vue create web
在 vue create 选择预设时,如果选了 Vue 2,脚手架会自动配上 ElementUI 所需的 Vue 版本匹配链。之后要单独安装 axios:
bash复制npm install axios
启动开发环境:
bash复制npm run serve
如果端口被占用,CLI 通常会提示询问是否换到其他端口。如果没自动处理,也可以自己指定端口启动:
bash复制npm run serve -- --port 8081
开发阶段检查 Vue 组件状态、路由变化时,Vue Devtools 插件几乎是必备工具。这个浏览器扩展能直观看到组件的 data、props 以及 Vuex 状态变化。如果安装后还是看不到面板,先确认开发页面是 Vue 2 还是 Vue 3,不同版本的 Devtools 分支支持范围不一样;再确认浏览器是否允许扩展访问本地文件。调试 Vue 项目时,“看不到面板”多数不是代码问题,而是扩展或浏览器权限的问题。
5.5 前后端联调时的代理配置
前端开发服务器默认跑在 8080 端口,Node 后端跑在 3000 或者 9090 端口,这属于跨域。用 axios 直接请求会报错,我通常会用前端代理解决,开发环境的配置在 Vue 项目的 vue.config.js 里:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
},
'/uploads': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}
配置之后,前端代码里请求地址写 /api/article/list,开发服务器会把请求转发到 http://localhost:3000/api/article/list。这样前端代码和实际部署后的地址格式保持一致,非常省心。如果项目里没有 vue.config.js,自己创建一份就行,Vue CLI 脚手架会读取这个文件。
6. 打包部署与运营扩展:从“做完”到“能用”的最后一公里
6.1 打包后的 dist 目录如何和 Node 服务一起运行
项目开发结束后,前端和后端通常要部署到同一台服务器上。先把前端打包成静态文件:
bash复制npm run build
打包完成后,web/dist 目录里就是可以直接托管的 HTML、CSS、JS 文件。在 Express 中把 dist 目录和上传目录都设为静态目录:
javascript复制const path = require('path')
app.use(express.static(path.join(__dirname, '../web/dist')))
app.use('/uploads', express.static(path.join(__dirname, 'uploads')))
这样用户访问服务器根路径时,Node 服务会返回前端页面;用户访问 /api/... 时,命中的是接口逻辑;访问 /uploads/... 时,返回的是图片和视频文件。整个项目只需要启动一个 Node 进程,对没有单独部署 Nginx 经验的初学者来说,这种方案最容易跑通。
6.2 路由 history 模式刷新 404 的问题
Vue Router 默认使用 hash 模式,URL 里会带一个 #,比如 http://localhost:3000/#/article/detail?id=1。这种模式虽然不好看,但刷新不会出问题。如果想把 # 去掉,就要用 history 模式,这时需要后端配合,否则用户直接访问 /article/detail 或者刷新页面时,Express 会去找不存在的真实文件,然后返回 404。
一个简化处理是在静态资源托管之后加一层回退:
javascript复制const history = require('connect-history-api-fallback')
app.use(history())
如果不想引入额外依赖,可以在所有 API 路由注册完后,对非 /api 开头的 GET 请求统一返回 dist/index.html:
javascript复制app.get(/^\/(?!api|uploads).*/, (req, res) => {
res.sendFile(path.join(__dirname, '../web/dist/index.html'))
})
但对于课程设计和内部演示,我往往直接建议用 hash 模式,减少一个坑。如果你的项目目标是正式上线且 SEO 要求比较高,再去考虑 history 模式和 SSR 方案。对传统文化宣传网站来说,内容更新本身比 SEO 优化更迫切,hash 模式完全够用。
6.3 部署上线前的几个环境细节
Node 服务默认端口可以用 3000,也可以自己配置一个端口。把项目靠 node server/app.js 跑起来后,不要关掉终端就完事。正式部署有几个细节值得提前准备。
开发阶段很多人用 node server/app.js 直接启动进程,窗口一关服务就断了。服务器上推荐用进程守护工具,比如 pm2:
bash复制npm install -g pm2
pm2 start server/app.js --name culture-website
pm2 save
pm2 可以保证服务在异常退出后自动重启,重启云服务器后也可以配置开机自启。
另外,不要把端口直接设为 80,除非你了解系统权限问题。通常的做法是让应用监听 3000 或 8080,再由云服务器管理面板放行对应端口访问权限。如果后续要绑定域名并将 80 端口转发到 Node 服务,建议用常见的反向代理方案处理,比如 Nginx,这样后续做 HTTPS、静态资源缓存也会更方便。
6.4 传统文化内容的日常运营与后台维护经验
网站真正“能用”,不只是功能跑通,内容更新机制也得顺。文化宣传网站最尴尬的状态是上线半年后,内容永远停留在第一次发布的几篇文章。数据模型设计时就要考虑到持续更新的可能性。
建议后台增加合适的运营状态字段:文章的 status 可以区分草稿、已发布、已下架;热门内容可以有置顶级别;首页的 Banner 和推荐板块应允许运营人员直接调整排序。这样平时更新栏目、下线过期活动、把重要节日内容推到首页,就不再需要改代码。
做这类内容型网站时,我还有一个实际体会:素材管理比代码更花时间。传统文化内容碎片化比较严重,一篇文章可能要查很多资料、找高清图、核对节气时间,甚至要反复确认不同地区民俗的差异。如果在项目一开始就定好素材规范,比如图片统一宽度、版权来源标注、引用资料记录,后续内容录入会轻松很多。
6.5 后续扩展方向:移动端适配与小程序复用
这类传统学问的题材,很多用户是从手机端访问的。如果做完桌面端网站后还有余力,可以优先做移动端适配,Vue 项目里响应式布局加上 ElementUI 的栅格系统,能让大多数页面在手机上不至于太难看。但要真想有更好的手机端体验,更推荐单独做一套移动端页面,移动端组件库可以考虑 Vant。
更省力的路线是:后端接口从一开始就不要绑死前端页面,所有数据都通过 /api 返回 JSON,那么后续做微信小程序或者别的展示端时,只要复用这套接口就可以,不需要重写 Node 服务。这也是我在组织后台接口时特别注意的事情——不要把业务逻辑直接写死在 Vue 组件里,而是抽象出 api 方法层,这样多端复用时非常顺畅。
这套技术方案从需求推导到部署上线,整个过程里最核心的经验就是“用合适的技术解决合适的问题”。传统文化宣传网站不需要高性能分布式架构,不需要炫酷的实时渲染,它需要的是清晰的信息结构、方便的内容维护、稳定的页面访问体验。Node.js + Vue + ElementUI 的组合恰好能在一个学习成本相对可控的范围内,把这些事情都做完。如果你正好在做一个类似课题,照着这个思路先把数据模型和内容管理闭环搭好,再去打磨页面视觉,整个过程会顺畅得多。
