Epic Stack 重定向实战:在 Express 层统一处理 HTTPS、尾部斜杠与 www 子域跳转
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
本指南基于 docs/redirects.md 展开,讲解 Epic Stack 如何在 React Router(Remix)框架接手请求之前,于 Express 中间件层统一完成三类重定向:HTTP 强制跳转 HTTPS、去除 URL 尾部斜杠、以及 www 子域与根域之间的互相跳转。读完本文,你将掌握在 Express 中实现高性能、SEO 友好的重定向的完整代码方案,并能直接在本仓库的 server/index.ts 中看到对应落地实现。
为什么在 Express 层处理重定向,而不是在 React Router 里
Epic Stack 的服务端架构是 Express 在前、React Router 在后:server/index.ts启动 Express 应用,所有中间件按注册顺序依次执行,最终才把请求交给server/app.ts中通过createRequestHandler创建的 React Router 请求处理器。
Express 中间件(压缩、Helmet、限流、重定向……) ↓ React Router 请求处理器(createRequestHandler,见 server/app.ts) ↓ 路由模块(app/routes/**)把重定向放在 Express 层而不是路由层,核心收益是性能:入站请求在到达 React Router 之前就会被 Express 拦截并直接返回响应,无需经过完整的路由匹配、加载器执行与渲染流程。Epic Stack 在 docs/redirects.md 中明确说明了这一设计初衷——"by redirecting earlier you improve performance"。
在源码 server/index.ts 中,这些重定向中间件确实被放置在中间件链的最前面(紧随app.set('trust proxy', true)之后),早于compression()压缩、Helmet 安全头、morgan 日志和限流中间件,确保重定向路径的开销降到最低。
前置条件:正确获取代理层转发的请求头
所有重定向逻辑都依赖一个共同的基础:正确识别客户端请求的协议与域名。由于 Epic Stack 部署在 Fly.io 这类反向代理之后,Express 拿到的直接连接来自代理而非客户端,因此必须信任并读取代理注入的转发头。
// 源码:server/index.ts const getHost = (req: { get: (key: string) => string | undefined }) => req.get('X-Forwarded-Host') ?? req.get('host') ?? '' // fly is our proxy app.set('trust proxy', true)app.set('trust proxy', true)告诉 Express 信任反向代理,使req.ip等字段基于代理头计算;getHost(req)优先读取X-Forwarded-Host(由 Fly 网关注入),取不到再回退到标准Host头,兜底为空字符串。
HTTP 强制跳转 HTTPS
Epic Stack 默认强制所有流量走 HTTPS,杜绝任何环节的明文请求被拦截的可能。实现方式是检查代理转发的协议头X-Forwarded-Proto,只要它是http就立即 302 跳转到对应的https地址。
文档中的核心实现:
app.use((req, res, next) => { const proto = req.get('X-Forwarded-Proto') const host = getHost(req) if (proto === 'http') { res.set('X-Forwarded-Proto', 'https') res.redirect(`https://${host}${req.originalUrl}`) return } next() })注意两个容易被忽略的细节:
X-Forwarded-Proto来自 Fly,文档明确指出 "we use Fly's request headers for determining when to redirect",因此本地开发(localhost)不会误触发该跳转;- 跳转目标拼接了
req.originalUrl,它包含完整的原始路径与查询字符串,保证跳转后用户停留在原页面而非首页。
源码中的一处增强:仅拦截 GET 请求
文档中的示例对任何方法都生效,而仓库实际实现 server/index.ts 加了一层守卫:
app.use((req, res, next) => { if (req.method !== 'GET') return next() const proto = req.get('X-Forwarded-Proto') const host = getHost(req) if (proto === 'http') { res.set('X-Forwarded-Proto', 'https') res.redirect(`https://${host}${req.originalUrl}`) return } next() })这是因为非 GET 请求(如表单 POST 提交)若被 302 重定向,浏览器会丢失原始请求体与方法,导致提交失败。只对 GET 请求做协议升级重定向,是一种更安全的做法。
平台层兜底:Fly 的 force_https
除了应用内中间件,Epic Stack 的 fly.toml 在平台层也做了 HTTPS 强制:
[[services.ports]] handlers = [ "http" ] port = 80 force_https = trueforce_https = true会让 Fly 网关在 80 端口直接拒绝或升级 HTTP 流量,与应用层中间件形成双重保障。实践中应用层逻辑仍然是必需的,因为并非所有部署环境都能配置平台级强制。
去除 URL 尾部斜杠(SEO 关键处理)
爬虫(如 Google)会把https://example.com/foo/与https://example.com/foo视为两个独立 URL,从而产生重复内容问题。Epic Stack 通过 Express 中间件自动将带尾部斜杠的 URL 301 跳转到无斜杠版本。
文档中的核心实现:
app.use((req, res, next) => { if (req.path.endsWith('/') && req.path.length > 1) { const query = req.url.slice(req.path.length) const safepath = req.path.slice(0, -1).replace(/\/+/g, '/') res.redirect(301, safepath + query) } else { next() } })逐行拆解这段逻辑:
req.path.endsWith('/') && req.path.length > 1:排除根路径/本身(根路径的斜杠是合法的,不能去掉);const query = req.url.slice(req.path.length):从完整 URL 中截取查询字符串部分(?a=b&c=d),因为req.url是"路径 + 查询串",而req.path只有路径;const safepath = req.path.slice(0, -1).replace(/\/+/g, '/'):去掉末尾斜杠,并把连续多个斜杠(///)折叠为单个斜杠;res.redirect(301, safepath + query):使用301 永久重定向,把 SEO 权重完整传递给新地址。
例如请求https://example.com/foo/?utm_source=x,req.path为/foo/,query为?utm_source=x,最终 301 到https://example.com/foo?utm_source=x,查询参数完整保留。
源码中的实际差异:302 而非 301
仓库实际实现 server/index.ts 与文档示例有一处状态码差异——生产代码使用的是 302:
// no ending slashes for SEO reasons // https://github.com/epicweb-dev/epic-stack/discussions/108 app.get(/.*/, (req, res, next) => { if (req.path.endsWith('/') && req.path.length > 1) { const query = req.url.slice(req.path.length) const safepath = req.path.slice(0, -1).replace(/\/+/g, '/') res.redirect(302, safepath + query) } else { next() } })另外两点差异值得注意:
- 注册方式不同:生产代码使用
app.get(/.*/, ...)而不是app.use(...),效果上只对 GET 请求生效,避免 POST 表单因重定向丢失请求体; - 状态码为 302:在"先验证跳转正确、再切换 301"的发布策略下更稳妥。如果你想追求最佳 SEO 效果,可以参照 docs/redirects.md 的示例改用
301,但要确保所有内部链接已同步去除尾部斜杠,避免永久跳转链。
www 子域与根域之间的跳转
Epic Stack 同时支持两种域名规范化方向:根域跳转到 www或www 跳转到根域,二选一即可,避免同一站点在多个域名下被索引为不同站点。
为什么不能在 DNS 层做
文档特别指出:DNS 级别的重定向在 Fly.io 上不可用(Fly 平台不支持 CNAME flattening 之类的隐式跳转),因此推荐在应用代码里实现。前提是你需要为两个域名分别注册 SSL 证书——在 Fly 后台选中应用后进入 "Certificates" 面板,或通过fly certs add类命令在终端注册,之后 Fly 会同时接受来自两个域名的流量,应用层再按规则跳转到首选域名。
方向一:根域 → www
app.use((req, res, next) => { const host = getHost(req) if (!host.startsWith('www.')) { return res.redirect(301, `https://www.${host}${req.url}`) } else { next() } })当Host不是以www.开头时,301 永久重定向到https://www.+ 原域名 + 原路径(req.url保留查询字符串)。若访问的是https://example.com/about?tab=1,将跳到https://www.example.com/about?tab=1。
方向二:www → 根域
app.use((req, res, next) => { const host = getHost(req) if (host.startsWith('www.')) { return res.redirect(301, `https://${host.slice(4)}${req.url}`) } else { next() } })host.slice(4)恰好去掉www.这 4 个字符,其余逻辑与方向一对称。两个方向只能启用其一,具体选择取决于你的品牌域名偏好(如epicstack.dev官方站点即采用 www 形式,见测试工具 tests/utils.ts 中的BASE_URL = 'https://www.epicstack.dev')。
注意:这三个重定向中间件(HTTPS、尾部斜杠、www)在实际部署时是按顺序叠加的,例如请求
http://example.com/foo/可能依次经历协议升级、去斜杠、域名规范化多次跳转。若想减少跳转次数,可以将判断合并进单一中间件。
在测试中验证重定向行为
Epic Stack 的测试体系为重定向断言提供了专用匹配器toHaveRedirect,定义于 tests/setup/custom-matchers.ts。它做三件事:
- 检查响应头存在
location,且状态码落在 3xx 区间; - 支持传入期望的跳转地址做精确断言;
- 对
location与期望值做规范化比较(urlsMatch:忽略 origin、排序后的 query 参数与 hash 差异),避免因端口、参数顺序导致误报。
例如断言"某请求应 302 跳转到登录页",可以这样写:
const response = await request(...) expect(response).toHaveRedirect('/login')配合BASE_URL(tests/utils.ts)与实际用例(如 tests/e2e/settings-profile.test.ts 中保存头像后的跳转断言),可以在端到端层面守住重定向行为,防止中间件顺序或状态码调整造成回归。
小结
Epic Stack 在 Express 层完成的三类重定向,体现了一个值得借鉴的服务端设计原则:能在入口层解决的问题,不要拖到应用层。通过 docs/redirects.md 与 server/index.ts 的对照,可以看到:
- HTTP→HTTPS 通过
X-Forwarded-Proto头判断,仅对 GET 请求生效,配合 Fly 的force_https双保险; - 去除尾部斜杠使用折叠斜杠 + 保留查询串的跳转,SEO 场景应使用 301;
- www 域规范化因 Fly 不支持 DNS 级跳转而必须在代码中完成,需先为两个域名注册证书;
- 所有重定向都发生在 React Router 接手之前,性能开销最小,且可通过
toHaveRedirect匹配器在测试中锁定行为。
直接复制文档中的三段 Express 中间件,即可在任意 Express + React Router 应用中复现这套完整、SEO 友好的重定向方案。
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考