拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

H5真机联调index.html踩坑:从Vite构建到Hexo部署全解析

H5真机联调index.html踩坑:从Vite构建到Hexo部署全解析

前阵子把一个 H5 项目从开发环境搬到真机去联调,结果一个 index.html 差点把我耗到半夜。平时在电脑浏览器上跑得好好的页面,一上手机就各种幺蛾子:构建打包报 could not resolve entry module "index.html",真机访问直接 404,最无语的是 hexo 博客的 public 目录里连 index.html 的影子都没有。

这篇分享不是讲什么高深架构,纯粹是记录我在实际开发中踩过的三个“真机才有的坑”。仔细想了一下,这三个坑分别对应三个阶段:构建期、运行期、部署期。构建期的坑是 Vite/Rollup 找不到 index.html 入口;运行期的坑是路由回退和缓存导致页面 404 或白屏;部署期的坑是 Hexo 生成后 public 目录里没有 index.html。每一个坑单看都不难,但它们凑到一起时,绝对能让你怀疑人生。

如果你是前端开发、H5 联调新手,或者是一个用 Hexo 搭个人博客的朋友,这篇内容应该能帮你省下几个小时的排查时间。

1. 坑一:构建期——rollup 找不到 index.html

1.1 报错现场:could not resolve entry module "index.html" 如何一步步复现

事情要从一个很“常规”的项目结构说起。当时我的目录长这样:

my-h5/ ├── src/ │ ├── pages/ │ │ └── home/ │ │ └── index.html │ └── main.js ├── vite.config.js └── package.json

在 src/pages/home 下面放了 index.html,因为我想让页面入口跟着业务模块走。本地执行npm run dev一切正常,页面能开,交互也没问题。于是我开始执行npm run build,准备打个包放到服务器上给手机访问。

结果命令刚跑起来,直接红了:

[Error] Could not resolve entry module "index.html". error during build: RollupError: Could not resolve entry module "index.html".

我当时的第一反应是:开什么玩笑,index.html 明明就在那里,你凭什么说解析不了?于是我检查了文件路径、大小写、文件权限,全都没问题。甚至把 index.html 复制到根目录再试,报错居然消失了,但一旦放回 src/pages/home 下,又报同样的错。这时候我才意识到,问题不在于“文件是否存在”,而在于“Vite 认为项目的根目录在哪里”以及“入口配置是否告诉了 Rollup 去哪里找这个文件”。

1.2 根因分析:Vite 的入口设计,root 与 rollupOptions.input 的关系

Vite 和传统的 Webpack 有一个很大的不同:Webpack 一般直接以 JS 文件作为入口,而 Vite 默认把 index.html 当作整个应用的入口。在 Vite 的构建流程里,它会先找到根目录下的 index.html,解析里面的<script type="module" src="...">,然后根据脚本引用关系把整个依赖图交给 Rollup 打包。

这里就牵扯到一个关键概念:root。Vite 默认的 root 是process.cwd(),也就是你执行 npm 命令的当前目录。如果你的 index.html 不在这个 root 下,Rollup 在解析入口时就会报Could not resolve entry module "index.html"。尤其是当你在项目里手动配置了 root,比如把 root 指向了 src:

export default { root: 'src' }

那么 Vite 会在src/下找 index.html,如果src/index.html不存在,构建同样失败。

还有一种情况,是我们喜欢在build.rollupOptions.input里手动指定入口。例如:

build: { rollupOptions: { input: 'src/index.html' } }

看起来好像没问题,但要注意,这个路径是相对 root 解析的。如果 root 恰好也是 src,那 Rollup 实际去找的是src/src/index.html,自然找不到。反之,如果你的目录结构是 src/pages/home/index.html,而 input 写的是src/index.html,同样找不到。

所以根因就是一个路径解析问题,但因为它涉及 root、base、input 多个配置项,并且不同版本 Vite 对路径的处理也有细微差异,排查起来就会很恼火。

1.3 正确的修法:三处配置一次改对

我最后是靠三处配置的重新梳理解决了问题。第一处,也是最推荐的做法:把 index.html 放回项目根目录,保持 Vite 默认的入口逻辑,不去折腾 root。

my-h5/ ├── index.html # 放这里 ├── src/ │ ├── pages/... │ └── main.js ├── vite.config.js

第二处,如果你的确有特殊结构,一定要用自定义 root,那就把 root 和 input 写清楚,并且使用绝对路径来避免歧义:

import path from 'path' import { defineConfig } from 'vite' export default defineConfig({ root: path.resolve(__dirname, 'src'), build: { rollupOptions: { input: path.resolve(__dirname, 'src/index.html') } } })

第三处,如果项目是多页应用,各页面的 index.html 分散在不同目录,可以在 build.rollupOptions.input 里写一个对象:

