项目:html5-examples | 版本:v1.0 | 生成日期:2026-10-06 | 依据:源码分析
| 组件 | 版本要求 | 用途 | 依据 |
|---|---|---|---|
| Node.js | ≥ 18(CI 实际用 20) | 前端构建与运行;NestJS 后端 | .github/workflows/ci.yml、AGENTS.md |
| pnpm | CI 用 v10 | 包管理(workspace) | .github/workflows/ci.yml、pnpm-workspace.yaml |
| PostgreSQL | 任意现代版本 | 仅后端需要(目录/用户/收藏数据) | server/src/app.module.ts TypeORM 配置 |
| 现代浏览器 | 支持 ESNext | 前端运行(build.target: 'esnext') | vite.config.ts |
版本要求里有一条隐含的红线值得展开:build.target: 'esnext' 意味着构建产物不做语法降级,产出的代码直接使用最新 ES 特性。这对内部工具型站点是合理的取舍(换取更小的产物与更快的构建),但决定了部署前必须明确目标用户的浏览器范围——若用户群中存在旧版浏览器(如未升级的企业内网 Chrome),要么接受白屏风险,要么自行调整 vite.config.ts 的 target 与相关插件,并把这条变更纳入评审,因为它会同时影响产物体积与构建时间。同理,esbuild 承担压缩(minify: 'esbuild')而非 terser,也是速度优先的选型,两者不可混为一谈。
关键架构提示:由于示例正文已内化为 src/content/examples/*.json(2026-10-05 迁移,见 src/api/examples.ts 头注释),纯静态部署的前端可以独立展示示例库、独立演示、动效库、CSS 实验室等全部内容板块;后端只在需要"主题/分类目录、登录、收藏、UI 组件、Canvas 示例、静态站点列表"时才必须存在。若后端不可用,首页主题目录加载会失败(exampleStore.error),但其余板块不受影响。
cd html5-examples
pnpm install
pnpm dev # Vite 开发服务器 → http://localhost:5555(host 127.0.0.1)vite.config.ts:server.port = 5555、server.proxy['/api'] → http://localhost:3000。后端未启动时,凡走 /api 的请求会失败(收藏、登录、主题目录、Canvas/UI/Tailwind-sites 列表),但控制台不阻塞其余功能。index.html 的 Google Fonts 外链加载,内网环境需自行本地化或接受降级字体。# ── 后端(新终端)──
cd html5-examples/server
pnpm install # 注意:server 有独立 package.json / package-lock.json
cp .env.example .env # 然后编辑(见下表)
# 创建数据库(任选其一)
createdb html5_examples # 或 psql: CREATE DATABASE html5_examples;
pnpm migration:run # 执行 server/src/migrations/ 的 11 个迁移
pnpm dev # nest start --watch → http://localhost:3000server/.env 必配项(模板 server/.env.example,读取逻辑在 server/src/main.ts 与 app.module.ts):
| 变量 | 说明 | 缺省/校验行为 |
|---|---|---|
DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD/DB_NAME | PostgreSQL 连接 | 库名默认 html5_examples;生产缺 DB_PASSWORD 拒绝启动 |
JWT_SECRET | JWT 签名密钥 | 命中弱密钥字典或 < 占位符时:生产拒绝启动、开发告警 |
JWT_EXPIRES_IN / JWT_ACCESS_EXPIRES_IN / JWT_REFRESH_EXPIRES_IN | 令牌时效(建议 7200s / 604800s) | auth.service 读取 |
ADMIN_DEFAULT_PASSWORD | 默认管理员密码(启动时 createDefaultAdmin()) | — |
PORT | 服务端口 | 默认 3000 |
NODE_ENV | 环境 | development 时开启 SQL 日志 |
CORS_ORIGIN | 逗号分隔白名单 | 默认 localhost:5555,localhost:5560,credentials: true |
DB_SYNC | 'true' 时 TypeORM synchronize | 仅限开发,生产禁用 |
OSS_* 五项 | 阿里云 OSS(网盘/上传) | 不配置优雅降级,后端照常启动 |
两点实践提醒。第一,server/.env 不入版本库(模板是 .env.example),团队协作时密钥的传递要走密码管理工具而不是聊天记录;main.ts 的启动安检就是为了兜住"有人把模板占位符 <生成随机32位密钥...> 原样留在 env 里"这种情况——它以 < 开头会被识别为占位符并在生产拒绝启动。第二,ADMIN_DEFAULT_PASSWORD 与默认管理员账号是攻击面上的显式目标,首次部署后应立即修改,或在 authService.createDefaultAdmin() 的语义上确认其"仅当不存在时创建"的行为,避免重置为默认值。
# 前端侧脚本(根目录,tsx 运行)
pnpm extract # scripts/extract-examples.ts:Markdown 目录 → CodeExample JSON
# 后端侧历史导入脚本(server/scripts/,233+ 个,ts-node/tsx 运行)
cd server
npx tsx scripts/<某导入脚本>.ts
pnpm import-data # scripts/import-data.tspnpm build # vue-tsc --noEmit && NODE_OPTIONS='--max-old-space-size=8192' vite build
pnpm build:fast # vite build(跳过类型检查,不分配额外堆内存)
pnpm preview # 本地预览 dist/pnpm build 需要 8GB 堆内存(package.json scripts)——项目动态导入组件上千,类型检查与打包内存开销大;内存受限机器用 build:fast 并另行跑 pnpm typecheck。
构建期要点(全部来自 vite.config.ts,改动前必读):
manualChunks 19 条规则:Vue 核心 → vendor;@vue/compiler-sfc + esbuild → sfc-compiler(仅 SFC 运行时动态加载);echarts/antv/three/gsap/pixi/fabric/konva/leafer/roughjs/monaco 等各成独立 vendor chunk——每个示例只下载自己的库。hoistTransitiveImports: false:禁止跨 chunk 传递依赖提升,防 chunk 间循环 TDZ。resolve.dedupe: ['gsap'] + optimizeDeps.include 列出全部 gsap 插件入口:强制单一 gsap core 实例(注释记录了 MotionPathPlugin 跨实例污染事故)。experimentalMinChunkSize:1182 个动态组件使该合并算法接近 O(n²),曾致打包 1 小时以上(源码注释明示,勿回退)。chunkFileNames 区分 assets/vendor-*--hash.js,缓存友好。形态 A:纯静态托管(推荐起步)
pnpm build
# 将 dist/ 整体发布到任意静态服务器 / CDN / 对象存储index.html,否则刷新 /examples 等深链 404。Nginx 示例:location / { try_files $uri $uri/ /index.html; }。public/ 目录(shared-assets/、static-sites/、echarts-data/ 等)构建时原样拷入 dist/,示例 iframe 内的 <base href="{origin}/"> 依赖同源可访问这些路径——前端域名必须与这些资源同域。VITE_API_BASE_URL 时前端请求走同源 /api,需在网关层把 /api 反代到后端服务;跨域部署则设置该环境变量指向后端绝对地址并确认后端 CORS 白名单。形态 B:静态前端 + 独立 NestJS 后端
cd server && pnpm build # nest build → dist/
NODE_ENV=production node dist/main # 或 pm2/systemd 托管ServeStaticModule 同时托管 server/admin/ 到 /admin(管理后台),生产可经网关限制来源 IP。JWT_SECRET 弱值、缺 DB_PASSWORD 直接拒绝启动(server/src/main.ts 31-50 行)——迁移到生产前先本地以 NODE_ENV=production 预检一次。pnpm migration:generate / migration:run / migration:revert(server/data-source.ts 提供连接)。发版注意:内容 hash 化的 chunk 在发版后,旧页面持有的旧 chunk 引用会 404;src/router/index.ts 的 router.onError 检测到动态导入失败会自动整页 reload 完成自愈,无需人工干预。
两种形态如何选,可以归结为一个问题:站点的哪些能力对你是核心。如果目标就是"一个能自燃传播的前端示例博物馆",纯静态形态在成本、运维、可用性上全面占优——没有数据库就没有数据库故障,没有会话就没有会话过期投诉,CDN 边缘节点天然高可用;代价是导航目录为空(首页主题卡缺失,但各板块直链与顶部固定菜单项仍可用)且无收藏登录。如果需要完整的账户体系、管理后台运营能力,则选形态 B,并接受"两套进程、一条代理、一个数据库"的运维复杂度。折中路径同样存在:静态前端 + 仅代理 topics/categories 两个只读接口的极简后端,即可让首页目录工作而不托管全部账户能力——但需评估这种"半残后端"是否反而增加理解成本。无论哪种形态,都建议保持前端构建产物的可重放性:dist/ 由 CI 产出并保留 artifact,配合 git tag,任何一次线上内容事故都可以在分钟级回滚到上一个发布版本。
| 命令 | 实际执行 | 说明 |
|---|---|---|
pnpm lint | eslint . | flat config(eslint.config.js),CI 门禁:零错误 |
pnpm lint:fix | eslint . --fix | 自动修复 |
pnpm format | prettier --write "src/**/*.{ts,vue,js,css,json}" | 配置 .prettierrc:无分号、单引号、行宽 100、LF |
pnpm typecheck | vue-tsc --noEmit | strict 全量类型检查,CI 门禁 |
pnpm test | vitest run | CI 门禁 |
pnpm test:watch / pnpm test:coverage | vitest watch / v8 coverage | 覆盖率统计范围限定 src/utils/** 与 src/composables/**(vitest.config.ts) |
happy-dom + globals: true,用例文件模式 src/**/*.{test,spec}.{ts,tsx};现有用例:api/client.test.ts、stores/authStore.test.ts、composables/{useListFilter,usePagination}.test.ts、utils/{date,debounce,format}.test.ts,以及 components/layout/__tests__/。lint-staged(package.json)对 *.{ts,vue,js} 依次跑 eslint --fix + prettier --write,对 *.{css,json,md} 跑 prettier。gui-test-screenshots/ 是历史 GUI 测试截图产物;scripts/ 下另有大量截图/目检脚本(如 screenshot-static-sites-batch.ts)。.github/workflows/ci.yml):push main / PR → pnpm install --frozen-lockfile → lint → typecheck → test 三道门禁。关于测试策略,需要客观陈述现状:单测覆盖集中在纯函数与状态机(client 的重试与错误归一、authStore 的会话生命周期、筛选与分页的边界计算、日期格式化与防抖),而不覆盖组件渲染快照与端到端旅程。这与项目的质量观一致——UI 的正确性主要靠代码评审加 GUI 目检保障(截图脚本与 gui-test-screenshots/ 即其产物),可测性投资优先给了逻辑最稠密、回归代价最高的模块。为纯函数写测试时遵循既有模式:用例文件与被测文件同目录同名加 .test 后缀,globals: true 下直接使用 describe/it/expect,不引入额外断言库。覆盖率报告限定了 utils 与 composables 两个目录(vitest.config.ts coverage.include),因为它们是"被所有页面复用的底座",一处缺陷放大为全站缺陷,值得用数字盯防;views 与 components 不在统计范围内并不代表允许裸奔,而是优先级排序。
pnpm format && pnpm lint && pnpm typecheck && pnpm build && pnpm devtopicSlug(当前仅 html5 / css 有量)与 sourceGroup(8 个分组之一,见 04 篇 §3)。分组即文件名。src/content/examples/<分组>.json 数组中追加条目。必填字段:id(全局唯一,数字风格 id 排序靠前)、title、description、category、categorySlug、topicSlug、tags、language、html、sourceFile、difficulty;可选 css/js/sourceGroup/sourceLine/createdAt/updatedAt。html 只写片段(不要 <!DOCTYPE>/<html>/<head>,沙箱会补全)。.example-stage 舞台容器包裹;若样式依赖 body 选择器,改写为 .example-stage(参考 src/utils/sandbox.ts buildSrcDoc)。pnpm dev → 打开 /examples 按分组筛选找到新卡片 → 进入 /example/:id 检查预览渲染、源码 Tab、设备切换、变体(若有 snippets)。临时改动不想写回 JSON 时,可用详情页编辑+保存(写入 localStorage 覆盖层)先行试验。format 脚本含 json);pnpm lint && pnpm typecheck 后提交,Conventional Commits 格式(如 feat(data): 新增 xxx 示例)。不要向后端
examples表导入新内容:前端已不读该表(见src/api/examples.ts头注释与 04 篇 §6)。 若需全新分组:除新建 JSON 外,必须在src/api/examples.ts的GroupKey联合类型与groupLoaders表各登记一行(漏登内容不可见)。
src/components/examples/demos/<kebab-name>/<Name>Demo.vue,组件用 <script setup lang="ts">,自包含(第三方库走项目既有依赖或确认新增)。src/views/PlaygroundPage.vue 增加一行 const XxxDemo = defineAsyncComponent(() => import('@/components/examples/demos/...')),并登记到演示清单(页面内的选择器数据结构处)。src/views/css-lab/css/generators/XxxGenerator.vue:组合 BaseGenerator.vue,用 generatorTypes.ts 的 GeneratorSection/GeneratorParam 声明参数(仅 slider/color/select/text/number/toggle 六类控件)。generators/index.ts 登记.kind → 组件映射(路由 css/gen-:kind 自动承接,无需改路由文件)。src/config/css-lab/catalog.ts 追加 ToolItem:id 建议 css-gen-xxx、link 必须用 toolPath('gen-xxx') 派生(src/config/css-lab/routes.ts)、选择子分类 categoryId 与 accent 十色之一(src/types/tool.ts)。/css-lab/css/gen-xxx 直达、代码复制浮层。按 03 篇 §5 的五步清单执行(entity → service → controller → module 注册 → 前端 api 封装),结构变更生成迁移并 migration:run;公开端点标 @Public(),写端点挂 AdminGuard。
src/views/NewPage.vue(复用 SidebarCardGridPage 等既有页型)。src/router/index.ts 注册懒加载路由 + meta.title;需缓存则加 meta.keepAliveComponent(App.vue 自动收集);需登录加 meta.requiresAuth。useHeaderNav.ts 导航、SiteFooter.vue 页脚、HeaderSearchModal.vue 搜索索引。/tools/* → /css-lab/*)。| 现象 | 根因定位 | 处理 |
|---|---|---|
| 首页主题目录为空/报错 | 后端未启动或 /api/topics 失败(exampleStore.error) | 启动 server(§2.2)或接受内容板块直连使用 |
| 登录后刷新即丢会话 | localStorage 被禁/隐私模式(authStore.persistSession 静默降级) | 换常规模式;cookie 回退通道需后端 CORS credentials |
| 示例预览白屏 | 源码 JS 报错或引用了不存在资源 | 检查 needsSameOrigin 是否该置 allow-same-origin;<base> 依赖同源 public/ 资源 |
| JS 顶级重复声明报错 | 导入数据重复拼接 | sandbox.ts 的 deduplicateRepeatedJs 已自动处理完全重复段;手工修 JSON |
gsap 插件报 Indexed property setter 类错误 | gsap 多实例污染 | 确认未破坏 vite.config.ts 的 dedupe: ['gsap'] 与 optimizeDeps gsap 插件列表 |
| 构建内存溢出 | 全量类型检查 + 大量 chunk | 用 pnpm build(8GB 堆)勿改小;临时可 build:fast |
| 发版后页面白屏一次后恢复 | 旧 chunk 404 触发自愈 | 正常现象(router.onError → reload) |
| 收藏重复插入失败 | favorites 表 userId+exampleId 联合唯一 | 属预期幂等保障 |
排查问题的通用次序建议:先定位层,再定位点。现象是"内容不显示"时,先确认数据域(04 篇三分法)——示例内容问题查 JSON 与沙箱链路,目录内容问题查后端接口返回;现象是"行为不生效"(收藏失败、登录失效)时,先开浏览器 Network 面板看请求的状态码与负载,401 系问题沿"本地会话 → Bearer 头 → 后端校验"三段排查;现象是"构建或加载异常"时,vite.config.ts 的注释几乎记录了每一次历史事故的根因与对策,先读注释再动手是这个仓库里的高效习惯。另一个实用技巧是善用 localStorage 键名清单(04 篇 §4.3 的四张表):大量"诡异"现象——改了代码没生效、别人能看到我的收藏、示例还是旧样式——最终都指向某个本地键的残留状态,清掉对应键即可复位,无需怀疑代码。
再补充环境差异类问题的定位经验。本地正常、部署后异常的案例,九成落在三个差异点上:其一是路径差异——本地 dev 由 Vite 处理历史回退与代理,静态托管需要自己配 SPA fallback 与 /api 反代,漏配的表现恰好是"刷新深链 404"或"接口全部失败";其二是协议与网络差异——<base href> 指向的 origin、Google Fonts 外链在目标网络的可达性、https 页面对 http 资源的混合内容拦截;其三是环境变量差异——VITE_API_BASE_URL 是构建期注入的,改 env 后必须重新构建而不是重启进程,这是从传统后端环境变量思维迁移过来时最常见的误区。对照这三点检查,多数部署后问题可以在十分钟内收敛到根因。
项目自带运维文档(docs/上线运维/部署与上线策略.md、docs/上线运维/运维检查清单.md,从 docs/目录索引.md 可达)。结合源码,上线前最低限度核对:
JWT_SECRET 为随机强密钥(后端启动安检通过)DB_PASSWORD 已设置、DB_SYNC 未开CORS_ORIGIN 为真实前端域名列表dist/ 全量发布(含 shared-assets/、static-sites/)VITE_API_BASE_URL 已在构建期注入pnpm migration:run 幂等验证通过/admin 入口已做访问限制本篇完。全文档集共 7 篇:00 总览 | 01 架构 | 02 页面 | 03 接口 | 04 数据模型 | 05 核心流程 | 06 部署运行(本篇)