index.html引发的三个真机调试事故:构建、部署与缓存全解析

发布时间:2026/10/6 5:50:54
index.html引发的三个真机调试事故:构建、部署与缓存全解析 花了一个下午准备给朋友做一次真机演示结果没死在业务逻辑上而是被三个看起来毫无关联的报错轮番教育。最讽刺的是三个坑绕来绕去最后全都汇成同一条线索——都是 index.html 的问题。第一个坑发生在打包阶段Rollup 直接报 cant resolve entry module第二个坑出现在跟 Hexo 相关的静态站上public 目录里愣是没有 index.html第三个坑更玄真机上刷新一百遍都是旧页面PC 上却一切正常。如果你也想把项目从 PC 浏览器搬到真机上或者部署完静态站后遇到过首页打不开、页面内容永远不更新的情况这篇文章应该能帮你省掉大量排查时间。我会把每个坑的报错信息、根因分析和最终改法都摊开讲明白里面有不少是常规文档里不会写的细节。1. 为什么所有矛头都指向 index.html可能有不少人觉得 index.html 就是个页面壳子里面无非是一段div idapp和几个 script 标签改来改去也就那样。但实际上它在前端项目的生命周期里扮演了三个不同角色。第一它是构建入口。Vite 这类工具默认拿着项目根目录的 index.html 作为 Rollup 的 input整个 bundle 的生成都是从解析这个文件开始的。也就是说构建能不能过、产物正不正确取决于这个文件在不在它应该在的位置。这个位置一旦错位Rollup 连报错信息都给你的明明白白但恰恰是这种过于直白的报错反而容易让人忽略底层的原因。第二它是站点着陆页。对于 Hexo 这类静态站点生成器首页的 HTML 不是随手放在 public 里的而是由 source 目录下的源码文件转换出来的。如果转换链条出了问题public 里就没有 index.html站点根路径自然 404。这个坑很有意思因为其他子页面都正常唯独根路径挂了如果只盯着部署配置看可能一晚上都找不出原因。第三它是客户端缓存的第一道关卡。浏览器、WebView、Service Worker无论哪一层缓存了旧的 index.html真机上你看到的永远是上一个时代的页面哪怕你重新构建了一百遍。这三种角色分别对应一次真实事故而这三个事故恰好都发生在我准备把项目拿到真机上给人看的那一天。所以与其说是 index.html 坑我不如说真机调试是面照妖镜把平时被 dev server 粉饰太平的问题全部照了出来。下面一个一个说每个坑我都按报错现场、底层机制、排查路线、最终改法的顺序展开。2. 坑一Rollup 端点解析失败Build 直接红牌2.1 报错现场还原当时我刚做完一轮功能调整准备在真机上看看效果惯例先跑npm run build。结果终端直接给我甩了一屏刺眼的红字could not resolve entry module index.html. error during build: rolluperror报错出处是 Rollup。说实话第一眼看到我脑子里是有点懵的因为我确定项目里明明有 index.html而且就在根目录下躺着这怎么可能解析不到这种矛盾感是排查的第一道坎——项目里有这个文件但工具说找不到那一定是我以为的位置和工具理解的位置不是同一个。2.2 这个报错的产生机制要理解这个报错得先搞清楚 Vite 和 Rollup 的分工。开发阶段 Vite 用自己的 dev server 给你提供页面入口路径找不找得到它都有自己的容错逻辑所以平时npm run dev一直正常哪怕文件大小写有点小问题也不会发作。但生产构建阶段交给 RollupRollup 是严格按入口配置来工作的它不存在猜一猜这种宽容机制入口写的是什么它就按什么去找找不到就直接失败。具体来说Vite 构建时会有两种入口场景。一种是完全默认也就是项目根目录下的 index.html 自动成为入口不需要写任何配置。这种情况最容易踩的坑是文件位置不对——比如你顺手把 index.html 挪到了 public 目录里Vite 在根目录找不到这个文件Rollup 自然就报出 could not resolve entry module。另一种是你在 vite.config.js 里手动指定了build.rollupOptions.input。这种情况常见的坑有两个一个是路径写错了比如指向了不存在的目录另一个是路径分隔符或大小写问题。我这次踩的就是大小写问题。2.3 排查与解决路线我当时先做了三件事。第一步确认文件到底在哪。在项目根目录执行find . -name index.html -not -path */node_modules/*结果有点意外index.html 确实在根目录但是文件名是Index.html。对就是首字母大写了。Windows 上文件系统默认不区分大小写Visual Studio Code 里你看不出任何异样开发预览也一直是好的但项目 clone 到 mac 上再打包macOS 的文件系统默认区分大小写Rollup 严格去找小写index.html自然找不到。第二步把文件重命名回小写并处理 Git 的大小写追踪mv Index.html index.html git config core.ignorecase false # 防止 Git 再次忽略大小写差异第三步检查 vite.config.js 里的入口配置。如果你手动配过 input最稳妥的写法是把路径写清楚尽量用绝对路径import path from node:path export default defineConfig({ build: { rollupOptions: { input: { main: path.resolve(__dirname, index.html) } } } })再跑一次npm run build问题消失。整个过程不到五分钟但如果你不知道大小写会是构建期的问题这五分钟可能变成五个小时。2.4 这个坑的注意事项这里我想多说两句因为这个坑太容易复发了。Windows 确实是大小写坑的重灾区。NTFS 默认大小写不敏感你从 Mac/Linux 的仓库里 clone 下来一个项目本地打包一切正常等到了 Linux 的 CI 服务器上构建反而报错。所以一定记得用git config core.ignorecase false把 Git 的大小写检查打开并在改动文件名后提交一次。在 CI 服务器或者任何 Linux 环境里index.html和Index.html就是两个完全不同的文件。如果你做的是多页面应用MPA入口配置从字符串改成对象会更稳一个页面一个入口别把所有页面入口都塞在同一个路径模式里去解析。入口配置对象化之后Rollup 报错时会直接告诉你哪个入口出错排查成本低很多。这个报错还有一种变体值得提一下指定了入口但 index.html 引用的资源路径写成了绝对地址比如/src/main.js本地 dev server 能通过重写规则找到但生产构建时 Rollup 去解析这个路径也一样会失败。所以检查入口配置的时候顺带看一眼 index.html 里 script 标签引用的路径是不是以/开头如果是改成相对路径./src/main.js或者检查base配置是否和你的部署路径一致。3. 坑二Hexo public 目录里没有 index.html3.1 现象首页 404其余页面正常第二个坑发生在另一个项目上。我拿 Hexo 搭了一个个人博客本地生成、本地预览一切正常感觉可以发给朋友手机看看了。于是执行hexo clean hexo generate打开 public 目录想看产物时我发现一个奇怪的现象archives、about、tags这些目录都在里面的index.html也都有唯独最顶层没有index.html。我当时没太在意直接把整个 public 目录丢到了服务器上。结果就是朋友在真机上打开首页域名直接 404点进其他页面又一切正常。这个部分可用的现象最容易迷惑人我一开始甚至怀疑是服务器默认文档配置的问题排除了大半天。3.2 根因定位先解释一下 Hexo 的生成机制。Hexo 不是一个把 source 目录原样拷贝到 public的工具它的核心是source 下的 Markdown 文件经过渲染器转换生成同名目录下的同名 HTML。注意这个对应关系——首页对应的文件应该是source/index.md渲染后才会生成public/index.html。但我的 source 目录下只有两样东西一个是_posts文件夹一个是README.md。README.md会被转换成public/README.html它不会变成根目录的index.html。这就是为什么其他子页面都有偏偏第一屏没有。还有一个容易被忽视的次因即使你在 source 下创建了index.md如果它的 front-matter 写得不对同样生成不出index.html。比如--- type: tags ---带这种 front-matter 的页面会被分配到 tags 布局下渲染结果会放到别的位置而不是根目录的index.html。又或者你在_config.yml里把source_dir改成了别的目录名但你的 Markdown 文件还放在原来的 source 里那 Hexo 同样会视而不见。3.3 实操修复我的改法很简单在 source 下新建一个index.md--- title: 首页 date: 2025-01-15 10:00:00 --- 欢迎来到我的个人博客这里是我记录技术笔记的地方。然后重新生成hexo clean hexo generate ls public/index.html看到public/index.html出现之后重新部署朋友真机上再打开域名首页就能正常显示了。如果你的 index.md 已经存在但就是生成不出来建议按顺序查三样东西front-matter 是否完整至少要有title_config.yml里的source_dir和public_dir是否正确主题是否有 index 布局对应的模板文件比如主题的layout/index.ejs或layout/index.pug。3.4 避坑心得这个问题看起来简单但它背后有一个原则值得记住静态站点生成器不会帮你创造任何页面只会把你给的原料转换出来。你想让根路径有页面就必须提供根路径对应的原料文件。还有一点就是hexo clean很重要。我见过太多人改了源码之后直接hexo generate结果 public 目录里残留了旧的目录结构根因没暴露反而被旧文件掩盖了问题。养成习惯改了配置或目录结构之后先 clean 再 generate否则排查会很痛苦。4. 坑三真机上刷新一万次看到的都是旧页面4.1 现场还原这是最让我抓狂的一个坑因为它只发生在真机上PC 上怎么刷新都是最新的。背景是这样Vite 项目打包完我用局域网的 IP 加端口在真机上访问比如http://192.168.1.105:8080。首次打开是旧页面我心想没关系可能是刚才构建完还没部署完于是把新的构建产物同步过去然后在真机上手动刷新。结果刷新了七八次页面纹丝不动还是上个版本的界面。PC 浏览器上访问同一个地址却是最新的。最后我拿无痕模式打开又是最新的了。这一刻我基本确定了缓存而且是多层缓存叠加。4.2 三层缓存拆解第一层是浏览器 HTTP 缓存。服务器返回 HTML 时带了响应头可能是Cache-Control: max-age3600也可能是ETag、Last-Modified这些做条件请求的字段。浏览器在有效期内根本不会重新请求 index.html直接用本地缓存渲染。很多静态服务器默认会给 HTML 设置缓存头这就是第一个背锅对象。第二层是真机 WebView 的缓存。Android WebView 默认的缓存模式是LOAD_DEFAULT强缓存未过期时会直接用缓存。iOS 的 WKWebView 对资源的缓存策略更激进有时候即使 HTML 响应没有强缓存头也会根据历史请求自动做内存级缓存。PC 浏览器因为使用习惯和缓存占用情况反而不太容易长时间保留旧版本。第三层是 Service Worker。如果项目曾经注册过一个 Service Worker那么它可能已经接管了页面的请求。即使你在服务器上部署了新的 index.htmlService Worker 也会拦截请求直接从 CacheStorage 里把旧 HTML 返回给你。这是最隐蔽的一层因为你在 Chrome DevTools 的 Network 面板里看到的状态可能是from ServiceWorker但普通用户根本不会打开 DevTools 来看。4.3 排查操作真机上排查最直接的办法是看响应头。在电脑上用 curl 模拟请求curl -I http://192.168.1.105:8080/index.html重点看返回的Cache-Control、ETag、Last-Modified这三项。如果Cache-Control里有max-age且数字很大说明服务器确实让浏览器缓存了。再打开真机浏览器的开发者调试工具。Android 上的 Chrome 可以通过 USB 连接后用 Chrome DevTools 远程调试可以直接看到 Application 面板里有没有注册 Service Worker、Cache Storage 里存了什么资源。iOS 上也可以连 Safari 的 Web Inspector 查看。这一步是定位 SW 问题的金标准用不了这一步的时候至少可以用无痕模式验证是不是缓存问题。4.4 解决配置正确的缓存策略我的最终解决方案分三步。第一步给 index.html 设置一个永不强缓存的响应头。Nginx 配置里可以这样写server { listen 80; server_name 你的域名或者IP; location /index.html { add_header Cache-Control no-store, no-cache, must-revalidate, proxy-revalidate; expires 0; } location /assets/ { expires 1y; add_header Cache-Control public, immutable; access_log off; } }注意HTML 文件走 no-cache意思是每次都要向服务器确认文件有没有变化带 hash 的静态资源走一年强缓存因为文件名变了就相当于新文件。第二步确认构建产物的文件名带 hash。Vite 默认就会给 JS/CSS 加 hash但如果你项目里的配置覆盖了输出格式可以手动指定build: { rollupOptions: { output: { entryFileNames: assets/[name]-[hash].js, chunkFileNames: assets/[name]-[hash].js, assetFileNames: assets/[name]-[hash][extname] } } }这套配置和上面 Nginx 的/assets/强缓存是配套的只要文件名带 hash哪怕 HTML 每次都重新拉取静态资源也能放心命中最新的缓存。第三步如果之前注册过 Service Worker需要在新的版本里把旧 SW 注销掉或者在 SW 内部做版本号对比。最简单的方式是给 SW 的注册文件名加上查询参数比如sw.js?v20250115去触发浏览器重新拉取 SW 文件配合self.skipWaiting()和clients.claim()立即接管页面。4.5 延伸history 路由下真机刷新又 404和缓存无关、但同样只在真机部署环境才暴露的还有一个变种SPA 用了 history 路由在 PC 的 dev server 里一切正常因为 Vite 开发服务器默认做了 history fallback把404兜底回 index.html。但部署到 Nginx 之后真机上直接访问http://IP:端口/about再刷新服务器找不到about这个文件就会返回 404。解决办法是在 Nginx 里加一条 try_fileslocation / { try_files $uri $uri/ /index.html; }这条配置的意思很简单先找真实的文件找不着就回退到 index.html把路径交给前端路由去解析。这个坑虽然不属于缓存范畴但同样属于真机上才暴露的问题排查时思路是一致的——别只盯着应用代码看看服务器到底怎么分发请求。5. 三个坑是一条链路真机调试前的自查清单5.1 为什么 PC 上不容易暴露回头看看这三个坑我最大的感受是问题不是出在某个环节的代码上而是出在你以为的链路和真实的链路不一致。PC 开发环境下dev server 帮你屏蔽了入口解析的严格性浏览器的 dev server 模式默认禁用缓存Hexo 的本地预览通常会告诉你首页能打开于是你根本意识不到这些环节的真实行为。真机访问一次等于把构建、生成、访问三个环节全部拉回现实所有想当然的地方都会露出原形。这也是为什么我建议在项目初期就把这些链路行为搞清楚。你平时可能觉得npm run build只是给打包用的Hexo 的public目录生成也无所谓但真到需要真机演示、上线、给别人看时这些无所谓全部会变成你的加班时间。5.2 自查清单我把这三个坑整理成了一张表每次上真机之前照着过一遍能省下大把排查时间。检查环节检查项验证方式构建项目根目录存在小写 index.htmlls -la构建Rollup 入口路径正确npm run build保证不报错构建index.html 内部资源路径相对/绝对无误检查 script/link 的地址生成source 下存在 index.mdls source/生成index.md 的 front-matter 正常head -6 source/index.md生成public 下确实生成了 index.htmlls public/index.html部署响应头 Cache-Control 合理curl -I 你的地址/index.html部署静态资源带 hash打开页面看 Network 面板真机使用无痕/隐私模式验证无痕访问一次真机Service Worker 未拦截DevTools Application 面板真机history 路由刷新不 404直接刷新你的地址/任意子路径表格看起来简单但每一行后面都有一次真实的崩溃经历。把这些检查项固化成习惯之后真机演示失败的概率会大大下降。6. 再分享一点我的习惯上真机前的四步动作最后聊点我自己养成的工作习惯也算是对这三个坑的总结性回应。我现在每次准备把项目部署到真机或发给别人看之前会固定执行四步。第一步先在本机构建。npm run build跑通过不了就直接进入真机演示失败的倒计时。看到报错别慌先按文件存在吗、路径对吗、大小写对吗、资源路径对吗的顺序查一遍。第二步检查产物目录。Vite 看 distHexo 看 public确认 index.html 真的在再往下走。很多部署后打不开的问题在这一步就能提前拦截。第三步本地起一个静态服务用 curl 看响应头。这一步是在模拟真机的请求行为确认服务器不会告诉浏览器存着别动。第四步真机上先用无痕窗口访问。无痕模式会默认绕过大部分缓存别直接把正常模式的缓存当错误排查对象。如果项目是 SPA 且用了 history 路由再多做一步在真机上访问一个子路由然后刷新确认服务器兜底配置有效。说回这三个坑它们的共同点其实是我对 index.html 的轻视——觉得它不过是一个入口文件不会惹出什么大乱子。但真机就像一面照妖镜任何一个环节不踏实的地方都会被放大。以后我每到一个新项目第一件事已经不是打开 App.vue 看代码了而是先想清楚这个项目的 index.html 是从哪个环节来的、会被哪些环节消耗掉。希望这篇记录能帮你少踩一次坑。