Archived
feat: 后端接线上 React 构建产物,legacy 模板降级为兜底
为什么:前端迁移到 Vite 后,首页应直接返回 frontend/dist/index.html 字节(golden 口径随之变为构建产物原样字节),仅当 dist 未构建时 回退 Jinja2 渲染的 legacy 模板。同步挂载 /assets 静态目录、排除 frontend 的 node_modules/tsbuildinfo 入库、docker 构建只带 dist 不带源码、ruff/pyright 排除 frontend 目录(TS 侧由 tsc+vite 把关)。
This commit is contained in:
1 parent
3a089caff2
commit
8803444755
5 files changed
+44
-18
No files matched your search
@@ -42,3 +42,10 @@ _css_ok.txt
|
|||||||
云村小助手-网站开关.bat
|
云村小助手-网站开关.bat
|
||||||
_audit_work/
|
_audit_work/
|
||||||
venv/
|
venv/
|
||||||
|
# 前端源码与依赖不进镜像(dist 构建产物保留,后端直接服务它)
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/src/
|
||||||
|
frontend/index.html
|
||||||
|
frontend/package*.json
|
||||||
|
frontend/tsconfig.json
|
||||||
|
frontend/vite.config.ts
|
||||||
@@ -20,6 +20,11 @@ __pycache__
|
|||||||
# _mkpkg.py 会在项目根写入打包标记文件(内容为文件数/体积或 public 标记)
|
# _mkpkg.py 会在项目根写入打包标记文件(内容为文件数/体积或 public 标记)
|
||||||
_pkg_ok.txt
|
_pkg_ok.txt
|
||||||
|
|
||||||
|
# 前端工程:node_modules 不进库;dist 构建产物必须提交(后端直接服务它,
|
||||||
|
# Docker COPY . . 无需 node 即可工作)
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/*.tsbuildinfo
|
||||||
|
|
||||||
# 运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理,
|
# 运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理,
|
||||||
# 以下内容绝不能进版本库(用户数据库/网易云 Cookie/日志/审计文档)
|
# 以下内容绝不能进版本库(用户数据库/网易云 Cookie/日志/审计文档)
|
||||||
data/
|
data/
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ package = false
|
|||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py312"
|
target-version = "py312"
|
||||||
line-length = 120
|
line-length = 120
|
||||||
extend-exclude = [".venv", ".zcode", "templates", "static", "data"]
|
extend-exclude = [".venv", ".zcode", "templates", "static", "frontend", "data"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# E/F 基础错误、I 导入排序、B bugbear、UP 升级、C4 推导式、ASYNC 异步陷阱、S 安全、RUF
|
# E/F 基础错误、I 导入排序、B bugbear、UP 升级、C4 推导式、ASYNC 异步陷阱、S 安全、RUF
|
||||||
@@ -90,7 +90,7 @@ include = [
|
|||||||
"music_downloader",
|
"music_downloader",
|
||||||
"_mkpkg.py",
|
"_mkpkg.py",
|
||||||
]
|
]
|
||||||
exclude = [".venv", ".zcode", "data", "downloads", "templates", "static"]
|
exclude = [".venv", ".zcode", "data", "downloads", "templates", "static", "frontend"]
|
||||||
venvPath = "."
|
venvPath = "."
|
||||||
venv = ".venv"
|
venv = ".venv"
|
||||||
pythonVersion = "3.12"
|
pythonVersion = "3.12"
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ def create_app(api_prefix: str = '', *, start_monitor: bool = True) -> FastAPI:
|
|||||||
if _static_dir.is_dir():
|
if _static_dir.is_dir():
|
||||||
app.mount('/static', StaticFiles(directory=str(_static_dir)), name='static')
|
app.mount('/static', StaticFiles(directory=str(_static_dir)), name='static')
|
||||||
|
|
||||||
|
# React 构建产物(vite build → frontend/dist,hash 指纹资源挂 /assets)
|
||||||
|
_dist_dir = Path(__file__).resolve().parent.parent / 'frontend' / 'dist'
|
||||||
|
if (_dist_dir / 'assets').is_dir():
|
||||||
|
app.mount(
|
||||||
|
'/assets',
|
||||||
|
StaticFiles(directory=str(_dist_dir / 'assets')),
|
||||||
|
name='assets',
|
||||||
|
)
|
||||||
|
|
||||||
# 业务路由:统一挂到 api_prefix(默认空前缀,即前端使用的根路径)
|
# 业务路由:统一挂到 api_prefix(默认空前缀,即前端使用的根路径)
|
||||||
app.include_router(system_router.router, prefix=api_prefix, tags=['system'])
|
app.include_router(system_router.router, prefix=api_prefix, tags=['system'])
|
||||||
app.include_router(site_router.router, prefix=api_prefix, tags=['site'])
|
app.include_router(site_router.router, prefix=api_prefix, tags=['site'])
|
||||||
|
|||||||
+21
-16
@@ -1,8 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""首页与静态资源。
|
"""首页与静态资源。
|
||||||
|
|
||||||
- GET / 首页:渲染 templates/index.html,禁止缓存
|
- GET / 首页:优先服务 React 构建产物 frontend/dist/index.html;
|
||||||
- 静态资源由 Starlette StaticFiles 挂载 /static(见 web/app.py)
|
dist 未构建时回退旧版 templates/index.html(平滑过渡)
|
||||||
|
- 静态资源由 Starlette StaticFiles 挂载 /assets 与 /static(见 web/app.py)
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -11,24 +12,28 @@ from fastapi.responses import HTMLResponse
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
_TEMPLATE = Path(__file__).resolve().parent.parent.parent / 'templates' / 'index.html'
|
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
_DIST_INDEX = _PROJECT_ROOT / 'frontend' / 'dist' / 'index.html'
|
||||||
|
_LEGACY_TEMPLATE = _PROJECT_ROOT / 'templates' / 'index.html'
|
||||||
|
|
||||||
|
|
||||||
|
def _render_index() -> str:
|
||||||
|
"""渲染首页 HTML:React 构建产物优先,缺失时回退旧模板。"""
|
||||||
|
if _DIST_INDEX.is_file():
|
||||||
|
return _DIST_INDEX.read_text(encoding='utf-8')
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
env = jinja2.Environment(
|
||||||
|
loader=jinja2.FileSystemLoader(str(_LEGACY_TEMPLATE.parent)),
|
||||||
|
autoescape=True,
|
||||||
|
)
|
||||||
|
return env.get_template(_LEGACY_TEMPLATE.name).render()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/', response_class=HTMLResponse)
|
@router.get('/', response_class=HTMLResponse)
|
||||||
async def index(request: Request) -> HTMLResponse:
|
async def index(request: Request) -> HTMLResponse:
|
||||||
"""首页(禁止缓存,确保前端更新即时生效)。
|
"""首页(禁止缓存,确保前端更新即时生效)。"""
|
||||||
|
|
||||||
用 Jinja2 以与历史模板渲染一致的默认口径(keep_trailing_newline=False),
|
|
||||||
使输出逐字节不变。
|
|
||||||
"""
|
|
||||||
import jinja2
|
|
||||||
|
|
||||||
env = jinja2.Environment(
|
|
||||||
loader=jinja2.FileSystemLoader(str(_TEMPLATE.parent)),
|
|
||||||
autoescape=True,
|
|
||||||
)
|
|
||||||
html = env.get_template(_TEMPLATE.name).render()
|
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
content=html,
|
content=_render_index(),
|
||||||
headers={'Cache-Control': 'no-cache, no-store, must-revalidate'},
|
headers={'Cache-Control': 'no-cache, no-store, must-revalidate'},
|
||||||
)
|
)
|
||||||
Reference in new issue
Block a user