ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

全栈项目Day2:搭建前后端骨架并实现数据联调

全栈项目Day2:搭建前后端骨架并实现数据联调

day2,这个标签在连续打卡类开发计划里,是最容易翻车的位置。第一天大家都热血沸腾,需求清单写满两页纸,技术选型讨论半天;到了第二天,打开编辑器面对一片空白,很多人就卡住了——不知道今天该干嘛。我给自己定了一个30天全栈小项目的挑战,day2的任务被我从源头收敛得非常具体:把第一天选定的技术栈落成一个能跑的前后端骨架,并且让前端页面能真实拿到后端返回的数据。这一天做的事情乍一看不酷,也不产生什么“看得见的功能”,但它是整个项目后续30天能不能稳定推进的地基。这篇文章会把我day2的操作记录、目录设计、联调过程、踩坑经历完整摊开。如果你也在做类似的连续学习或项目打卡,这些内容可以直接照着落地,不用重复踩我走过的坑。

1. 项目整体设计与Day2目标拆解

1.1 把“第2天”拆出明确边界

第一天做规划时,我给自己定的规矩是不写代码,只做三件事:确定项目要解决一个什么问题、画出核心模块清单、给技术栈列候选人。这三件事做完,除了几张草稿纸和几个文档,没有任何能运行的东西。这也是为什么很多连续打卡计划会死在第二天——目标太模糊,规划再漂亮,落到编辑器里就变成“打开一个新项目,然后发呆”。

为了避免这种情况,day2开始之前,我把当天的目标压成三个可以验收的点:

  1. 前后端项目都能在本地用一条命令启动,并且访问地址固定不变。
  2. 目录结构按照功能域来组织,而不是按文件类型堆在一起,为后续增量开发留出稳定上下文。
  3. 打开前端页面时,浏览器能通过代理把请求发到后端,拿到接口数据并渲染到页面上。

这三个点都有一个共同特征:每一项都能明确判断“完成”或“没完成”,不存在模糊地带。在这个基础上,我在文档里给每一天都定义了一个验收模板,格式很简单:今天要完成什么、完成的标志是什么、如果没完成明天要怎么处理。这套模板在day1其实就写好了,day2只是第一次正式使用。实践下来,它最大的作用是帮我在中途想“要不要顺手多做一个功能”的时候踩下刹车——如果这个功能不在今天的验收清单里,那就列入明天再做。连续打卡真正考验的从来不是一天能写多少代码,而是能不能一直稳定地推进,所以控制每天的任务颗粒度,比堆工作量重要得多。

如果某天真的因为意外没能完成当天的目标,我的处理方式是:不补进度,把目标顺延到第二天,但第二天只保留这一个目标,不再叠加新任务。这样做的原因很朴素——连续打卡的意义在于连续性,如果为了赶进度而连续几天熬夜,那这种“打卡”大概率坚持不到一周就会崩。允许自己偶尔放慢节奏,但绝不允许自己没有完成感地混过一天。

1.2 技术选型的取舍理由

技术选型这个环节,最容易犯的错误是“跟风”。今天看到某框架很火就想换过去,明天看到别人发了新工具又觉得不错。day2我不能再纠结这个,应该在day1定下的基础上直接开工。当时我的决定是:前端用 Vue 3 + Vite + TypeScript,后端用 Node.js + Express,数据存储暂时用最简单的方案,等业务逻辑出现后再定。

选Vue 3而不是其他框架,理由很实际:对单人、小项目的开发节奏来说,它的学习曲线最平缓,单文件组件的写法很直观,类型集成也不绕。Vite作为构建工具,冷启动速度是肉眼可见的快——我实测过,一个空白项目从敲命令到页面能访问,基本是秒级完成。这种细节在一天只写一点代码的打卡节奏里很重要,因为等待启动的时间,其实是消耗耐心最厉害的地方。

