ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Express从零搭建文学交流平台:架构、安全与性能实战

用Express从零搭建文学交流平台:架构、安全与性能实战 1. 文学交流平台到底需要什么从需求到技术选型我接到这个项目的时候需求方说得挺笼统做一个文学爱好者的交流平台让用户能发布作品、互评互动。听起来简单但文学交流这四个字背后其实藏着一堆需要想清楚的问题——作品以什么形式呈现用户之间怎么产生社交关系内容如何分类和检索这些不在一开始定清楚后面写代码就是反复推翻重来。1.1 这个平台要解决什么问题文学交流平台的本质不是发文章而是让作品被看见、被讨论。我拆解下来核心需求主要集中在四块作品展示用户能发布小说、诗歌、散文、书评等内容支持分类浏览和关键词搜索。互动评论读者能对作品发表评论作者能回复形成基本的交流闭环。用户体系注册、登录、个人主页记录用户的发布的文章和评论历史。内容管理管理员能对违规内容进行下线处理保证平台基本的内容安全。这么拆完就会发现这个项目的难点不在某个单一功能而在模块之间的联动。比如用户登录状态怎么维持、评论如何关联到对应作品、首页热门推荐怎么计算——这些都是集成层面的活。1.2 为什么是Express而不是其他框架选Express不是因为它是最先进的而是因为它在我需要解决的问题面前是性价比最高的选择。我拿它和几个同类方案做了对比框架优势劣势适合场景Express轻量灵活中间件生态极其丰富上手快Node.js社区事实标准架构自由度太高需要自己约定规范中小型Web应用、RESTful API、快速原型Koa更现代的异步处理洋葱模型中间件生态相对Express略少部分插件需要自己封装对异步流程控制有特殊需求的场景NestJS模块化架构依赖注入适合大型团队协作学习成本高样板代码多对简单项目偏重企业级复杂系统Fastify性能极佳Schema验证内置社区规模和资料量不如Express追求极致性能的API服务这个项目是典型的单体应用服务端渲染页面的形式Express的路由和中间件模型让模板渲染与API接口可以共存一套体系。而且遇到问题搜解决方案Express的答案永远是最多的——这对个人开发者来说非常重要因为你不会想在一个冷门框架的坑里卡三天。1.3 整体分层架构设计我最终定的架构是经典的三层结构路由层Routes接收请求、参数校验、调用控制器、返回响应。只管分发不管怎么算。控制器层Controllers处理业务逻辑编排数据操作决定返回给前端什么数据格式。数据访问层Models与MySQL数据库打交道封装增删改查不暴露SQL细节到上层。这样的好处是以后如果想加一个用户关注功能只需要在Model层加对应的数据操作在Controller层写业务规则再在Routes层挂一个接口改动完全隔离。另外考虑到页面渲染我使用了Express默认支持的EJS模板引擎。服务端渲染在这个项目里的优势明显文学内容对SEO有天然需求读者在搜索引擎搜某部小说书评时需要能被搜到。纯前后端分离的SPA方案在这一点上很吃亏。2. 从零搭建开发环境Node.js安装与项目初始化很多人在这一步就被卡住了。热搜词里关于npm.ps1因为在此系统上禁止运行脚本的报错频率相当高我自己的学生和同事隔三差五就会发这个截图给我。这里我详细说一下我在Windows和Mac上分别是怎么处理的。2.1 Node.js安装与那个著名的npm.ps1报错Node.js的安装本身不复杂直接到官网下载LTS版本一路下一步就行。但很多人装完以后在终端执行npm -v突然冒出来这么一句npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这个问题的根源是Windows PowerShell的脚本执行策略默认是Restricted状态不允许执行任何.ps1脚本文件。npm在Windows上通过npm.cmd和npm.ps1两个入口暴露命令PowerShell默认走.ps1于是就被拦住了。解决方案有几个层级当前用户生效推荐以管理员身份打开PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned然后选Y。这个策略的含义是本地脚本可以直接运行从远程下载的脚本必须经过签名验证。日常开发完全够用。仅当前会话生效Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process。缺点是关掉终端就失效了适合临时救急。改用CMDCMD不执行.ps1文件直接走npm.cmd。不想动策略的话换终端也行。我个人的建议是用第一种因为后面很多工具链比如vue-cli、pnpm的脚本都会依赖PowerShell的执行能力早早放开比每次遇到再处理要省心。配置完以后把npm的全局包路径和缓存路径也顺手改一下不要让所有包都堆在C盘系统目录里npm config set prefix D:\nodejs\node_global npm config set cache D:\nodejs\node_cache2.2 Express项目初始化与目录规划初始化Express项目有两条路用express-generator脚手架生成骨架手工创建项目结构我一般推荐新手先用脚手架因为它的目录结构是社区沉淀下来的标准布局先跑起来看看每个文件是干什么的再按照自己的项目改造。执行npx express-generator literary-platform -e-e参数是指定EJS作为模板引擎这样生成的项目自带views目录、public静态资源目录、bin/www启动文件。生成完以后我先做一次减法把默认脚手架里用不到的代码清理干净然后再按我的业务分层调整目录literary-platform/ ├── bin/ │ └── www # 项目启动入口 ├── public/ │ ├── css/ │ ├── js/ │ └── images/ ├── routes/ │ ├── index.js # 首页路由 │ ├── users.js # 用户相关 │ ├── works.js # 作品相关 │ └── comments.js # 评论相关 ├── views/ │ ├── layout.ejs │ ├── index.ejs │ ├── works/ │ │ ├── detail.ejs │ │ └── publish.ejs │ └── users/ │ ├── login.ejs │ └── register.ejs ├── models/ │ ├── db.js # 数据库连接池 │ ├── userModel.js │ ├── workModel.js │ └── commentModel.js ├── controllers/ │ ├── userController.js │ ├── workController.js │ └── commentController.js ├── app.js # Express应用核心配置 └── package.json2.3 Express应用初始化与核心中间件配置app.js是整个应用的枢纽Express所有的中间件都串在这里。我把自己项目里的核心配置拿出来讲一下关键点const express require(express); const path require(path); const cookieParser require(cookie-parser); const logger require(morgan); const session require(express-session); const app express(); // 视图引擎配置 app.set(views, path.join(__dirname, views)); app.set(view engine, ejs); // 中间件加载顺序很重要 app.use(logger(dev)); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, public))); // 会话管理 app.use(session({ secret: your_secret_key, resave: false, saveUninitialized: true, cookie: { maxAge: 1000 * 60 * 60 * 24 } // 一天 })); require(./routes/index)(app); require(./routes/users)(app); require(./routes/works)(app); require(./routes/comments)(app); module.exports app;这里有两个容易被忽视的点一个是urlencoded的extended: false它决定了表单提交的POST参数是用querystring模块还是qs模块解析取false就够了嵌套对象解析在这个项目里用不上。另一个是session的resave和saveUninitialized现在很多教程还在用旧写法Express 4.x以上如果不显式声明服务端会一直报warning刷屏。resave: false的意思是会话数据没修改就不重新保存saveUninitialized: false的意思是未初始化的会话比如用户只是看了一眼首页没登录不创建session存储减少数据库和内存的无效写入。3. 数据库建模文学平台的表结构设计数据库是一个内容平台的地基表结构如果设计得不好后面做任何功能都像在烂泥上盖楼。我花了一天时间专门建模这里把最终的表设计展开讲。3.1 用户、作品、评论三大核心实体文学交流平台最核心的实体就是这三个用户、作品、评论。我分别建了users、works、comments三张主表再加一张categories分类表和一张work_praises赞记录表。先看用户表CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, avatar_url VARCHAR(255) NULL, bio VARCHAR(500) NULL, is_admin TINYINT DEFAULT 0, status TINYINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );password_hash存储的是bcrypt加密后的哈希值绝对不存明文密码。is_admin区分普通用户和管理员status是留作封号用的软删除标记。作品表设计的时候我纠结了一下到底要不要把章节做成单独的表。后来想明白了这个平台的定位是文学交流大多数人分享的是短篇、诗歌、书评、随笔而不是连载长篇小说。如果为了一个假设性的场景过度拆分表结构反而增加复杂度。所以我把作品设计成单表加了一个content_type字段区分体裁CREATE TABLE works ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, title VARCHAR(100) NOT NULL, content MEDIUMTEXT NOT NULL, type VARCHAR(20) DEFAULT essay, category_id INT NULL, view_count INT DEFAULT 0, like_count INT DEFAULT 0, comment_count INT DEFAULT 0, is_featured TINYINT DEFAULT 0, status TINYINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_category (category_id), INDEX idx_created (created_at) );view_count、like_count、comment_count这三个字段是冗余字段。按理说这些数据通过COUNT(*)和JOIN也能算出来但文学平台的列表页、排行榜、首页推荐都需要频繁读取这些数据每次都去实时聚合计算数据量一大MySQL的压力会非常大。用空间换时间定期同步是内容类项目里的常规做法。评论表则体现了多级嵌套的设计CREATE TABLE comments ( id INT AUTO_INCREMENT PRIMARY KEY, work_id INT NOT NULL, user_id INT NOT NULL, parent_id INT DEFAULT 0, content TEXT NOT NULL, status TINYINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_work (work_id), INDEX idx_user (user_id) );parent_id为0表示一级评论不为0则表示是某条评论的回复。这里我没有做无限级嵌套因为文学平台里评论区通常只要两层就够用了——对作品的评论以及评论之间的回复。无限层级在查询和分页上都会指数级变复杂大部分场景下属于过度设计。3.2 表关系与业务联动设计表之间的关系其实很清晰一个用户可以有多个作品所以works.user_id外键关联users.id。一个用户可以有多个评论所以comments.user_id关联users.id。一个作品下有多条评论所以comments.work_id关联works.id。我在应用层处理外键约束而不是完全依赖数据库的ON DELETE CASCADE。原因在于比如用户注销后他的作品可能是要保留的作协、作家的老作品有历史价值只是显示为已注销用户。这个业务规则在数据库层面表达不出来只能靠Controller层的逻辑去判断。分类表categories用来解决找同类作品的需求。文学内容天然可以分小说、诗歌、散文、评论、剧本等多个类别我用一棵最简单的树形结构存储CREATE TABLE categories ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0, sort_order INT DEFAULT 0 );parent_id为0的是顶级分类非0则为二级分类。前端导航栏用一次查询拉全量数据在内存里拼树不反复查库。3.3 连接池管理为什么不能每次请求都新建连接数据库连接这一块我自己早期踩过大坑——每个请求都mysql.createConnection()跑一段时间数据库就报Too many connections。原因很简单MySQL的默认最大连接数是151个左右而Node.js是事件驱动的高并发模型几百个请求同时进来的时候每个连接都没来得及释放连接数瞬间被打满。正确做法是使用连接池const mysql require(mysql2); const pool mysql.createPool({ host: localhost, user: root, password: your_password, database: literary_platform, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); const promisePool pool.promise(); module.exports promisePool;connectionLimit: 10的意思是池子里最多保持10个连接复用用完归还而不是销毁。queueLimit: 0表示当10个连接全被占用时新的请求排队等待而不是报错。使用的时候配合async/awaitasync function findUserByUsername(username) { const [rows] await promisePool.query( SELECT * FROM users WHERE username ?, [username] ); return rows[0]; }连接池的好处在于连接本身是复用的创建和销毁的开销被省掉了在高并发下数据库的负载也会平滑很多。4. 核心功能模块设计与实现4.1 用户注册登录密码加密与Session登录态用户的注册登录是文学平台的入口。这里我要重点说说密码加密的选型。以前很多老项目用MD5加密密码后来加了加盐逻辑但MD5本身已经被验证是不安全的——它的计算速度太快了GPU能在一秒内跑几十亿次暴力枚举。正确的做法是用bcrypt这类刻意设计得很慢的哈希算法慢到什么程度一次哈希运算在几百毫秒到一秒之间。这个慢对用户感知来说无所谓但对暴力破解来说是灾难性的。我用的bcryptjs是纯JavaScript实现安装简单不需要编译原生模块const bcrypt require(bcryptjs); async function register(req, res) { const { username, email, password } req.body; if (!username || !email || !password) { return res.status(400).render(users/register, { error: 所有字段都是必填项 }); } // 检查用户名是否已存在 const existing await userModel.findUserByUsername(username); if (existing) { return res.status(409).render(users/register, { error: 该用户名已被占用 }); } // 哈希密码10是salt rounds通常10-12比较合适 const passwordHash await bcrypt.hash(password, 10); const userId await userModel.createUser({ username, email, password_hash: passwordHash }); // 注册成功后自动登录写入Session req.session.user { id: userId, username: username }; res.redirect(/); }登录时验证密码的逻辑就是bcrypt.compareconst isMatch await bcrypt.compare(password, user.password_hash); if (!isMatch) { return res.status(401).render(users/login, { error: 用户名或密码错误 }); } req.session.user { id: user.id, username: user.username }; res.redirect(/);登录态是通过express-session维护的session ID存放在客户端的cookie里服务端Session里存用户信息。每次请求时中间件根据cookie里的session ID找到对应的session数据把req.session.user挂上后续接口就能识别当前用户了。为了通用性好我把是否已登录做成了一个中间件function requireLogin(req, res, next) { if (req.session.user) { return next(); } return res.redirect(/users/login); }发布作品、管理个人中心的页面全都可以挂上这个中间件一行声明式代码解决问题。4.2 作品发布与管理文件上传与富文本处理作品发布是整个平台的核心功能我选择用multer处理内容中的图片上传用express-validator做参数校验。先看发布接口的路由与控制器// routes/works.js router.get(/publish, requireLogin, (req, res) { const categories await categoryModel.getAllCategories(); res.render(works/publish, { categories }); }); router.post(/publish, requireLogin, upload.single(cover), async (req, res) { const { title, content, type, category_id } req.body; // 服务端二次校验不能只依赖前端 if (!title || title.trim().length 2) { return res.status(400).render(works/publish, { error: 标题不能少于2个字符 }); } if (!content || content.trim().length 10) { return res.status(400).render(works/publish, { error: 正文内容不能少于10个字符 }); } const workId await workModel.createWork({ user_id: req.session.user.id, title: title.trim(), content: content, type: type, category_id: category_id, cover_url: req.file ? /uploads/${req.file.filename} : null }); res.redirect(/works/${workId}); });这里必须强调一个经验前端可以校验但后端绝不能相信前端。表单的必填项、格式要求前端做是为了用户体验后端做才是真正的安全边界。绕过前端校验直接Post是攻击者的基本功。管理自己的作品我用了一个简单的我的书架页面逻辑是展示用户发布的所有作品列表。每条作品有编辑、删除按钮。删除操作使用POST方法而非GET——因为GET请求会被搜索引擎爬虫、浏览器预取机制触发如果GET /works/delete/1被误访问作品就没了。安全习惯要在项目早期养成。4.3 评论交流与热门推荐如何串联数据评论区的实现是文学交流平台交流二字的灵魂。我采用的结构很简单——前端表单提交评论后端记录数据同时更新作品的冗余comment_count字段。// controllers/commentController.js async function addComment(req, res) { const { work_id, content, parent_id } req.body; if (!content || content.trim().length 1) { return res.status(400).json({ error: 评论内容不能为空 }); } const commentId await commentModel.createComment({ work_id: work_id, user_id: req.session.user.id, parent_id: parent_id || 0, content: content.trim() }); // 同步更新作品评论计数 await workModel.incrementCommentCount(work_id); res.redirect(/works/${work_id}); }热门推荐这块我算法很简单也很实用按like_count comment_count * 2 浏览量权重算热度分取前十条作为本周热门。文学平台不是电商平台不需要太复杂的推荐算法用户看得有热度排序的依据就够了。真正做的时候热门榜放在首页右侧栏7天内发布的才有资格进榜时间权重通过SQL的WHERE created_at DATE_SUB(NOW(), INTERVAL 7 DAY)限制防止老文章霸榜。首页列表的SQL写法也值得说一下分页查询和作者信息要一次性查出SELECT w.*, u.username, u.avatar_url, c.name AS category_name FROM works w LEFT JOIN users u ON w.user_id u.id LEFT JOIN categories c ON w.category_id c.id WHERE w.status 1 ORDER BY w.created_at DESC LIMIT ? OFFSET ?用LEFT JOIN是因为联表查询在数据量小的时候没什么性能问题但能少写很多后续的业务代码。在分页参数上我用LIMIT ? OFFSET ?配合MySQL的索引这个查询在百万级数据量下依然能保持毫秒级响应——前提是created_at上有索引。5. 上线前必做的安全加固与性能优化5.1 常见安全漏洞与防护文学交流平台这类内容型网站常见的攻击面集中在XSS、SQL注入和CSRF三类。这里我逐一说下我的处理方式。XSS跨站脚本攻击用户在作品内容、评论里写入script标签如果不是做过滤每个访问页面的人都会执行这段脚本轻则弹窗骚扰重则窃取用户Session。我的做法是双保险存储时不做任何转义保持原文存入数据库。渲染时进行转义与过滤。对于用户提交的纯文本内容服务端渲染前用escapeHTML把所有、、等字符转成实体对于富文本内容用sanitize-html白名单过滤只允许p、br、img、blockquote这类安全的标签。在EJS模板里输出变量的写法要特别注意!-- 正确转义输出 -- %- escapeHTML(comment.content) % !-- 错误直接把原始内容输出 -- %- comment.content %% %在EJS里本身会有转义但如果你是输出的HTML片段就不要用%而是手写转义函数。这一行代码的区别决定了一个站是安全的还是全是洞。SQL注入我在前面所有查询里都用了占位符?这是最简单且最有效的防御方式。绝不拼SQL字符串尤其是用户输入的部分。比如用户搜索// 错误示范 const sql SELECT * FROM works WHERE title LIKE %${keyword}%; // 正确示范 const sql SELECT * FROM works WHERE title LIKE ?; const [rows] await pool.query(sql, [%${keyword}%]);mysql2的占位符会自动做参数转义如果谁直接字符串拼接攻击者输入 OR 11 --就把整张表拖出来了。CSRF跨站请求伪造想象一下用户在登录状态下访问了恶意站点恶意站点的页面上有一个表单自动提交到你的平台form actionhttp://yoursite.com/works/delete/123 methodPOST input typehidden nameconfirm valueyes scriptdocument.forms[0].submit()/script /form如果你没有CSRF防护这个请求会带上用户的cookie服务器以为是用户本人操作作品就被删掉了。我的方案是使用csurf中间件在每个表单里注入一个随机tokenapp.use(csrf()); app.use((req, res, next) { res.locals.csrfToken req.csrfToken(); next(); });模板里同步带上input typehidden name_csrf value% csrfToken %表单提交时中间件会校验token恶意站点拿不到这个随机token自然无法伪造合法请求。5.2 接口性能优化压测、缓存与Nginx部署本地开发调通功能只是第一步真正上线前我习惯用loadtest或autocannon压一下看看接口能扛住多少并发。简单的压测命令npx autocannon -c 100 -d 10 http://localhost:3000/works-c 100是100个并发连接-d 10是持续10秒。我测完发现首页接口的QPS每秒请求数大概在300左右对于一台2核4G的云服务器来说已经可以支撑一个中小型文学社区。但如果要准备的更充分我做了三件事静态资源交给Nginx处理。Express虽然能托管静态文件但性能远不如Nginx。CSS、JS、图片这些文件直接让Nginx返回Node.js只处理动态请求。开启Redis缓存热点数据。首页的本周热门榜其实十分钟才有必要更新一次每次都查一次数据库纯属浪费。我把热门榜的SQL结果直接缓存到Redis设置10分钟过期过期后第一次请求回源数据库并重建缓存。压缩传输内容。Express里启用compression中间件文本类内容HTML、JSON、JS体积直接缩小60%-70%网络传输时间大幅下降。部署结构上我用的是Nginx反向代理到Node.js进程Nginx监听80端口Node.js在3000端口运行server { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /var/www/literary-platform/public/uploads/; expires 30d; } }Node.js进程管理我用pm2pm2 start bin/www --name literary-platform -i 2-i 2表示启动两个实例一个实例挂掉另一个还能顶上。pm2自带的负载均衡和自动重启能让项目在夜间大数据量的情况下也稳定运行。5.3 内容安全与反垃圾机制文学交流平台作为UGC平台内容安全是绕不过去的一环。这里说的安全不光是合规问题还有用户体验层面的垃圾信息过滤。我的做法是分层处理敏感词过滤维护一份敏感词表发布作品和评论时用字符串扫描替换的方式命中直接拒绝或打码。发言频率限制每个用户每分钟最多发布一条评论超过就提示发言过于频繁。这个用Session在内存里记录最后发言时间就能实现不需要额外引入消息队列。举报与下架每篇作品、每条评论旁边放一个举报按钮。被举报超过三次自动进入待审核状态管理员后台能人工审核。审核下架的操作本质就是更新status字段为0前端所有查询默认带上status 1条件被下架的内容立刻在所有页面消失。这套机制从用户感知层面看很轻但在实际运营中能挡住绝大多数的垃圾灌水和违规内容。6. 从开发到上线的完整复盘写到这里整个项目的主体内容已经全部覆盖。最后我把这个项目从开始到上线踩过的坑、总结出的经验按主题整理一遍。这部分的每一句话都是我自己真实操作中得出的教训不是从文档里抄来的。6.1 开发顺序先通主干再填枝叶当时我最大的教训是一开始太执着于把完美方案想透才动手导致前三天都在写设计文档进度几乎为零。后来我调整成主干流程优先的思路——先把注册→登录→发布作品→看详情→评论这一条核心链路打通用最朴素的方式实现。这条链路通了以后项目已经能用了再从用户的视角去加分类、热门、搜索、个人中心这些功能每一步都是增量每加一个功能都看得到实际效果。给所有做类似项目的读者一个实操建议先做能用再做好用最后做好看。顺序反了你会在一个按钮圆角半径上抠半天而忽略了登录接口还没做防暴力破解。6.2 Express调试的几个实用技巧开发过程中用到了几个非常顺手的调试方式分享出来Morgan日志分析开启morgan后控制台会打印每个请求的方法、路径、状态码和响应时间。当首页变慢时第一件事就是看日志里哪个接口的响应时间异常。Postman保存接口的请求集合每次改完Controller用Postman自动跑一遍核心接口的请求集合10秒内能验证所有功能没被改坏。node-inspect断点调试不要只会console.log关键是debugger配合Chrome DevTools的Node.js调试模式看调用栈和变量值排查逻辑问题效率能翻好几倍。错误处理中间件的四参数写法Express的错误捕获必须写成function(err, req, res, next)四参数形式不然Express无法识别这是一个错误处理中间件异常会被吞掉。6.3 对未来扩展的一点建议这个平台目前的定位是交流但如果要往创作社区的方向走未来可以考虑的方向包括连载功能把作品按章节拆分存储支持连载更新这是从晒单式分享走向平台级写作的关键一步。关注与私信用户之间建立关注关系形成作者-读者的订阅链路内容触达会更精准。全文检索引擎目前用的是MySQL的LIKE %keyword%模糊匹配数据量过百万后性能会断崖式下跌到时候考虑接入Elasticsearch做全文检索。我个人在实际操作中的体会是Express这个框架的定位就是让你快速把想法变成能跑的服务它不替你决定架构也不限制你的业务想象力。文学交流平台这个项目用它来实现属于典型的在合适的场景使用合适的工具——框架的轻量与内容的轻盈恰好匹配。如果你正在规划类似的项目照着上面这套设计与实现的路径走一遍应该会比从零摸索省下不少弯路。
返回列表