uniapp-history路由深度指南

uni-app H5 路由切 history 模式:编译时特化、三层 Base 与动态路径部署

把 H5 路由从 hash 切到 history,表面上是一行配置的事。但这个改动背后牵扯三个层面的机制:浏览器的导航模型、服务器的部署契约、构建工具的编译策略。任何一个层面理解有偏差,产出的就是"本地正常、线上白屏"或"配置写 A、产物跑 B"的缝合包。

这篇文章把这三层机制讲透,最后给一个进阶实践:一份产物部署到运行时才能确定的路径

一、先讲本质:hash 和 history 的差异到底在哪

SPA 路由的一切差异,源于浏览器导航模型中的一个规定:

URL 的 fragment(# 及之后的部分)不会出现在 HTTP 请求中。

由此推导出两种路由模式的全部行为差异:

导航场景 hash 模式 history 模式
应用内跳转 改 fragment,pushState,不发请求 改 path,pushState,不发请求
用户刷新 / 直接输入 URL 请求的永远是 /index.html 把完整 path 发给服务器
服务器视角 永远只有一个真实文档 无穷多个"看起来存在"的路径
路由状态的归属 客户端私有 服务器可见的公共状态

切换到 history,本质上是把路由状态从"客户端私有"提升为"服务器可见"。收益是真实的:URL 即状态,分享、埋点、埋深链接都更干净;OAuth 回跳不再有 fragment 被截断的经典问题。代价也来自同一个地方:服务器必须参与兜底

二、history 模式的部署契约

契约一:SPA fallback,注意 try_files 的语义

所有未命中真实静态文件的路径,都要返回 index.html 让前端路由接管。nginx 的标准写法:

nginx 复制代码
location /app/ {
    alias /path/to/deployed/;          # 文件系统路径配在这里
    try_files $uri $uri/ /app/index.html;  # 最后一项是回退目标
}

这里有个高频配置错误值得单独强调:try_files 最后一项是 URI,不是文件系统路径 。它触发的是一次内部重定向------nginx 会拿着这个 URI 重新匹配 location。如果写成 try_files $uri /build/app/index.html(服务器上的真实文件路径),没有任何 location 匹配它,规则等于没配。

验证方式永远是状态码矩阵,而不是"页面能不能打开":

powershell 复制代码
curl.exe -s -o NUL -w "%{http_code}`n" https://your-domain.com/app/            # 应 200
curl.exe -s -o NUL -w "%{http_code}`n" https://your-domain.com/app/pages/me    # 应 200(fallback 生效的铁证)

另外提醒一个容易误判的点:hash URL 能访问不构成任何证据/app/index.html#/pages/me 这个请求发给服务器的只有 /app/index.html------一个真实存在的文件,跟 fallback 没有任何关系。

契约二:缓存策略是发版正确性问题,不只是性能问题

  • index.html:必须 Cache-Control: no-cache。它引用的是带 hash 的资源文件名,一旦被缓存,发版后老用户会拿着旧 HTML 去请求已被覆盖删除的旧资源------白屏
  • assets/*(文件名带 hash):放心 immutable 长缓存

三、SPA 的 Base 其实有三重身份

部署出问题时,多数人对"base"的理解是混成一团的。实际上一个 SPA 有三个互相独立的 base:

身份 控制什么 在 uni-app 里由谁决定
文档 Base 用户访问的 URL 前缀,如 /app/ 服务器部署位置决定,前端无法控制
资源 Base index.html 里 JS/CSS 的引用前缀,可指向独立 CDN vite 的 base(生产可用 CDN 绝对地址)
路由 Base createWebHistory(base) 的参数,路由匹配的起点 manifesth5.router.base

三条约束关系:

  1. 资源 Base 可以与文档 Base 解耦------指向 CDN 绝对地址时,页面部署在哪个路径都不影响资源加载。这是很多部署方案的地基
  2. 路由 Base 必须等于文档 Base------vue-router 会拿 URL 剥掉 base 后去匹配路由表,两者不一致的表现是:页面能加载(资源 200)但路由失配、白屏
  3. 三者由两套独立配置控制(vite base 与 router base),最常见的错误是只改了一个
ts 复制代码
// 资源 Base:vite.config.ts → index.html 里的引用前缀
base: VITE_H5_CDN_BASE || VITE_APP_PUBLIC_BASE

// 路由 Base:manifest.config.ts → 编译注入运行时
'h5': {
  router: { base: VITE_APP_PUBLIC_BASE, mode: 'history' }
}

把这三重身份分开,"部署到子路径后白屏"这类问题的排查路径就清晰了:先看资源请求是否 404(资源 Base 错),再看路由是否匹配(路由 Base 错),最后才轮到服务器 fallback。

四、uni-app 的路由模式是编译时特化,不是运行时配置

这是整篇文章最值得记住的机制。

uni-app H5 产物里,运行时配置对象 __uniConfig.router.mode 看起来记录了路由模式------但路由初始化根本不读它 。读源码(@dcloudio/uni-h5):

js 复制代码
// uni-h5 运行时
function initHistory() {
  let { routerBase } = __uniConfig.router;
  if (routerBase === "/") { routerBase = ""; }
  const history2 = __UNI_FEATURE_ROUTER_MODE__ === "history"
    ? createWebHistory(routerBase)
    : createWebHashHistory(routerBase);
}

路由模式由 __UNI_FEATURE_ROUTER_MODE__ 决定,而它是个编译时常量 。生成逻辑在 @dcloudio/uni-cli-shared

js 复制代码
function initManifestFeature({ manifestJson, ... }) {
    const features = { routerMode: '"hash"', /* ... */ };
    const webManifest = manifestJson.web || manifestJson.h5;
    if (webManifest?.router?.mode === 'history') {
        features.routerMode = '"history"';   // 编译为字面量
    }
    return features;
}

