重生之我在 Vibe Coding 时代当程序员:第六课,第一个 AI 全栈项目
上节课我明白了一件事:AI 写代码越快,Git 越重要。它不是"程序员的好习惯",而是人和 AI 之间的安全栅栏。装备齐了之后,这节课拿到一个叫
user-chat的项目,两个字:跑通。打开目录,里面有前端、有后端,有一堆我从没见过的文件。我盯着这个项目停了几秒,意识到一件事:这次不是改别人写好的代码了,而是要自己把一个"有来有去"的完整应用从零搭起来------这是第一个 AI 全栈项目。
从一个目录结构说起
user-chat 的目录长这样:
lua
user-chat/
├── fe/
│ ├── index.html
│ ├── bootstrap.min.css
│ └── common.js
└── backend/
├── db.json
└── package.json
看到 fe/ 和 backend/ 两个文件夹,前后端分离------不是课本上的概念,而是物理上的:前端代码放一个文件夹,后端代码放另一个文件夹,两边各管各的,通过网络请求连起来。
这节课要搞清楚的主线是------数据是怎么从一个 JSON 文件流进页面的。
前端三件套:骨架、皮肤、神经
fe/ 里只有三类文件,分别做三件事:
index.html管"页面上有什么东西"------表格、标题、导航,每一个都是一个标签bootstrap.min.css管"这些东西长什么样"------颜色、间距、斑马条纹common.js管"数据来了怎么渲染进去"------发请求、拿数据、往表格里追加行
拆成三个文件有一个很实际的理由:改一处,全局生效,不影响其他 。如果把样式直接写在 HTML 标签的 style="" 里,想改颜色就要改五十个地方;如果把逻辑混在 HTML 里,一个文件几百行,找 bug 要翻半天。拆开之后,改样式只动 CSS,改逻辑只动 JS,改结构只动 HTML------这就是模块化:每个文件只做一件事,改动的爆炸半径被限制在一个文件里。
顺带一提,common.js 开头有一行注释我觉得很重要:
js
// 不够严格:单引号、双引号、反引号、分号、类型声明 不强求
JS 是弱类型语言,这意味着它对语法的要求比 Java 宽松得多------引号可以混用,末尾分号可以省略,变量不需要声明类型。这让 JS 的上手门槛很低,但也需要团队约定一套统一的风格。
HTML 骨架:盒子、块级行内、Bootstrap 行列
打开 index.html,整个页面在浏览器眼里是一堆套娃的"盒子"。
写 HTML 的原则是:先写盒子,再写内容 ------先决定大盒子的位置,再决定中间盒子,最后才往最里层填字。对着 index.html 数一下层次:
ini
<main class="container"> ← 外层大盒子
<div class="row col-md-6"> ← 中间一行
<table class="table"> ← 表格盒子
<thead> / <tbody> ← 表头/表体小盒子
container / row / col-md-6 这三个 class 来自 Bootstrap:
container:中间内容宽度固定,左右留白,解决不同屏幕尺寸下布局不散的问题row:一行,块级元素,默认占满整行宽度col-md-6:一列,占父容器宽度的 6/12(一半)。Bootstrap 把一行切成 12 等份
HTML 标签本身分两大类:
| 类别 | 行为 | 例子 |
|---|---|---|
| 块级元素 | 默认占据整行,自己就是一个盒子 | div header main table h1 |
| 行内元素 | 多个元素可以挤在同一行 | span a strong img |
判断方法:把两个同类元素挨着放,看它们是上下排列还是左右排列。做布局用块级,做行内高亮或链接用行内。
语义化、DOM 与挂载点
我注意到 index.html 里除了 <div>,还有 <header> <main> <aside> <footer> <thead> <tbody>。我以为这只是"写法好看",后来发现根本不是。
这叫语义化标签 ------给盒子标注身份。<main> 和 <div class="main"> 视觉上一样,但浏览器能识别 <main> 的含义,搜索引擎抓取时知道这是正文,读屏软件能直接跳到主体内容。大厂项目对语义化标签的重视程度很高------可维护性、SEO、无障碍,每一样都靠它兜底。
但让我真正重视语义化的,是它和 JS 操作的关系。
浏览器解析完 index.html 之后,会把所有标签建成一棵树状结构 ,叫 DOM 树------每一个标签变成树上的一个节点,嵌套关系变成父子关系:
css
document
└── html
├── head
└── body
├── header
├── main.container
│ └── div.row
│ └── table.table
│ ├── thead → tr → th*3
│ └── tbody → tr → td*3
└── script
document 是这棵树的入口。document.querySelector('.table tbody') 就是"从入口出发,顺着树找第一个匹配的节点"。
common.js 里把找到的节点存进变量:
js
const oBody = document.querySelector('.table tbody');
注意命名:o 开头表示"这个变量存的是一个 DOM 节点对象",这是一种团队命名惯例,帮你一眼看出变量类型。oBody 就是整个项目的挂载点------后面所有的动态内容都往这里"挂"。
往挂载点写内容,用的是 innerHTML +=,这一行做了三件事:读取 <tbody> 现有的 HTML 字符串 → 拼接新的 <tr> → 写回去触发浏览器重新渲染。
拼接新行用的是反引号模板字符串:
js
oBody.innerHTML += `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.hometown}</td>
</tr>
`;
反引号(`````)里可以直接用 ${} 嵌入变量,比拼接字符串用 + 拼起来可读性高很多。这是 ES6 之后 JS 写字符串的推荐方式。
AI 在这节课做了什么
这节课叫"第一个 AI 全栈项目",AI 在哪里?
在设计决策上。课堂上讨论怎么搭这个页面的时候,我们用 AI 来辅助思考了两件事:
- 页面美观 ------ AI 聊到了 Bootstrap 这个 CSS 框架:想要一个好看的响应式表格,直接引入 Bootstrap,不用自己从零写样式规则
- 结构良好 ------ AI 聊到了语义化标签:想让搜索引擎正确理解页面结构,
<thead><tbody><main>这些标签要写对
AI 在这里扮演的角色是技术选型顾问------它不是替你写代码,而是帮你在"有哪些方案"这一步做了快速筛选。从 Prompt Engineering 到现在,这个模式没有变过:AI 负责提供选项和背景知识,你负责决定用哪个、为什么用。
json-server:30 秒拉起一个假后端
backend/ 里没有任何逻辑代码,只有 db.json 和 package.json。
package.json 是项目的"身份证",记录了依赖和可执行命令:
json
{
"scripts": { "dev": "json-server --watch db.json" },
"dependencies": { "json-server": "^1.0.0-beta.15" }
}
拿到别人的项目,先跑 npm install ------ npm 读 package.json 里的 dependencies,把所有依赖下载好。node_modules/ 不需要一起发,因为这份"购物清单"在,随时能还原。
跑 npm run dev,等价于直接跑 json-server --watch db.json。json-server 读取 db.json,按里面的键名自动生成 REST API:
bash
GET /users → 返回所有用户
GET /users/1 → 返回 id=1 的用户
POST /users → 新增
PUT /users/1 → 修改
DELETE /users/1 → 删除
--watch 监听 db.json 文件变化,改了自动重载,不需要手动重启。
在浏览器里输入 http://localhost:3000/users,看到 JSON 数据,后端就通了。
我在这里停了一下。以前我以为"搭后端"需要学很多:Node.js、路由、数据库......但眼前一个 JSON 文件加一行命令就跑通了一个能被前端真实调用的 API。跑通一个完整链路,不需要每个环节都精通------先跑通,再补课,不要被"还没学完"挡住。
fetch + .then + 异步:为什么数据要"等一会"
前端拿数据的代码:
js
fetch('http://localhost:3000/users')
.then(data => data.json())
.then(data => {
users = data;
const oBody = document.querySelector('.table tbody');
for (let user of users) {
oBody.innerHTML += `<tr>...</tr>`;
}
});
我第一次看到这段代码很困惑:为什么不直接 const data = fetch(...)?
后来搞清楚了:fetch 是异步的 。从发出请求到后端返回数据,网络需要时间,如果 fetch 在等待时把浏览器冻住,页面不能滚动、按钮不能点。所以浏览器把 fetch 设计成"发请求,继续往下执行,等数据回来了再通知你"------叫号、坐下、等叫到名字再取餐。
fetch 立刻返回的是一张承诺票(Promise) ,不是数据。直接赋值拿到的是 Promise,不是数组。两层 .then 各有分工:
| 层 | 等的是什么 | 拿到的是什么 |
|---|---|---|
第一层 .then(data => data.json()) |
等 HTTP 响应到达 | Response 对象(快递盒) |
第二层 .then(data => {...}) |
等 JSON 解析完成 | 真正的 JS 数组(快递内容) |
data => data.json() 是 ES6 的箭头函数 写法,等价于 function(data) { return data.json(); }------只有一行时可以省略 {} 和 return。
拿到数组之后,用 for...of 循环处理每一条数据:
js
for (let user of users) { ... }
for...of 是 ES6 的语法糖,跳过了计数器------直接说"对 users 里的每一个 user,做这件事",不需要维护 i、写 users[i]、判断 i < users.length。计数循环 for(let i=0; i<n; i++) 符合 CPU 计算逻辑,性能略好,但可读性差。日常业务代码里数据量不大,可读性优先,用 for...of。
这节课我学到了什么
- 前端三件套分离是为了"改一处,全局生效"。 HTML 管结构、CSS 管样式、JS 管行为,拆开之后每次改动的爆炸半径被锁在一个文件里,不会牵一发动全身。
- 语义化标签不只是好看,它是 JS 精准操作 DOM 的前提。
<tbody>是挂载点,querySelector('.table tbody')能找到它全靠你在 HTML 里写了这个标签。<div>写一时爽,JS 和搜索引擎都不知道那块"div"到底是什么。 - json-server 让你在不写后端的情况下跑通前后端联调。 一个
db.json+npm run dev,增删改查的 HTTP 接口全齐。先跑通整条链路,再补每个环节的原理------这是 Vibe Coding 时代学东西的节奏。 fetch是异步的,数据要"等一会"才能用。 直接赋值拿到的是 Promise,不是数据。两层.then:第一层等 HTTP 响应,第二层等 JSON 解析,只有在第二层里,数据才真正可用。- AI 在全栈项目里扮演的是技术选型顾问的角色。 它帮你从"Bootstrap 还是自己写样式"、"用语义化标签还是全用 div"这些问题里快速找到方向,你负责理解背后的原因并做最终决定。
- 模块化 + DOM + 异步,串成了一条完整的数据流。
db.json→ json-server →fetch→.then→innerHTML +=→ DOM 树更新 → 浏览器渲染。这条线上任何一个环节出问题,你立刻知道去哪里找。
从第一课的 Prompt Engineering,到这节课第一次跑通一个真实的前后端分离项目,我越来越清楚一件事:学编程最难的不是记住语法,而是建立"数据在哪、往哪流、怎么触发"的整体感。语义化保证了 JS 能找到正确的节点,异步保证了页面在等数据时不卡死,模块化保证了改动不会失控------每一个"规范"背后都有一个具体的原因,搞懂原因,规范就不再是约束,而是武器。
下一课,继续往下挖。