后端选Express,核心原因是这个阶段根本不需要重型的应用框架。Express的中间件模型足够表达“接收请求、解析参数、返回响应”这一整套最基础的链路,等项目的业务复杂度上来了,路由、控制器、服务分层都可以在现有基础上循序渐进地加,而不是一上来就背上一个沉重脚手架。我给自己定的选型原则就三条:能简单就不复杂,能用现成的就不造轮子,不追求“显得很专业”,只追求“能稳定跑完这个项目”。

2. 骨架搭建:目录、配置与初始化细节

2.1 脚手架初始化:一条命令背后的约定

前端的初始化我直接用 Vite 官方脚手架:

npm create vite@latest frontend -- --template vue-ts

执行完,frontend 目录里已经有一个完整的 Vue 3 + TypeScript 工程,自带开发服务器、构建脚本和基础配置。我做的第一件事不是写代码,而是清理模板自带的示例组件和样式。这一步很多人会忽略,结果项目里残留着一个和自己需求毫无关系的页面,每次打开都是那个图标,容易让人搞混。清理完,目录里只剩下干净的入口文件、App.vue 和必要的配置。

后端的初始化更加朴素,手动创建 backend 目录,然后:

npm init -y npm install express

这样得到一个最小化的 Node.js 项目。手动初始化的好处是,每一行结构都知道是用来干嘛的,不会出现脚手架生成了一堆不知道用途的配置。但手动初始化的代价是,所有的工程规范都要自己立。day2我没有引入 ESLint 和 Prettier 这类工具,不是它们不好,而是这个阶段的代码量很小,手写规范完全够用;等代码量上来了,再在一个空闲的日子里一次性配置好,比在每一天里分心处理工具配置要高效得多。

这里有一个在连续开发中经常被忽视的点:脚手架自带的版本和配置,在你的运行环境里不一定兼容。我在初步完成后验证了一下启动命令,发现前端可以正常启动,但构建时有几条告警。经过检查,确认是模板默认的 Node.js 版本要求和我本机环境存在差异导致的。遇到这种情况,正确做法是调整本机工具链版本,而不是反过来去改动项目依赖。这个细节我在后面的排查章节还会展开。

2.2 目录结构设计的意图

目录结构这件事,决定了未来30天每天写代码时“该把文件放哪”的直觉。如果一开始就乱放,后面每加一个功能都要纠结一下文件位置,这种纠结虽然小,但每天都在消耗注意力。

很多初学者习惯按文件类型分目录:一个文件夹放所有页面,一个文件夹放所有组件,一个文件夹放所有工具函数。这套方案在项目很小时挺直观,但模块一旦变多就难受了——要改一个用户相关的功能,你需要在页面、组件、工具、接口四个文件夹里跳来跳去才能看清全貌。day2我选择按功能域来组织:

frontend/src/ ├── api/ # 接口请求封装,一个文件对应一个领域 ├── views/ # 页面级组件 ├── components/ # 可复用的局部组件 ├── router/ # 路由配置 ├── stores/ # 全局状态 └── types/ # TypeScript 类型定义

后端目录同样强调功能归属:

backend/ ├── src/ │ ├── routes/ # 路由定义层 │ ├── controllers/ # 请求参数处理 │ ├── services/ # 业务逻辑 │ └── index.js # 应用入口 ├── .env └── package.json

这个结构不是最复杂的,但它足够清晰。核心优势是“稳定性”:一旦确定下来,后续每天的新代码都有固定的去处,你不需要反复思考文件该放哪里。比如第10天要加一个收藏功能,流程就是:api 加一个收藏请求文件、views 加一个收藏页面、router 挂一条路由、如果需要状态就在 stores 加一个模块,完事。对连续打卡来说,这种稳定的开发节奏比任何花哨的架构都更重要。