链路是:manifest.json → 特性分析 → vite define 注入 → 编译期字符串替换 → minify 死代码消除(常量比较,一个分支直接消失)。

最终产物里,三元表达式整个没有了,选中的构造器被直接焊死:

js 复制代码
// history 产物(minify 后)
const history2 = createWebHistory(routerBase);
// hash 产物
const history2 = createWebHashHistory(routerBase);

为什么框架要这样设计

这叫编译时特化(compile-time specialization) :同一个框架源码,按项目配置裁剪出只含所需实现的产物。收益是确定的------vue-router 的两套 history 实现可以被 tree-shake 掉一份。同类设计在业界很常见:Vue 的 __VUE_OPTIONS_API__、React 的 __DEV__、Svelte 的各种编译开关,都是同一个思路。

对开发者的三条推论

  1. 改配置 = 改代码 。路由模式变更必须触发完整重新编译。任何构建缓存链路上的断层(CI 缓存了中间产物、manifest 生成与构建被拆散)都会造成"配置与产物不一致"------而且这种不一致极难发现,因为产物里的 __uniConfig.router.mode 元信息还是对的
  2. 验证产物要看代码,不要看配置。判断一个包到底是哪种模式,搜构造器:
powershell 复制代码
$js = Get-Item .\dist\build\h5\assets\index-*.js | Select-Object -First 1
Get-Content $js.FullName -Raw | Select-String 'initHistory|createWebHashHistory' -AllMatches
# initHistory 内为 createWebHistory 且 createWebHashHistory 出现 0 次 → 纯 history 产物
  1. 配置驱动编译的框架,调试行为不一致时优先怀疑编译链路。"源码里明明有分支,产物里怎么没有了"------先想 define,再想 minify

五、进阶实践:一份产物,部署到动态路径

需求与约束分析

需求:同一份 H5 产物,要部署到运行时才能确定的路径,如 /share/123/prod//share/366/prod/......构建时不可能预知所有前缀,也不可能每个路径构建一次。