build: { rollupOptions: { input: { main: path.resolve(__dirname, 'index.html'), about: path.resolve(__dirname, 'src/about/index.html') } } }

这样 Rollup 会生成对应的多个 HTML 文件,而不会只认一个入口。改完配置后再执行npm run build,epoch 报错就消失了。

1.4 为什么这个错非得到真机阶段才蹦出来

这个问题看起来像是构建期的问题,为什么我标题会说“真机才有的坑”?因为我实际遇到的情况是:在开发模式下,Vite Dev Server 有非常强的容错能力,它会自动把某些路径请求映射到项目根目录的 index.html,即使入口配置有瑕疵,只要你能通过浏览器访问到某个页面,就不会察觉。而且不少同学和我一样,平时开发根本不会先 build 一次再上真机,都是直接npm run dev然后用手机访问 dev server。

也就是说,只要你在本地没有执行过 build,这个 RollupError 就永远不会暴露。等到你真机上需要稳定的静态包时,通常第一次打包就炸了。再加上报错信息比较晦涩,很多人会先去查网络、查手机端,完全没想到问题出在构建配置上。

另一个连带场景是:本地 build 成功生成了 dist,但查看 dist 目录时发现只有 assets 和 devtools 之类的东西,根本没有 index.html。这种情况多半是构建输出的文件名或者目录结构被改过,导致服务器上入口缺失。所以我的建议是,每次 build 之后先看一眼 dist 根目录有没有 index.html,再继续真机调试,这个习惯能帮你把构建问题挡在手机之外。

2. 坑二:真机访问——页面白屏、404、路由回退与缓存

2.1 先解决“手机看不到页面”:dev server 必须监听局域网地址

构建问题解决后,我接着把打包好的静态文件放到一台内网服务器上,用手机访问。手机打开http://localhost:5173,当然不行。这个地址在手机浏览器里指向的是手机自己,压根没有服务。正确的做法是通过电脑的局域网 IP 访问,比如http://192.168.1.100:5173。

问题来了:Vite Dev Server 默认绑定的是127.0.0.1,也就是只监听本机回环地址。你从手机访问电脑的局域网 IP 时,请求到达了电脑的网卡,但是 Node 进程根本没有监听那个网卡,于是连接失败。在电脑上一切正常,是因为浏览器和 Node 在同一台机器上,走的回环路径。

解决办法很简单,在 vite.config.js 里设置:

server: { host: '0.0.0.0', port: 5173 }

或者启动的时候加上--host 0.0.0.0。这样 Vite 会监听所有网络接口,手机和电脑在同一个 Wi-Fi 下,就能通过http://192.168.1.100:5173访问了。如果你用了 8080、3000 这些端口,也要注意防火墙放行。Windows 上经常出现“手机能 ping 通电脑,但浏览器打不开页面”的情况,多半就是 Windows 防火墙或第三方安全软件拦截了 Node 进程的入站连接。

值得注意的一点是:手机访问 dev server 和访问静态打包产物的体验不同。dev server 会有热更新,通过 WebSocket 和浏览器保持连接,而手机端经常因为网络波动断连,你需要确认 Vite 的clientPort配置是否和访问端口一致,否则 HMR 会连不上,但页面本身还是可以打开的。

2.2 刷新子路由404:history 路由需要服务端回退到 index.html

把 dev server 跑通之后,我又在真机上遇到了新问题:从首页进入点击导航到/user/profile,页面正常;但是一旦在手机浏览器里直接刷新这个子路由,就 404。这个经典问题很多人应该都遇到过。

原因很简单:你的前端框架用了 History 模式路由,比如 Vue Router 的createWebHistory()、React Router 的BrowserRouter。这种模式下的 URL 是https://example.com/user/profile,浏览器直接发起请求时,服务器会在文件系统里寻找user/profile文件,当然找不到。开发环境里 Vite Dev Server 会自动做 history fallback,所以刷新也没事。但部署到 nginx、Apache 或任何静态文件服务器上,服务器不会自动跳回 index.html,于是 404 就在真机上出现了。

解决方式有几类。如果你用 nginx,最经典的配置是:

location / { try_files $uri $uri/ /index.html; }

意思就是优先找真实文件,找不到就回退到 index.html,交给前端路由处理。如果只是本地起了一个静态服务器做测试,可以使用sirv-cli --single或http-server --history-fallback这类工具。

如果你不太想折腾服务端配置,也可以直接把前端路由改成 Hash 模式。Hash 模式下的 URL 是https://example.com/#/user/profile,#后面的内容不会被发送到服务器,所以刷新时服务器只会去请求根路径,拿到 index.html。这个方案在真机调试时最省事,代价是 URL 不够美观,分享链接时也会多一个#。

我在实际项目中更推荐的做法是:开发时用 History 模式,方便查看真实路由;部署时如果服务器自己控制不了,就统一改成 Hash 模式,避免 404。这样最大程度降低线上事故率。