另外,目录结构一旦定下来,就不要随便改。我知道有人会在项目中途突然觉得结构不好,然后花一整天重构目录。这个收益在项目早期几乎为零,成本和风险却很高。骨架期定的结构可能不完美,但只要它能容纳接下来一周的增量迭代,就足够了,等真正遇到结构瓶颈时再来调整不迟。

2.3 环境变量与配置管理

配置管理看着基础,实则坑很多。day2我给自己定了一条规矩:不允许出现硬编码的端口、地址和密钥,全部收进环境变量。

后端的环境变量放在backend/.env:

PORT=3000

代码里通过 dotenv 插件读取。这里写的端口是经过考虑的:3000 是开发场景非常常用的端口,不容易和前端默认的 5173 冲突,而且生态兼容性好。

后端入口里这样读取:

import 'dotenv/config'; const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`[server] running at http://localhost:${port}`); });

前端这边,Vite 开发服务器的代理目标同样不写死,而是读取环境变量。这样设计最大的收益在项目后期——当你要把项目部署到服务器时,只需要改 .env 文件,不需要翻代码找那些写死的地址。这个习惯越早养成,后期重构的成本就越低。

需要注意的是,.env文件默认是不应该提交到版本仓库的,因为它将来会存放敏感配置和不同环境的差异配置。我在.gitignore里把.env直接排除掉,同时准备了一份.env.example提交到仓库,里面只写变量名和示例值,别人 clone 下来后复制一份改成自己的配置就能跑。这个做法在协作时会显得特别重要,单人项目也建议从一开始就养成这个习惯。

3. 前后端联调:让页面真正拿到后端数据

3.1 先把后端跑起来

很多教程喜欢把前端后端放在一起讲,但实操里应该先把后端单独跑通。这一步虽然简单,却能为后面的联调减少一半的排查时间。

后端入口代码我写得很克制:

// backend/src/index.js import express from 'express'; import 'dotenv/config'; const app = express(); app.use(express.json()); app.get('/api/health', (req, res) => { res.json({ code: 0, message: 'ok', time: Date.now() }); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`[server] running at http://localhost:${port}`); });

这里我特意定义了/api/health作为健康检查接口,后面所有联调都先从这个接口开始。如果这个接口不通,那就说明基础设施有问题,不应该继续往下开发功能。启动命令是:

node src/index.js

看到终端输出 running 日志后,我用浏览器打开地址http://localhost:3000/api/health。页面返回 JSON 数据的时候,后端链路就确认正常了。单独验证这一步的价值是:当后续联调失败时,你不需要同时怀疑前端和后端,而是可以非常有底气地判断问题出在哪一侧。

在这个阶段我还顺手验证了一件事:接口返回的time字段用的是服务端时间戳,说明数据确实是从后端实时生成的,而不是被某些缓存策略挡在了中间层。这一开始可能觉得多余,但当你后面引入缓存、CDN 或者部署到服务器上时,这种“确认数据来源”的敏感度会帮上大忙。

3.2 前端代理转发与跨域处理

前端开发服务器默认端口是 5173,后端是 3000,两个不同源之间直接 fetch 会被浏览器的同源策略拦下来。处理这个问题有两种常见做法:一种是后端开启 CORS,另一种是前端开发服务器做代理转发。我选了后者。

为什么不选 CORS?因为 CORS 需要后端额外配置一堆响应头,而且它在部署阶段还有各种各样的边界情况;代理转发则是开发服务器天然支持的能力,配置一行就行,上线后由反向代理统一处理,逻辑更干净。

在 Vite 配置文件里加代理:

// vite.config.ts import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, });

这段配置的意思是:凡是请求路径以/api开头的,开发服务器都把它转发到http://localhost:3000。注意 target 不能写错端口,此外changeOrigin一定要设置成 true。这个参数的作用是改写请求头里的 Host 字段,让后端感觉这个请求就像来自同一个域名,很多代理配了但仍然报跨域错误,八成是在这里漏掉了。