用第三节的框架分析,三个 base 里:

  • 资源 Base:已指向固定 CDN,与文档路径天然解耦------不用动
  • 文档 Base:服务器决定------不受前端控制
  • 路由 Base :唯一需要动态化的,且它是运行时 从全局对象读取的(initHistorylet { routerBase } = __uniConfig.router

问题从"不可能"变成了"找到改写 __uniConfig 的时机窗口"。

时机窗口分析

产物加载的时序:

arduino 复制代码
① index.html 内联同步脚本:定义 __uniConfig(含编译期注入的 routerBase)
        ↓
② main.ts(module 脚天然 defer,晚于①执行)
        ↓
③ uni 初始化路由:initHistory 读 __uniConfig.router.routerBase

②和③之间就是窗口:在 main.ts 模块体顶部(所有 import 之后、createApp 之前)按当前 URL 改写即可,一定先于路由初始化。

实现

ts 复制代码
// src/main.ts 顶部
// #ifdef H5
;(function adaptDynamicBase() {
  // 部署在 /share/{id}/prod/ 等动态路径时,按当前 URL 改写路由 Base
  const m = location.pathname.match(/^(\/share\/\d+\/prod)\//)
  const cfg = (window as any).__uniConfig
  if (m && cfg?.router) {
    const base = `${m[1]}/`
    cfg.router.base = base
    cfg.router.routerBase = base
  }
})()
// #endif

设计要点:

  • 正则不匹配时不改写------回落到构建时的兜底值,本地开发和既有部署零影响
  • 正则按实际路径形态收紧/扩展
  • 应用内后续所有跳转(uni.navigateTo 等)都基于同一个 router 实例,base 改写对全链路生效

服务器侧:正则 location 一次配齐所有部署点

nginx 复制代码
location ~ ^(/share/\d+/prod)/ {
    root /path/to/deploy_root;
    try_files $uri $uri/ $1/index.html;   # $1 = 正则捕获的动态前缀,逐点回退
}

风险声明

__uniConfig 是 uni-h5 的内部约定(自 vue2 时代就存在,非常稳定),但不是公开 API 。框架升级时需要回归:构建后确认产物中 initHistory 仍从 __uniConfig.router 解构 routerBase。接受这个约束,方案就是稳的;不能接受,退回构建时注入方案(每个路径构建一次,cross-env VITE_APP_PUBLIC_BASE=/share/123/prod/ uni build),零 hack 但产物不可复用。

六、总结:history 部署的完整心智模型

lua 复制代码
浏览器(导航模型)  → fragment 不发请求 / path 发请求 → 决定服务器是否需要参与
服务器(部署契约)  → fallback 回退 URI / 缓存二分法    → 决定刷新和发版是否正确
构建(三层 Base)  → 文档 / 资源 / 路由 各自独立        → 决定部署到哪里不出错
框架(编译特化)   → 路由模式是编译时常量              → 决定配置如何真正生效

最后给一份生产级 checklist,三个阶段各 30 秒:

构建后(产物自检)

powershell 复制代码
# 路由模式:initHistory 内应是 createWebHistory,createWebHashHistory 出现 0 次
$js = Get-Item .\dist\build\h5\assets\index-*.js | Select-Object -First 1
Get-Content $js.FullName -Raw | Select-String 'createWebHistory|createWebHashHistory' -AllMatches

# 资源前缀:index.html 里的引用应符合预期(CDN 或部署路径)
Select-String -Path .\dist\build\h5\index.html -Pattern 'src="([^"]+)"' | Select-Object -First 5

部署后(服务验证)

powershell 复制代码
# fallback 生效(全部应 200,路由路径 200 是铁证)
curl.exe -s -o NUL -w "%{http_code}`n" https://your-domain.com/app/
curl.exe -s -o NUL -w "%{http_code}`n" https://your-domain.com/app/pages/xxx

浏览器验证(无痕窗口)

打开入口 URL,确认两件事:地址栏不会 自动补 #/(它是 hash 产物的指纹);点切换页面,地址栏是纯路径 /app/pages/xxx

三个阶段都过,history 部署才算真正闭环。

相关推荐
suliqiang3 小时前
【前端技术】 Web 前端技术36年 演进全景图
信息可视化·微信小程序·小程序·前端框架·uni-app·人机交互·xcode
梦曦i21 小时前
uni-router v0.2.0重磅发布:全链路拦截+重复导航优化
前端·uni-app
EatFan21 小时前
【实战经验】uni-app使用 SSE 踩坑,EventSource不支持怎么办?
android·后端·ios·uni-app
凡泰AI1 天前
如何为政务 APP 搭建开放平台,实现多部门、第三方供应商标准化入驻与统一管理
android·大数据·小程序·uni-app·app·政务
梦曦i2 天前
@meng-xi/uni-router 未来展望:夯实基础、深化体验、探索前沿
前端·uni-app
2501_916007472 天前
使用Apple Dashboards显示和自定iOS应用性能指标与数据可视化指南
android·ios·小程序·https·uni-app·iphone·webview
爱折腾的编程老炮2 天前
潮玩小程序订单模型-一张表装下所有玩法
spring boot·小程序·uni-app
两个人的幸福online2 天前
UniApp 对接 IM 即时通讯全攻略(下篇)
uni-app
2501_915106322 天前
Flutter iOS混淆打包详细教程与步骤
android·flutter·ios·小程序·uni-app·iphone·webview