2.3 真机浏览器缓存:index.html 万年不变,新代码不执行

接着我又踩了一个更隐蔽的坑。代码改了,包也重新打了,服务器文件也更新了,但手机访问还是旧页面。一开始我怀疑是 CDN 缓存,上去刷新了很多次也没用。后来用电脑无痕模式访问,发现是新版本;再用手机普通模式访问,依然是旧版本。这明显是手机浏览器的缓存策略比桌面浏览器更激进。

仔细一看响应头,发现服务器对index.html也返回了强缓存头,浏览器直接把旧 HTML 缓存住了。HTML 文件一旦被缓存,里面引用的新旧 JS 资源文件名就都是旧的,自然不会去加载新代码。

正确的缓存策略应该是:带 hash 的静态资源(JS/CSS/图片)使用强缓存,文件名变了 URL 就变;不带 hash 的 index.html 必须使用 no-cache,确保每次请求都回源检查是否更新。在 nginx 里可以这样配置:

location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; }

加了这段配置以后,手机端刷新页面就能拿到最新的 index.html,然后通过新文件名的 URL 加载新资源。如果你用的是 CDN,还需要检查 CDN 的缓存规则,确保 HTML 的 TTL 为 0 或者设置为不缓存。这里有个小技巧:真机调试时,我一般会在地址栏直接手动修改 URL,比如加一个?v=日期参数来绕过缓存,但这个方法治标不治本,还是要在服务端把响应头配置对。

另外,有些低端安卓浏览器对 no-cache 的处理并不完美,可能需要配合meta标签,但我个人更建议优先保证服务器响应头正确,因为meta标签在部分浏览器里根本不管用,尤其是 HTTP 响应头已经存在时,浏览器优先信任响应头。

2.4 部署到静态服务器的路径问题:base 配置决定一切

最后一个运行期坑是路径问题。打包完 dist 目录后,我直接在电脑上用vite preview访问,页面完全正常。然后我把 dist 整个文件夹放到服务器/h5/子目录下,让手机访问http://服务器IP:8080/h5/,结果整个页面白屏,打开 devtools 一看,JS 资源请求的是/assets/index.js,而不是/h5/assets/index.js。

这就是 Vite 的base配置搞的鬼。默认情况下,Vite 打包出的 HTML 中资源引用是绝对路径/assets/xxx.js,这意味着资源永远指向域名的根目录。如果你部署在根路径,没问题;但如果部署在子目录,就要告诉 Vite 资源的公共路径是什么。

解决办法是设置 base:

export default { base: '/h5/' }

如果完全不确认部署路径,也可以用相对路径:

export default { base: './' }

但相对路径也有坑:如果你的前端路由启用了 History 模式,子路由下的相对路径可能会算错。所以最稳妥的方式是明确知道部署路径,然后写死 base。这个坑是在真机上才显眼的,因为你在电脑上通常有某种服务器环境或插件帮你修正了路径,手机浏览器可没有这个待遇,它只会按 HTML 里的引用来请求资源。

3. 坑三:Hexo 博客——public 下没生成 index.html

3.1 现象:本地预览正常,真机/服务器找不到 index.html

第三个大坑,来自我的 Hexo 博客。某天我更新了一篇文章,执行了hexo clean && hexo generate,本地跑hexo server预览首页完全正常。于是我把生成的public文件夹部署到服务器,想着手机上访问一下看效果。结果手机访问域名,直接 404。

登录服务器一看,网站的目录结构里居然没有index.html。更奇怪的是,本地明明有public/index.html,怎么上传之后就不见了?后来才搞清楚,不是上传丢了,而是我在服务器上执行的部署脚本里,用了rsync同步目录,某个环节把 source 目录底下的旧文件清掉了,而没有重新生成。但真正的根源还是 Hexo 本身没有正确生成根目录下的 index.html。

3.2 最常见原因:Front-matter 缺失导致页面没被渲染

Hexo 不是简单地拿 markdown 文件转成 HTML,它会根据文件头部---包裹的 YAML Front-matter 来决定如何处理这个文件,包括使用哪个 layout、生成的路径等。如果一篇 markdown 文件头部没有---块,或者是纯文本没有任何 meta 信息,Hexo 在渲染时可能会跳过它,甚至直接把它当作不需要处理的静态文件复制过去。

我之前遇到过的一个案例是:首页的源文件source/index.md内容如下:

这是一个自动生成的首页

没有 title,没有 date,没有任何 Front-matter。Hexo 在 generate 的时候就不会把这个文件当作需要渲染的页面,因此 public 下缺少 index.html。补上 Front-matter 后:

--- title: 我的首页 layout: index --- 这是一个自动生成的首页

重新执行hexo generate,index.html 就出现了。