配置完成后,建议先重启一次前端开发服务器再验证联调。因为 vite.config.ts 这类配置文件不会在页面刷新时自动重载,这也是我后面会反复提到的“改配置必须重启”的典型场景。

3.3 接口联调验证与数据打印

代理配好了,接下来写前端页面。day2不需要什么华丽功能,我的目标只是让页面真实地拿到后端数据并显示出来。为此我封装了一个最简单的请求工具:

// frontend/src/api/client.ts const request = async (url: string) => { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return res.json(); }; export const getHealth = () => request('/api/health');

然后在页面组件里调用:

<script setup lang="ts"> import { ref, onMounted } from 'vue'; import { getHealth } from '../api/client'; const status = ref('加载中...'); const time = ref(''); const init = async () => { try { const data = await getHealth(); status.value = data.message; time.value = new Date(data.time).toLocaleString(); } catch (e) { status.value = '请求失败:' + (e as Error).message; } }; onMounted(init); </script> <template> <div> <h1>Day2 联调验证</h1> <p>后端状态:{{ status }}</p> <p>后端时间:{{ time }}</p> </div> </template>

我特意在 catch 分支写了错误提示,而不是让请求失败时静默。这是个好习惯:页面出错时,至少能知道是请求 URL 写错了、后端挂了还是网络层出了问题。验证时,我打开浏览器 DevTools 的 Network 面板,确认/api/health请求的 Status 是 200,响应体里有预期的字段,再切到页面看渲染结果。这一步做到位,day2 的三个目标才算全部通关。

这里我再补充一个细节:联调时不要只看页面显示对了就收工。页面显示对,只能证明这条链路是通的;但你还应该确认控制台没有报错、Network 面板里没有 404 或 500、请求头里的 Origin 和 Host 符合预期。这些信息才是后续排查问题的底牌。

4. 常见问题排查与避坑记录

4.1 端口占用与重启策略

实际操作中,端口占用是最常见的问题。我在启动后端时有一次终端直接报EADDRINUSE,说明 3000 端口已经被占用。排查思路很简单:先查看哪一个进程占用了这个端口,确认是上次没关干净的后端进程后,结束掉再启动。

这里要特别提醒的是,在开发机上结束进程时要确认它确实是自己跑起来的,不要误杀其他项目的进程。除此之外,我还养成了一套固定的开发启动顺序:先把后端启动起来,确认日志正常,再启动前端开发服务器。这个顺序能保证联调时不会出现“前端开了,后端忘了”的尴尬。

关于重启策略:day2 我没有引入热重载工具,所以后端每次改完代码都必须手动重启。这个流程看起来原始,但好处是让人对“代码生效”这件事有明确的掌控感,不会出现改了半天以为生效了,实际跑的还是旧代码的情况。等后面项目变大,再考虑引入热重载也不迟。前端那边的热更新相对完善,改业务代码保存后页面会自动刷新,但需要注意vite.config.ts等配置文件仍然需要整个重启开发服务器。

4.2 依赖版本不一致导致的诡异报错

安装完前端依赖后,我一启动就报语法错误,报错信息指向一个第三方库的源码。初看很吓人,仿佛库有问题。但冷静下来梳理会发现一个关键细节:报错原因是这个库使用了当前 Node.js 版本不支持的语法特性。问题不在库,在我的运行环境版本太旧。

排查这类问题,我的顺序是:先读报错信息里的模块名和行号,确认指向的是第三方库还是自己的代码;如果是第三方库,先检查 Node.js 的版本;最后确认本机环境是否符合项目要求。实测下来,绝大多数“莫名其妙”的报错,根源都是环境版本不匹配,和业务代码没有半毛钱关系。

一个延伸出来的习惯是:每次开始一个新项目,先记录当前运行环境的版本信息,再决定要不要升级。保持稳定优先,不要为了“用新版本”而用新版本,这是连续打卡项目能否长期稳定推进的一个隐形前提。环境这种东西,越接近稳定越容易写出可预期行为的应用,反之则会时不时给你添乱。

