做了个匿名聊天站,我把 7 个前端坑踩了一遍

先说结果:站上线 37 天,能跑,但中间有三次我以为"这么简单的东西不可能出错",结果全错在同一个地方------我信了本地开发环境的判断。

技术上没什么花活。Vite + React 写 SPA,构建的时候把每篇文章预渲染成静态 HTML,丢到 nginx 后面。就是这套很普通的组合,让我踩了下面这些坑。

第一个坑:所有不存在的 URL 都返回 200

这个坑最要命,而且是我自己写出来的。

SPA 的 nginx 配置是抄来的,长这样:

nginx 复制代码
location / {
    try_files $uri $uri/ /index.html;
}

看起来天经地义:路由交给前端嘛。但它带来的后果是------/asdfgh、/letter/xxx、任何乱敲的路径,服务器都返回 200 和一个完整的首页 HTML。

对用户没影响,对搜索引擎是灾难。Google 抓到这些页面,会归进「已抓取,但未编入索引」,白烧抓取预算;更糟的是垃圾页面和真页面在同一个响应码下,等于告诉爬虫"我这儿有无限多个页面"。

我是翻了后台的索引报告才发现这个的。修法是给 SPA 路由加白名单:

nginx 复制代码
location ~ ^/(app|plaza|chat|mine|inbox|write|draw|match|coin|letter|block|doc)(/|$) {
    try_files $uri /index.html;
}

location / {
    return 404;
}

error_page 404 /404.html;

改完拿 26 个真实路由和 5 个垃圾路径逐个 curl,才敢确认。

第二个坑:标题里的双引号,吃掉了整页的 meta

Bing 后台一直提示我「Meta descriptions on many pages are too short」。我以为是描述写得太敷衍,就写了一版更长的,重新构建,再提交。

没变化。

于是我把 40 个页面全抓下来,用 HTML 解析器(不是正则)读 description 的值。结果有一页是空字符串。

原因在这篇的标题:《"树洞"这个说法从哪来》。

预渲染脚本干的事很简单:拿标题和描述,替换模板里的占位符。

js 复制代码
html = html.replace(
  /(<meta\s+name="description"\s+content=")[\s\S]*?("\s*\/>)/,
  `$1${description}$2`
);

description 里带一个英文双引号,插进去之后 content=" 就提前闭合了,浏览器读到的值是空串。剩下的半截变成了这行 meta 上的垃圾属性------页面照常渲染,肉眼看不出任何异常。

修法是所有属性值统一转义:

js 复制代码
function escAttr(s) {
  return String(s ?? '')
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

顺便说一句,这个坑我踩了一整天才反应过来。因为"描述太短"这个提示太具体了,具体到让人不去怀疑别的地方。

第三个坑:og:type 写死成 website

模板里 og:type 一直是 website。博客文章应该是 article,还要配 article:published_time。不改的话,Google 和 Bing 都按普通页面处理,不当作时效性内容。

这个改起来一分钟,但前提是你知道要改。

第四个坑:原生 ESM 不做扩展名解析

构建脚本是个 .mjs,直接 import 站点数据:

js 复制代码
import { BLOG_POSTS } from './src/pages/content/blogPosts';

在 Vite 里这么写完全没问题,esbuild 会帮你补 .js。但同一个文件被 node scripts/generate-sitemap.mjs 跑的时候,直接报 ERR_MODULE_NOT_FOUND。

因为原生 ESM 不做扩展名解析,Vite 会做的事 node 不会。

加上 .js 就好:

js 复制代码
import { BLOG_POSTS } from './src/pages/content/blogPosts.js';

我的教训是:构建脚本里 import 的东西,一律写全后缀。你不知道它哪天会被 node 直接跑一次。

第五个坑:rsync --delete 删掉了三个目录

这个是纯操作失误,但破坏力最大。

部署命令我图省事写成了:

bash 复制代码
rsync -a --delete dist/ /www/wwwroot/site/

--delete 的意思是"目标目录里多出来的东西也删掉"。听起来合理------保持和构建产物一致。但我的 dist/ 里不会有后台管理面板、不会有 APK 下载目录、不会有 .well-known/(那个是后来手工放的)。

于是部署完成的瞬间,/admin/、/download/、/.well-known/ 全没了,包括一个 1.34MB 的安卓包。

好在部署前我习惯先打个 tar。从备份里单独解出这几个目录恢复,逐个 curl 确认 200,花了十分钟。

现在的部署流程固定成:

bash 复制代码
rsync -a --dry-run dist/ /www/wwwroot/site/   # 先看要动什么
rsync -a dist/ /www/wwwroot/site/             # 不加 --delete

--dry-run 这一步不能省。 它输出的那一屏,是唯一能在动手前看见"会删什么"的地方。

第六个坑:String.replace 的 $ 序列

接着第二个坑说。转义做完之后我还留了个隐患:替换字符串里用了 $1、$2 这种分组引用。

js 复制代码
html.replace(re, `$1${value}$2`);

如果 value 里恰好出现 $&、$'、````` 这类特殊序列,`replace` 会把它们当成分组引用展开,内容当场变形。概率很低,但我的描述是从文章正文生成的,谁都说不准哪天冒出个 ``。

换成函数式替换,彻底没这个问题:

js 复制代码
html.replace(re, (_m, a, b) => a + value + b);

这种改法没什么技术含量,纯粹是吃过一次亏之后的老实。

第七个坑:路由白名单会漏

第一个坑的修法留了个尾巴:白名单是正则,新增路由就得同步改 nginx。

我加 /letter/:id 的时候忘了。表现是本地一切正常,线上直接 404。

这种事没有优雅解法,我的土办法是让路由清单只有一份来源------把 SPA 路由集中写在 NewApp.jsx 里,改完顺手 grep 一遍 nginx 配置对比。丑,但有效。

站现在长这样

站现在 55 个页面,全部预渲染,sitemap 55 条。

但有个我没解决的问题:55 个页面一张图都没有。不是忘了,是我不知道配什么图------这类内容站配图很容易沦为占位图,反而拉低观感。如果你有做过类似站点的配图方案,很想听听。

上面这些坑里,第五个(rsync)是我最不想再犯的。前端问题可以慢慢调,一个 --delete 能让你半小时的活儿变成排查两小时。

相关推荐
Momo__2 小时前
你写的 import 重试,从来没生效过
前端·javascript·typescript
鶴哥只手遮天2 小时前
Angular 22 源码级深度解析-第 13 章 依赖注入(一):Injector 抽象、`R3Injector` 与 bloom filter
前端·开源·angular.js
青烟小生2 小时前
前端 AI Agent 编排引擎:从 ReAct 循环到生成式 UI(完整版)
前端
MetWeave2 小时前
一朵玫瑰,装下一年的风(《风不是一个数》系列第 5 篇
前端·数据可视化
UNIHHIOT2 小时前
开源一套工业物联网平台:驱动崩了不影响别的协议,64MB 堆跑边缘网关
前端·后端
deli0072 小时前
秒杀超卖实测:1000 个并发请求下 SQL 加锁前后差多少
前端
徐小夕2 小时前
JitKnow知识库4.8发布:本体+知识图谱检索引擎,让AI回答不再“一本正经地胡说八道”
前端·vue.js·github
智迁2 小时前
MySQL 到 Kingbase 实时同步:从 Docker 部署到增删改验证
前端·后端
tryCbest2 小时前
Vue3——第 11 篇:组合式 API(上)——setup 与响应式 API 深度解析
前端·javascript·vue.js