diff --git a/README.md b/README.md index c79dd4b..7eb0005 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,24 @@ bun run start # 打包前端并启动,默认 http://127.0.0.1:7580 ## 开发 -改前端时用 Vite,它会把接口请求转发给后端: +改代码的时候用这个,前后端都不用重启: ```bash -bun run dev # 后端,带热重载 -bun run dev:web # 前端,http://localhost:5173 +bun run dev:kernel # 终端 1:本地测试内核,开着不用管 +bun run dev:all # 终端 2:后端 + 前端,改了自动生效 ``` -本地有一份测试用的内核配置(`dev/config.yaml`,全是占位地址,端口刻意避开 mihomo 的默认值),可以对着它跑完整的「写回 → 校验 → 重载 → 失败还原」: +然后访问 **http://localhost:5173**(不是 7580)。后端代码改动由 Bun 的 `--hot` 重载, +界面改动由 Vite 直接更新浏览器。 + +`bun run start` 不具备这个能力——它把前端打成静态文件再由后端提供,代码一改就得重新 +打包重启。那种方式适合最终验证和部署。 + +本地测试内核的配置由模板生成(`dev/config.yaml` 进 git,订阅地址从 `.env` 取, +生成的 `dev/home/config.yaml` 不进 git): ```bash -bun run dev:kernel # 启动本地测试内核,管理接口 19090 -bun run dev:reset # 把测试配置恢复成模板的样子 +bun run dev:reset # 第一次,或想把测试配置恢复成模板时 ``` ## 检查 diff --git a/package.json b/package.json index 5f76c65..10e4beb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "vite build && bun run server/index.ts", "dev": "bun run --hot server/index.ts", + "dev:all": "bun run scripts/dev-all.ts", "dev:web": "vite", "dev:kernel": "../binary/mihomo-windows-amd64-v3-go125.exe -d dev/home -f dev/home/config.yaml", "dev:reset": "bun run scripts/dev-reset.ts", diff --git a/scripts/dev-all.ts b/scripts/dev-all.ts new file mode 100644 index 0000000..223b8dc --- /dev/null +++ b/scripts/dev-all.ts @@ -0,0 +1,25 @@ +/** + * 开发模式:一条命令同时起后端和前端。 + * + * 后端带 --hot,改了服务端代码自己重载;前端是 Vite,改了界面代码浏览器立刻更新, + * 两边都不用手动重启。访问 Vite 那个地址(5173),它会把接口请求转发给后端。 + * + * 打包后的 `bun run start` 不具备这个能力:它把前端打成静态文件再由后端提供, + * 代码一改就得重新打包重启。那种方式适合最终验证和部署。 + */ +export {} + +const procs = [ + Bun.spawn(['bun', 'run', '--hot', 'server/index.ts'], { stdio: ['inherit', 'inherit', 'inherit'] }), + Bun.spawn(['bunx', 'vite'], { stdio: ['inherit', 'inherit', 'inherit'] }), +] + +const stopAll = () => { + for (const p of procs) p.kill() + process.exit(0) +} +process.on('SIGINT', stopAll) +process.on('SIGTERM', stopAll) + +await Promise.race(procs.map((p) => p.exited)) +stopAll() diff --git a/server/routes.ts b/server/routes.ts index 5ace110..fd5ae61 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -61,15 +61,37 @@ const WEB_DIST = new URL('../web-dist/', import.meta.url).pathname.replace(/^\/( /** * 静态文件不需要令牌——它们只是一个空壳,所有数据都要带令牌才拿得到。 - * 单页应用的路由由前端接管,所以找不到的路径一律回 index.html。 + * + * 单页应用的路由由前端接管,所以找不到的路径回 index.html。但这条**不能**用在 + * 带扩展名的路径上:资源文件是带哈希的,每次打包都换名字,浏览器要是缓存了旧的 + * index.html,它引用的那个 .js 就已经不存在了。这时候回 index.html 等于把一份 HTML + * 当作 JavaScript 交给浏览器,报出来的是「MIME 类型不对」——一句完全指不到病根的错误。 + * 老老实实回 404,浏览器才会重新去取 index.html。 */ +const HAS_EXTENSION = /\.[a-z0-9]+$/i + async function serveStatic(path: string): Promise { + const shellHeaders = { + 'Content-Type': 'text/html; charset=utf-8', + // 空壳不缓存,否则打包后旧的 index.html 会一直引用已经不存在的资源 + 'Cache-Control': 'no-cache', + } + const target = Bun.file(WEB_DIST + (path === '/' ? 'index.html' : path.slice(1))) - if (await target.exists()) return new Response(target) + if (await target.exists()) { + return path === '/' ? new Response(target, { headers: shellHeaders }) : new Response(target) + } + + if (HAS_EXTENSION.test(path)) { + return new Response(`找不到 ${path}`, { + status: 404, + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) + } const shell = Bun.file(`${WEB_DIST}index.html`) - if (await shell.exists()) return new Response(shell) - return new Response('前端还没打包。先运行 bun run build,或者开发时用 bun run dev:web。', { + if (await shell.exists()) return new Response(shell, { headers: shellHeaders }) + return new Response('前端还没打包。运行 bun run build,或者开发时用 bun run dev:web。', { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8' }, })