4.3 配置不生效的排查路径

还有一次我改完 Vite 代理配置,刷新页面却发现请求仍然打到原来的地址上,第一反应是“配置写对了没有”,反复检查也没发现语法错误。最后才意识到问题出在:Vite 配置文件的修改需要重启开发服务器才能生效,刷新页面不会重新读取配置文件。

这里想分享一个通用的排查原则:当程序行为没有按照预期变化时,先别急着怀疑逻辑,先确认“修改是否真正加载了”。对于配置文件来说,最直接的验证方式就是重启进程后看行为是否改变。如果重启了还是不对,再进入下一步:检查是否有一个优先级更高的配置在覆盖它,或者实际加载的配置文件根本不是你以为的那一个。

这套排查路径看起来简单,但能解决开发中大量的“我明明改了但没生效”问题。我把这类的常见情况整理成一个速查表,方便以后查看:

现象第一反应第二反应
改了配置文件但行为没变重启对应服务检查加载的是否是当前文件
端口被占用查看占用进程并处理确认占用者是自己项目还是其他程序
前端请求跨域检查代理是否生效检查 changeOrigin 是否设置
安装依赖后启动报错查看报错模块核对 Node.js 版本要求

5. 连续打卡类开发怎么稳住节奏

5.1 每天早上先定今天能验收的事

连续打卡计划最常见的死法是“第二天热情还在,但目标太大”。day2 的成功经验让我意识到,每天开工前一定要花 5 分钟想清楚:今天做完什么,就算这一天赢了?

这个验收标准不需要宏大,甚至可以是“把环境搭起来并跑通一个接口”。关键是它必须足够小、足够清晰,让你在晚上回顾时能明确地说出“做到了”或“没做到”。写到一半想顺手加功能时,用这个标准作为刹车,可以避免每天的任务无限膨胀。

我个人的实际体会是:连续开发中,完成感比成就感重要。成就感需要做大事,而完成感只需要每天兑现一个小承诺。小承诺不断兑现,项目就会像滚雪球一样自然地往前推进。而且这种节奏有一个额外的好处——因为每天目标不大,你有余力去观察和思考,而不是一直埋头赶进度。很多好设计,反而是在这种“不太忙”的状态下冒出来的。

5.2 用轻量记录代替流水账

打卡项目最容易犯的第二个毛病,是把记录写成流水账——今天做了什么、改了哪些文件、跑了哪些命令。这种记录当下觉得很充实,一周后回看基本没有任何信息量。

我更推荐的记录格式是“决策型日志”:今天做了什么决策、为什么这么决策、踩了什么坑、如果再来一次会怎么避免。我今天的记录是这样的:选择代理转发而非 CORS,因为开发环境代理只需一行配置,且上线后由反向代理统一处理,整体链路更简单;踩坑:修改 vite.config.ts 后必须重启开发服务器,刷新页面不会重新加载配置。这几十个字,三个月后回看,依然能想起当时的全部背景。

这些决策记录在项目后期会异常宝贵。当你想回看为什么某个模块要长这样、为什么当初没选某个方案时,这些日志能直接告诉你答案,不需要靠猜。这也是连续打卡和一次性开发最大的区别——节奏让过程可以被记录,记录让经验可以被沉淀。

day2 跑通,骨架落地,第一轮前后端数据已经在浏览器里渲染出来了。回看这一天,真正让我觉得值得的不是那几行代码,而是三个被验证的决策:功能域目录结构、代理转发方案、拒绝硬编码的配置管理。这三条看起来不炫技,但后续30天里每一个新增功能都在享受它们的红利。

如果你也在做连续开发或学习打卡,我给的建议是不用纠结每天要产出一个多牛的成果,把今天真正做完、做一个能验收的事就够了。积累两三个星期你会发现,一天一点真的比一次猛冲要强。明天继续更新 day3 的业务模块实现,到时候见。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进