2.1 仓库搭建:Node 项目结构规范

第一篇的 Hello World 只有一个 index.html,整个“项目”装在一个文件里,复制一遍就能部署。但创业页不可能永远是单页:产品介绍、价格表、博客、活动落地页,每周都在加。我是周一早上被一份微信传输文件搞崩溃的:对方发了“最终版 v3 真的最终.zip”,解压出来和我们线上版本根本不是一回事。那天我决定,不管网站多小,先 git init。

本节不讲花哨的架构,就讲一个 Vue 静态创业页在 Node(TypeScript)技术栈下,仓库目录怎么分、package.json 和 tsconfig 怎么写、哪些文件打死也不能提交。这些约定在后面 2.2 构建脚本 和第三篇的托管接入里都会直接用到。

2.1.1 先定目录:源码与产物分离

核心原则一句话:源码和构建产物必须分开,构建产物(dist)原则上不进仓库。目录长这样:

cloud-pages-node/
├─ src/                # 前端源码
│  ├─ pages/           # 各业务页(home / product / blog)
│  ├─ components/      # 可复用组件
│  ├─ styles/          # 全局样式
│  └─ assets/          # 需要构建处理的图片与字体
├─ scripts/            # Node 构建脚本(TS 写的工具链)
├─ public/             # 不经过构建、原样拷贝的静态资源
├─ dist/               # 构建产物(gitignore)
├─ docs/               # 项目文档与部署记录
├─ package.json
├─ tsconfig.json
└─ .gitignore

各目录的职责约定,团队对齐后就贴在 docs/ 里:

目录 放什么 进仓库 说明
src/pages 业务页面源码 是 每页一个文件夹,独立可删
scripts/ Node 工具脚本 是 压图、部署、清缓存
public/ favicon、robots.txt 是 构建时原样拷贝
dist/ 构建产物 否 部署时现构建,不存档
docs/ 架构决策、排障记录 是 新人 onboarding 就看它
node_modules 第三方依赖 否 npm ci 按锁重建

把 dist 和 node_modules 挡在仓库之外,是后面 CI 能跑快的前提——这两项动辄上万文件,一旦提交,git clone 就要从几秒变成几分钟。

2.1.2 初始化:npm init 之后改三处

git init
npm init -y

npm init -y 生成的 package.json 是模板,有三处必须手改:

{
  "name": "cloud-pages-node",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "node --watch scripts/serve.ts",
    "build": "npm run clean && npm run compile"
  },
  "engines": { "node": ">=20" }
}

private: true 防止哪天手滑 npm publish 把内部项目发了出去;type: module 让 .js/.ts 默认走 ESM,import 写起来比 require 顺;engines 锁 Node 大版本,CI 机器装错版本时 npm 会直接报错,而不是跑出莫名其妙的结果。

版本号语义化:0.x 阶段怎么改都行,一旦开始往 腾讯云网站托管 发布正式版,就用 npm version patch 管理发布节奏,别手动改版本号。

2.1.3 TypeScript:一个目录两摊事

src/ 里是给浏览器跑的前端代码,scripts/ 里是给 Node 跑的构建脚本,运行环境完全不同。我一开始图省事用一份 tsconfig,结果 scripts 里调 process.argv 报全局类型缺失,前端代码里又混进 Node 类型。最后分摊处理:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src", "scripts"]
}

strict: true 头两周会让人想关掉,但配合 2.2 的构建脚本,类型错误在本地就拦住了,总比部署后白屏好。Node 脚本直接用 node --watch scripts/xxx.ts 跑,Node 22 内置类型剥离,无需 ts-node 这一层。

2.1.4 .gitignore:几行不能省

node_modules/
dist/
.env*
.DS_Store
*.log

最危险的是 .env。我们早期把 COS 的 SecretId/SecretKey 写进 .env 本地调试,某次同事差点把它加进暂存区,还好 .gitignore 里的 .env* 生效,git status 看不到它。密钥统一走云函数环境变量或控制台配置,落盘文件只放非敏感配置,这条红线在 腾讯云实践案例集 里也有强调。

2.1.5 分支与协作:main 加 dev 就够了

两个人的团队别整五条分支。约定只有两条:

  • main:部署来源,merge 前本地 npm run build 必须通过;
  • dev:日常开发,功能做完合并回 main。

配合第五篇的 CI 自动部署,push main 即上线。hotfix 分支只在紧急修线上 bug 时开,用完即删,绝不沉淀。

2.1.6 编辑器与格式化:消除代码风格争论

风格之争最浪费两人团队的时间。我们花半小时定了三条规矩,写进 .editorconfig 和 .prettierrc:

# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
{
  "semi": true,
  "singleQuote": true,
  "printWidth": 80,
  "trailingComma": "all"
}

prettier 挂进 git 提交钩子(用 simple-git-hooks,比 husky 轻量),提交时自动格式化。从此 review 里再没出现过“空格还是 Tab”的讨论,diff 也干净了。注意 prettier 只管代码文件,md 文档交给团队手动约定:标题与正文之间空一行,列表统一中划线开头。

2.1.7 踩过的三个坑

坑一:dist 被提交。有次构建脚本输出路径写错,产物落到了 dist-backup,.gitignore 只忽略了 dist,几十个文件进了仓库。清掉用 git rm -r --cached,再补忽略规则,全员重新 clone 才彻底干净。

坑二:Windows 大小写。同事在 Mac 上建了 components/Header.vue,我在 Windows 上写成 header.vue,本地能跑、CI 挂。统一在 docs/ 里写死命名规范:文件名一律小写中划线,组件文件名除外。

坑三:lockfile 漂移。npm install 每次都可能更新小版本,CI 上 npm ci 与本地不一致,出现过本地通过、CI 失败的幽灵 bug。团队约定:装包后必须提交 package-lock.json,CI 统一 npm ci。

小结

仓库规范不是写文档写出来的,是被“最终版 v3.zip”教育出来的:源码与产物分离、两份 TypeScript 运行环境、.env 不入库、main/dev 双分支。地基打好了,下一节 2.2 构建脚本 就在这个目录里开始写第一个 npm script。

延伸阅读