当然,layout: index并不是每个主题都适用,具体要看主题支持的 layout 名称。但关键是:当你发现自己生成出来的 public 目录里没有 index.html 时,第一件事就是检查首页源文件是否有合法、完整的 Front-matter。

3.3 Hexo 目录与生成器陷阱:不是所有 index.md 都会变成 public/index.html

很多人会误以为只要新建一个 index.md,Hexo 就会在 public 根目录生成 index.html。其实不然。Hexo 的目录结构有明确分工:source/_posts下面存放文章,它们会在public下生成类似2025/01/01/文章标题/index.html的归档路径;source/index.md通常是自定义首页模板的源文件,是否会生成public/index.html取决于主题和生成器。

我踩过一次很深的坑:我写了一个source/_posts/index.md,然后指望它成为博客首页,结果它只是被当成了文章,生成的页面是public/2025/xx/xx/index.html,根目录的 index.html 照样没有。直到我明白,博客首页的 index.html 是由hexo-generator-index这个插件把最新文章列表渲染成 index 页面,而不是由 index.md 本身生成的。

如果你发现文章都正常,但首页不出来,检查 package.json 里是否安装了hexo-generator-index。有些精简主题或自定义配置会把默认生成器去掉,导致博客没有列表页入口。安装它:

npm install hexo-generator-index --save

然后重新执行hexo clean && hexo generate。

另外,还有一种情况是source里存在名为index.html的文件,但它被当作静态文件直接拷贝,没有参与渲染。这样 public 下可能会有一个空白 index.html,你也看不出来到底是不是通过 Hexo 生成的。检查一下文件的生成时间,以及里面是否有主题模板的内容,就能分辨。

3.4 处理流程:从 clean 到 generate 的排查清单

为了让你们少走弯路,我总结了一套排查流程,可以按顺序执行。

第一步,在项目根目录运行hexo clean,它会清空 public 以及 db.json 缓存。第二步,运行hexo generate --debug,注意看输出里有没有Processing ...相关日志,以及是否有文件被跳过。第三步,打开 public 目录,确认根目录是否有 index.html。如果没有,回到 source 目录,检查首页相关的 md 或 html 文件是否存在、格式是否正确、layout 是否匹配主题。

如果 public 里有 index.html,但服务器上访问 404,那么问题可能出在同步部署环节。最常见的是把文件上传到了错误的目录,或者使用了 CDN 缓存,导致服务器根目录一直用的是旧文件。这时候可以先用服务器上的 curl 直接请求http://你的域名/index.html,看返回的是 200 还是 404。如果是 404,再排查路径;如果是 200 但手机访问还是 404,检查 CDN 配置和 DNS 解析。

最后还有一个容易被忽略的问题:真机访问 Hexo 本地预览时,同样会遇到 host 绑定问题。默认hexo server监听的是 0.0.0.0,但如果你手动设置了-i 127.0.0.1,手机就访问不到。建议直接使用:

hexo server -i 0.0.0.0 -p 4000

同时在手机浏览器里访问http://电脑IP:4000,才能看到本地预览。不然你折腾半天还以为页面坏了,其实就是没监听局域网地址。

4. 我的排查顺序与三个避坑习惯

说实话,这三个坑单拎出来都不难,难的是它们会同时出现。我那次 debug 的顺序非常乱:先以为路由问题,改了 Hash 路由没用;又以为是缓存,清了缓存还是旧版;最后才发现是构建时入口配置错误,导致 dist 根本没有 index.html。教训很深。

我现在固定一套流程:先跑npm run build,检查 dist 里有没有 index.html;再起一个本地静态服务器,模拟服务器环境访问;最后再用真机去访问局域网 IP。任何一步没通过,都不急着让手机上场。这样可以把“真机才有的坑”提前暴露在电脑上。

另外一个习惯是:真机联调前,先在手机上强制清缓存或开无痕模式,避免 index.html 强缓存干扰判断。如果还是旧页面,直接在 devtools 网络面板看 index.html 的响应头,判断是不是 Cache-Control 的问题。

最后一个小技巧:使用 Vite 构建时,如果你想快速在真机上测试,而不想每次构建,可以先启动 dev server 同时设置server.host: '0.0.0.0',然后手机直接访问http://IP:端口。这样避开了打包、静态服务器路径这些环节,能快速排查是前端代码问题还是部署问题。等确认代码没问题,再去做 build 和部署。我后来基本都这么干,省了不少时间。

一个 index.html 的坑,背后牵出了构建入口、静态服务器、路由回退、浏览器缓存、Hexo 生成机制整整五层问题。每次都以为是环境坏了,最后发现都是自己对工具链某个默认行为理解不到位。把这些记录下来,也是想提醒自己:下次再遇到真机上的诡异问题,先从最基础的 index.html 开始查,别上来就怀疑玄学。

返回列表