项目:PEXELS 壁纸应用 | 版本:v1.0 | 生成日期:2026-10-06 | 依据:源码分析
| 项 | 要求 | 依据 |
|---|---|---|
| Node.js | 建议 20.19+ 或 22.12+(Vite 8 的最低要求) | vite ^8.2.2(rolldown 内核);package.json 未声明 engines 字段,仓库层不做强制 ⚠️ 待确认(以实际 CI/部署机为准) |
| 包管理器 | pnpm(仓库含 pnpm-lock.yaml,README 命令均以 pnpm 示例) | pnpm-lock.yaml |
| 网络 | 需能访问 api.pexels.com(真实数据)、picsum.photos 与 commondatastorage.googleapis.com(降级占位) | src/utils/pexels.ts 的 BASE URL 与降级生成器 |
| Pexels API Key | 可选;不配则全站降级为占位数据 | src/api/pexels.ts 的 `client = API_KEY |
.npmrc 设有 optional=false:禁用全部可选依赖,使 vite 的 optional 依赖 sass 及其 @parcel/watcher 不被安装,规避 pnpm 11 的 [ERR_PNPM_IGNORED_BUILDS] 构建脚本报错(文件内注释原文)。
# 1. 安装依赖(锁文件锁定版本)
pnpm install
# 2. 配置环境变量(可选):复制模板并填入 Pexels Key
cp .env.example .env.development
# 编辑 .env.development:
# VITE_PEXELS_API_KEY = '<在 https://www.pexels.com/api/ 申请的 Key>'
# VITE_PEXELS_PROXY_BASE = '' # 本地一般留空,走内置代理
# 3. 启动开发服务器
pnpm dev # 即 vite --host,监听 0.0.0.0,便于手机连同一局域网真机调试
# 4. 类型检查(建议提交前执行)
pnpm type-check # vue-tsc --noEmit,strict 模式检查(唯一纯 JS SFC 盲区见第 5 节)
# 5. 生产构建与本地预览
pnpm build # 产物输出 dist/
pnpm preview # 本地静态预览 dist/开发期的两条网络行为需要理解:
VITE_PEXELS_PROXY_BASE 时,开发环境的 Pexels 请求自动走 Vite 同源代理 /api/pexels(vite.config.ts 的 server.proxy,target https://api.pexels.com/v1、changeOrigin: true、rewrite 去前缀)。文件内注释说明目的:规避 localhost 来源跨域直连 Pexels 被 Cloudflare 拦截;线上走真实域名直连不受影响。client = null,首页/视频/合集均为 Picsum 占位与示例视频——这是预期行为而非故障(README.md 常见问题第一条)。各命令的职责边界再明确一遍:pnpm dev 是唯一会启用 /api/pexels 代理与 import.meta.env.DEV 分支的命令;pnpm build 按 production 模式加载 .env.production,产物里 import.meta.env.DEV 为 false,因此通道计算直接跳过内置代理;pnpm preview 只是静态伺服 dist/,不执行任何代理逻辑——用它验证"直连 Pexels"时请通过本机 hosts 或浏览器改写把域名指过去,否则更可靠的做法是配一个本地反向代理并把 VITE_PEXELS_PROXY_BASE 指向它。pnpm type-check 与构建互相独立,CI 中应作为独立步骤执行(构建不会做全量类型检查,vite 的 esbuild 转译只剥离类型、不校验类型)。
环境变量变更后必须重启 dev server——Vite 只在启动时读取 .env.* 文件,热更新不会重新求值 src/api/pexels.ts 的模块级 API_KEY/PROXY_BASE 常量(它们是模块首次加载时确定的)。这也是把客户端做成模块级单例的副作用:改 Key 必须重启,改完不重启的"看起来没生效"基本都是这个原因。
| 变量 | 加载模式 | 是否被代码读取 | 说明 |
|---|---|---|---|
VITE_PEXELS_API_KEY | development / production | 是(src/api/pexels.ts;类型声明在 src/env.d.ts) | Pexels API Key,直连 api.pexels.com/v1 时经 Authorization 头透传(无 Bearer 前缀)。会被打进前端产物,任何访问者可提取盗用配额。留空且无代理时全站降级占位 |
VITE_PEXELS_PROXY_BASE | development / production | 是(同上) | Pexels 反向代理地址;配置后请求改走代理,Key 由服务端注入,前端产物不含 Key。生产环境推荐。dev 未配置时自动回退 /api/pexels(Vite 代理) |
VITE_BASE_API | development(/api)/ production(/prod-api) | 否(历史遗留) | 自有后端时代的变量,随账号体系移除已无读取方;.env.example 注释与 src/env.d.ts 类型声明仍保留痕迹 |
VITE_APP_SECRET | — | 否(历史遗留) | 接口签名盐值,同上,无读取方 |
补充说明:.env.development 与 .env.production 均被 .gitignore 忽略(.env 与 .env.*,仅 !.env.example 放行模板),注释写明"含 API Key 等敏感信息,禁止入库"。但当前工作区里的 .env.development / .env.production 实际写入了一个真实 Key——属于敏感信息落盘,建议在部署前于 Pexels 后台轮换该 Key,并只以环境变量注入或改用代理方案(见下文"安全注意事项")。
pnpm build → dist/)构建策略全部位于 vite.config.ts:
manualChunks):vendor-vue(vue、@vue、vue-router、pinia)、vendor-utils(@vueuse/core)、vendor-animation(gsap)、vendor-media(file-saver)、其余 node_modules 落 vendor-misc;业务代码按路由自动分包(所有页面组件均 () => import() 懒加载)。注释强调 @vue 必须单独列出,否则整个 Vue 运行时会落进 vendor-misc 并被入口预加载。chunkSizeWarningLimit: 300:注释明确"不要为了消除告警而调大它:这是发现分包退化的唯一信号"。构建时若出现告警,应先查是否又有大依赖漏配 manualChunks。sourcemap: false:不把源码结构暴露到线上。reportCompressedSize: false:跳过 gzip 体积计算以缩短构建耗时。产物为纯静态文件(index.html + assets/*.js|css + favicon.ico),可部署到任意静态服务器 / CDN / 对象存储。
createWebHistory(src/router/index.ts),/pins/:id、/videos/:id、/collections/:id 等地址刷新或直接访问时需要服务器返回 index.html。以 Nginx 为例:server {
listen 80;
root /var/www/pexels-dist;
location / {
try_files $uri $uri/ /index.html; # SPA 回退,缺它则详情直达页刷新 404
}
}
# 可选:为 dist/assets 追加长缓存(Vite 产物带内容 hash)
# location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; }vite.config.ts 未设置 base,产物默认以 / 为根;若需部署在子路径,需同时补 base 配置 ⚠️ 待确认(当前源码无子路径部署的适配)。VITE_PEXELS_PROXY_BASE 时请求直连 api.pexels.com;src/api/pexels.ts 注释说明"线上真实域名来源,已验证可用",而 localhost 来源会被 Cloudflare 拦截(因此才有 dev 代理)。api.pexels.com/v1 的路由并在服务端注入 Authorization 头,再把该路由地址填入 VITE_PEXELS_PROXY_BASE(同源路径如 /pexels 亦可),重建部署。此后前端产物零 Key。index.html 引入了 https://analytics.zhangzhengyang.com/script.js(带 data-website-id,形如自托管 umami 的访问统计)。部署到自己的环境时若不需要统计,应删除该 <script>;保留则确认该统计服务可达 ⚠️ 待确认(仓库内无其服务端代码)。部署形态选型。纯静态产物的三种常见落法,按运维成本从低到高:其一,对象存储 + CDN(如 OSS/COS + CloudFront),只需开启"index.html 作为默认首页 + 404 回退到 index.html"两项(后者就是 SPA 回退的托管版实现),注意 HTML 文件应设短缓存或不缓存、assets 设长缓存;其二,Nginx/Caddy 自托管,按上文 nginx 示例配置,额外可以加 gzip/brotli 压缩(Vite 产物为未压缩 JS,文本压缩率高);其三,挂在已有网关的子路径下——这需要先补 vite.config.ts 的 base 并重新审视 postcss-px-to-viewport 与统计脚本路径,工作量不大但改动点分散,不建议首次部署就选它。无论哪种形态,都没有服务端进程、不需要 systemd/容器守护,发版即"覆盖文件 + 失效 CDN 缓存"。
发布校验清单:构建完成后建议按序确认四件事——dist/index.html 里统计脚本是否符合预期;assets/ 中出现 vendor-vue、vendor-utils、vendor-animation、vendor-media、vendor-misc 五类前缀即分包正常(缺失或合并说明 manualChunks 匹配失败);grep -r "VITE_PEXELS_API_KEY的值" dist/ 无命中(走代理方案时)或有命中且符合预期(直连方案时必然命中,这正是其风险所在);浏览器无痕窗口走一遍 06 第 5 节的手工验收路径。
项目没有单元测试 / E2E 测试(package.json 无 test 相关脚本与依赖,仓库内无测试目录),质量保障依赖三件事:类型系统(vue-tsc strict 检查,覆盖 29 个 SFC 中的 28 个,唯一纯 JS SFC 例外见下表)、格式统一(Prettier + Tailwind 类名排序插件)、运行时防线(全局 unhandledrejection 兜底与页面级错误分支),具体配置见下表。在此基础上,源码注释承担了"第四种测试"的角色——大量历史 bug 的成因与修复都写在触发点旁边(如空数据时 Math.min() 返回 Infinity 的兜底、left: undefinedpx 非法样式的防护、=== 判完成导致无限翻页的修正),改动对应模块前先读注释能避开绝大多数回归。
| 工具 | 配置 | 作用 |
|---|---|---|
| vue-tsc 类型检查 | pnpm type-check(vue-tsc --noEmit);tsconfig.json 开启 strict、noUncheckedIndexedAccess、noUnusedLocals/Parameters、noFallthroughCasesInSwitch,路径别名 @/* | 静态类型检查;实测 29 个 SFC 中 28 个为 <script setup lang="ts">,仅 src/views/videos/components/list/index.vue 为纯 JS(checkJs: false 使其按 JS 处理),该文件的内部类型与模板 props 校验不在检查范围内(《09-测试与质量保障》第 2.2 节实测口径,2026-10-06 一致性回刷修订);src/env.d.ts 注释说明刻意不加 *.vue shim,保证 lang=ts 的 SFC 模板内 prop 类型错误也能被查出 |
| Prettier | .prettierrc:semi: false、singleQuote: true、trailingComma: 'none'、printWidth: 100;插件 prettier-plugin-tailwindcss | 统一格式化并按官方顺序排序 Tailwind 类名;无 lint-staged/husky 钩子 ⚠️ 待确认(仓库未见相关配置),格式化靠手动执行 |
| 运行时防线 | src/main.ts 的 window.addEventListener('unhandledrejection') 全局兜底打印;docs/dependency-upgrade-plan.md 的"安全现状"章节 | 捕获未处理 Promise 拒绝避免静默失败;依赖安全状况记录在 docs 中 |
手工验收建议(对应核心流程):① 无 Key 启动应看到 Picsum 占位图与「示例合集」卡;② 配 Key 后切分类、搜索、滚动翻页;③ 点卡片看放大动画与地址栏变化,浏览器后退应关闭弹层;④ /pins/某个真实id 刷新直达;⑤ 视频页播放并切换清晰度;⑥ 切主题后刷新,主题应保持;⑦ 合集页停留观察封面排队加载与配额提示。
README.md 与 .env.example 反复强调 VITE_PEXELS_API_KEY 会被打进产物,任何访问者都能提取并盗用配额。这不是理论风险——打开浏览器 DevTools 的 Sources 面板就能在 JS 文本里直接搜到 Key 明文。生产环境的正确姿势是 VITE_PEXELS_PROXY_BASE + 服务端注入 Key 的反向代理,前端产物保持零 Key。.env.development / .env.production 中已填入同一真实 Key(两文件虽被 gitignore,但文件本身存在于工作区)。建议:轮换该 Key;若仓库历史中曾提交过含 Key 的 env 文件,视为已泄露并立即作废 ⚠️ 待确认(需查 git 历史确认是否入库过)。轮换操作在 Pexels 控制台完成,应用侧只需替换环境变量值并重建,无代码改动。/search、/curated 这类只读端点做短 TTL 缓存(比如热门分类关键词缓存几分钟)能大幅摊薄配额消耗。dist/ 不含 sourcemap;上线前可抽查 assets/*.js 确认无 Key 明文(改用代理后应 grep 不到 Authorization 相关值)。统计脚本属第三方域名,若有合规要求(隐私政策、Cookie 声明)需一并评估。| 现象 | 原因与处理 |
|---|---|
| 页面全是灰彩占位块/示例视频 | 未配置 Key(或代理),client=null 全量降级。配置 VITE_PEXELS_API_KEY 后重启 dev server |
| 控制台 401「API Key 无效」 | Key 填错或已被 Pexels 停用;此错误被刻意上抛不降级(src/api/pexels.ts) |
| 频繁 429「请求过于频繁」 | 配额耗尽(约 200 次/小时);合集封面自动进入 5 分钟冷却。缓解:代理层缓存、降低翻页频率、合集页每页本已降为 12 条 |
刷新 /pins/123 返回 404 | 服务器未配 SPA 回退(try_files ... /index.html) |
| dev 下样式与 prod 不一致 | px-to-viewport 必须显式配在 vite.config.ts(Vite 8/rolldown 下 postcss.config.js 对 .vue 样式块加载不可靠),勿迁移回 postcss.config.js |
| 构建出现 >300KB chunk 告警 | 分包退化信号(如新依赖落入 vendor-misc),在 vite.config.ts 的 resolveVendorChunk 补充分组,而非调大阈值 |
| 手机访问不了 dev server | 确认用 pnpm dev(含 --host),手机与电脑同网段,访问 http://<电脑IP>:5173 |
日常改动的标准路径。加新分类:只改 src/constants/index.ts 的 CATEGORY_NOMAR_DATA(id 必须唯一且是有效 Pexels 搜索词),导航、store、接口参数自动跟随。加新 Pexels 能力(如颜色筛选 UI):协议层在 PexelsClient.search 已有 color 参数、无需动;门面层给 PexelsListQuery 加字段并透传;页面层从筛选控件写入 store 或局部状态。加新页面:在 src/router/index.ts 挂路由,若需要 keep-alive,defineOptions name 与路由 name 保持一致并评估是否加入 CACHE_NAMES(缓存页必须同时实现滚动恢复与失活同步两个配套,参考三个一级导航页)。加新全局组件:放入 src/libs/<name>/index.vue 即自动获得 m-<name> 全局注册(异步);放不进通用库约束的业务组件放 src/components/。
依赖升级。docs/dependency-upgrade-plan.md 是本仓库的升级档案,记录了 Vite 2→8、Vuex→Pinia、Tailwind 3→4 的完整路径与六条踩坑(scrollbar 工具类失效、postcss 配置位置、rem 放大、图标 fill 陷阱、层叠上下文、persistedstate 4.x 选项变更)。再次升级前先通读它;该文件"待评估"清单里点名了 file-saver(2020 年后无维护但稳定,可用原生 <a download> 替代,需自行处理跨域)。升级后必跑 pnpm type-check 与第 2 节的手工验收清单,并观察构建是否出现 300KB chunk 告警。
性能预算。当前架构下的隐性契约:首屏 JS 只含 vendor-vue/utils 与首页代码(gsap、file-saver 都在异步链路上);瀑布流 DOM 数量恒定(视口±400px);图片走 medium/small 缩略档、大图只在弹层加载。引入新依赖前对照三条检查:是否进首屏包(若是,考虑 defineAsyncComponent 或动态 import)、是否会破坏 px-to-viewport 的 include 规则(纯 JS 库无影响)、是否新增运行时请求(对照 03 篇的配额清单)。