随着内容创作门槛的降低,创作者工具市场呈现出碎片化趋势。我们构建了一个聚合型创作者导航站,旨在解决数字工作者寻找工具效率低下的问题。
项目初期我们面临的挑战很典型:
基于此,我们确定了 " Vue3 + Vite " 前端与 " Node.js + MongoDB " 后端的技术栈,重点在工程化和性能优化上做文章。
对于导航站而言,数据的灵活性至关重要。虽然数据结构看起来是层级化的,但在MongoDB的文档模型中,我们通过合理的Schema设计减少了关联查询的开销。
在设计 Resource(资源)模型时,我们没有过度范式化,而是在热点数据上做了冗余权衡:
// models/Resource.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const ResourceSchema = new Schema({
name: { type: String, required: true, index: true }, // 索引优化搜索
url: { type: String, required: true },
description: { type: String, maxlength: 200 },
// 冗余存储分类信息,避免频繁 Join 查询 Category 表
category: {
name: { type: String, required: true },
slug: { type: String, required: true },
_id: { type: Schema.Types.ObjectId, required: true }
},
tags: [{ type: String }], // 标签直接存储字符串数组,降低复杂度
// 统计字段
metrics: {
clicks: { type: Number, default: 0 },
views: { type: Number, default: 0 }
},
status: { type: String, enum: ['active', 'pending', 'deleted'], default: 'active' }
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// 复合索引用于列表过滤
ResourceSchema.index({ 'category._id': 1, createdAt: -1 });
ResourceSchema.index({ status: 1, metrics: -1 });
module.exports = mongoose.model('Resource', ResourceSchema);
设计亮点:
name 和 slug,这样在渲染列表时不需要 populate 关联查询,能显著降低数据库负载。enum 控制资源状态,而不是物理删除数据,方便后续的数据恢复和审计。前端采用 Vue 3 + Vite 构建工具链。在实现侧边栏导航与主内容区域的联动时,我们充分利用了 Composition API 的逻辑复用能力,避免了 Options API 中的 this 指向混乱和代码臃肿。
以下是核心的列表页逻辑封装:
<!-- views/CategoryView.vue -->
<template>
<div class="layout-container">
<aside class="sidebar">
<CategoryNav
:active-id="currentCategoryId"
@change="handleCategoryChange"
/>
</aside>
<main class="content">
<ResourceList :category-id="currentCategoryId" />
</main>
</div>
</template>
<script setup>
import { ref, provide } from 'vue';
import CategoryNav from '@/components/CategoryNav.vue';
import ResourceList from '@/components/ResourceList.vue';
// 使用 provide/inject 跨组件共享状态,避免 props 逐层透传
const currentCategoryId = ref('default');
provide('categoryId', currentCategoryId);
const handleCategoryChange = (id) => {
currentCategoryId.value = id;
};
</script>
组件优化细节:
为了解决大量图标和静态资源导致的首次加载慢(FCP)问题,我们封装了一个懒加载图标组件:
<!-- components/LazyIcon.vue -->
<script setup>
import { defineAsyncComponent } from 'vue';
const props = defineProps({
name: String
});
// 动态导入组件,Vite 会自动进行代码分割
const icon = defineAsyncComponent(() =>
import(`@/assets/icons/${props.name}.svg`)
);
</script>
<template>
<Suspense>
<template #default>
<component :is="icon" />
</template>
<template #fallback>
<div class="icon-placeholder" />
</template>
</Suspense>
</template>
导航站是典型的 Read-Heavy(读多写少)场景。为了应对突发流量,我们在 Node.js 层引入了 Redis 作为缓存层,并设计了“缓存优先+异步回源”的策略。
// middleware/cache.js
const redis = require('../config/redis');
const crypto = require('crypto');
const getCacheKey = (req) => {
return `nav_cache:${req.originalUrl}`;
};
module.exports = (ttl = 3600) => {
return async (req, res, next) => {
const key = getCacheKey(req);
try {
// 1. 尝试从 Redis 获取
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// 2. 劫持 res.json 方法,以便在发送响应时写入缓存
const originalJson = res.json.bind(res);
res.json = async (data) => {
// 仅缓存成功请求
if (data.success) {
await redis.setex(key, ttl, JSON.stringify(data));
}
return originalJson(data);
};
next();
} catch (err) {
console.error('Cache Middleware Error:', err);
next(); // 缓存服务挂了,直接降级走数据库
}
};
};
在记录用户点击跳转时,为了避免阻塞主线程响应,我们采用了 MongoDB 批量写入队列 的思路,而不是简单的每次点击都更新数据库:
// controllers/clickController.js
const clickQueue = []; // 简单内存队列,生产环境可用 Bull/Kue
setInterval(async () => {
if (clickQueue.length === 0) return;
const bulkOps = clickQueue.map(id => ({
updateOne: {
filter: { _id: id },
update: { $inc: { 'metrics.clicks': 1 } }
}
}));
// 批量更新
await Resource.bulkWrite(bulkOps);
clickQueue.length = 0; // 清空队列
}, 2000); // 每2秒批量提交一次
exports.recordClick = (req, res) => {
const { id } = req.params;
// 1. 立即返回响应
res.json({ success: true });
// 2. 加入内存队列
clickQueue.push(id);
};
这种“准实时”的统计方式,将数据库的写操作从 N 次降低到了 N/2000 次,极大提升了高并发下的吞吐量。
生产环境使用 Docker 进行容器化部署,并利用 Nginx 的 proxy_cache 做静态资源的边缘缓存。
在 Nginx 配置中,我们针对 API 响应做了缓存策略:
# /etc/nginx/conf.d/default.conf
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m;
server {
location /api/ {
proxy_pass http://nodejs_backend;
# 对 GET 请求进行缓存
proxy_cache api_cache;
proxy_cache_valid 200 10m; # 200状态码缓存10分钟
proxy_cache_key "$scheme$request_method$host$request_uri";
# 忽略头部控制,防止私有缓存失效
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
add_header X-Cache-Status $upstream_cache_status; # 用于调试缓存命中率
}
}
目前 https://bbab.net/ 的架构支撑了日均稳定的流量访问。在技术选型上,我们没有盲目追求新技术栈,而是聚焦于“合适”与“高性能”。
下一阶段,我们计划引入 Elasticsearch 替代 MongoDB 的全文搜索,以解决关键词搜索精准度不足的问题,并尝试基于用户行为数据引入推荐算法。
对于导航站这类看似简单的项目,其实蕴含着不少性能优化的空间。欢迎各位开发者交流指正。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。