重生之我在 Vibe Coding 时代当程序员:第十五课,正则表达式和 HTTP 请求:规则不是背出来的,是拆出来的

上一节课,我把 Agent 和知识库的关系重新捋了一遍:知识库不是外挂,而是 Agent 的业务记忆。

这一节课,我又回到更底层的编程基本功:一边练 JavaScript 正则表达式,一边练前端怎么通过 HTTP 请求拿到后端数据,最后再看 LLM 接口调用里 fetch 请求到底由哪些部分组成。

这节课的材料分成两组。

第一组在 interview/js/reg,主题是正则表达式。它从拼多多笔试题里的手机号校验开始,接着讲 RegExp、match、replace、捕获组、模板字符串渲染。

第二组在 ai/aigc/http-demo,主题是 HTTP 接口。它从前端请求方式讲到 Browser/Server、Client/Server,再用 json-server 做了一个 /friends 接口,最后又保留了一个直接调用 LLM HTTP 接口的原生页面。

乍一看,一个是字符串规则,一个是网络请求规则。但我现在更愿意把它们放在同一节课里理解:程序不是靠感觉运行的,它总要有一种规则来判断输入、组织数据、发送请求、等待结果。

从拼多多笔试题开始:手机号不是"差不多就行"

正则这组笔记的标题是:

markdown 复制代码
# 拼多多笔试题

课堂先讲的是正则表达式:

markdown 复制代码
## 正则表达式 (Regular Expression)

笔记里把格式写得很直接:

text 复制代码
格式  / /  (中间不能为空)

然后列了几个基础规则:

  • 每次匹配一个字符
  • { } 表示匹配的字符长度,也就是次数
  • [ ] 表示匹配的字符范围
  • \d 表示匹配数字
  • ^ 表示匹配的字符串必须以正则表达式开头
  • $ 表示匹配的字符串必须以正则表达式结尾

这节课的第一个具体问题是:

text 复制代码
如何验证用户输入的手机号是正确的?

课堂把手机号规则拆成三条:

  • 11 位数字
  • 第一位以 1 开头
  • 第二位只能是 3-9,不能是 0、1、2

这里我觉得最重要的不是背一个手机号正则,而是先学会拆规则。

如果我只说"验证手机号",这个需求是模糊的。但拆开之后,它就变成了可以被代码表达的条件:

text 复制代码
第 1 位:1
第 2 位:3-9
后 9 位:数字
总长度:11 位
从开头匹配到结尾,不能多也不能少

所以最后就能写成:

js 复制代码
/^1[3-9]\d{9}$/

这就是正则像数学公式的地方。它不是一段普通字符串,而是一个"范式识别器"。

笔记里还有一句业务场景提醒:

text 复制代码
实际业务场景:永远不要相信用户的输入(把用户当小白)

这句话听起来有点狠,但在工程里很真实。用户输入的数据一定要检查,因为用户可能输错,也可能乱输,甚至可能故意构造异常输入。程序不能靠"用户应该会正确填写"来维持稳定。

RegExp 也是对象

第一个代码文件是 1.js:

js 复制代码
let str = '13888888888'
// 正则表达式中,是一个字符一个字符的匹配
// [] 表示匹配的字符范围 不匹配中括号本身
// {} 表示前一个字符匹配的次数
// ^ 表示匹配的字符串必须以正则表达式开头
// $ 表示匹配的字符串必须以正则表达式结尾
// \d 表示匹配数字
// reg 是一个正则表达式对象,用于匹配字符串中的模式 描述匹配的规则
let reg = /^1[3-9]\d{9}$/
// text 方法测试字符串是否符合正则表达式的规则
console.log(reg.test(str));
console.log(typeof reg);
console.log(
    Object.prototype.toString.call(reg)
);
console.log(
    Object.prototype.toString.call([1,2,3])
);

这里有一个小笔误:注释里写的是 text 方法,实际代码调用的是 test:

js 复制代码
reg.test(str)

test 的作用就是测试字符串是否符合正则规则,返回布尔值。

这段代码除了手机号校验,还顺手复习了 JS 数据类型。

笔记里把 JS 数据类型分成两派:

  • 简单数据类型
    • 数值(Number)
    • 字符串(String)
    • 布尔值(Boolean)
    • Null
    • Undefined
  • 引用类型,也就是对象 Object 中包含的类型
    • Array(数组)
    • Object(对象)
    • Function(函数)
    • RegExp(正则表达式)
    • Date(日期)
    • 等等

所以代码里才会打印:

js 复制代码
console.log(typeof reg);
console.log(
    Object.prototype.toString.call(reg)
);
console.log(
    Object.prototype.toString.call([1,2,3])
);

typeof reg 会告诉我正则是对象类型,而 Object.prototype.toString.call(...) 可以更细地看出它到底是 [object RegExp] 还是 [object Array]。

这对我理解正则很有帮助:正则不是"写在斜杠里的神秘语法",它在 JS 里也是一个对象,一个用来描述匹配规则的对象。

用 match 提取字符串中的数字

第二个代码文件是 2.js:

js 复制代码
const str = '价格是100元,数量是80'
const reg = /\d+/g;
console.log(reg.test(str));
const res = str.match(reg);
console.log(res);

这里的需求是从字符串里提取数字。

笔记里对应的知识点是:

  • str 字符串对象中的 match 方法
  • 正则的修饰符 g 表示全局匹配,不停下
  • \d+ 里的 + 表示匹配一次或多次

所以:

js 复制代码
/\d+/g

可以拆成:

text 复制代码
\d:匹配数字
+:连续匹配一次或多次
g:全局匹配,找到一个以后不要停

如果没有 g,可能只拿到第一个数字片段。有了 g,match 会继续往后找,所以 价格是100元,数量是80 里能提取到 100 和 80。

这里我开始意识到,正则的"难"不只是符号多,而是符号之间会组合。一个 \d 很简单,一个 + 也很简单,一个 g 也很简单,但组合起来就变成了一个可执行的提取规则。

replace 和捕获组:为什么回调里有 _ 和 c

第三个代码文件是 3.js,这段比前面更值得复盘:

js 复制代码
// 将-去掉 并且将w替换成W(-后面的第一个字母进行替换)
const str = 'hello-world';
// () 分组 不匹配(),是提取()中的内容
const ref = /-(\w)/;
// match 返回的数组中 第一个元素是匹配的字符串,后续元素是分组(()中包含了的内容)匹配的内容
console.log(str.match(ref));
// 2. 回调函数参数 (_, c)
// 这里的参数含义一般是:

// _:表示当前匹配到的完整字符串
// c:表示正则中第一个捕获组匹配到的内容
// 为什么 replace中的回调函数参数中,_ 表示当前匹配到的完整字符串,
// 而 c 表示正则中第一个捕获组匹配到的内容?
// 他内置了match方法吗?
const res = str.replace(ref, (_, c) => {
    console.log(_, c);
    return c.toUpperCase();
});
console.log(res);

这个例子要做的事是把:

text 复制代码
hello-world

变成:

text 复制代码
helloWorld

也就是:

  • 去掉 -
  • 把 - 后面的 w 变成大写 W

正则是:

js 复制代码
const ref = /-(\w)/;

这里的 () 是分组,也叫捕获组。笔记里写得很清楚:

text 复制代码
() 分组 不匹配(),是提取()中的内容

更准确地说,括号本身不是要匹配的字符,它的作用是把匹配结果中的一部分单独捕获出来。

/-(\w)/ 里:

  • - 匹配短横线
  • \w 匹配一个字母、数字或下划线这样的单词字符
  • (\w) 把这个字符捕获出来

所以 str.match(ref) 的结果里:

  • 第一个元素是完整匹配到的字符串,比如 -w
  • 后续元素是捕获组匹配到的内容,比如 w

这也解释了 replace 回调函数里的参数:

js 复制代码
(_, c) => {
    console.log(_, c);
    return c.toUpperCase();
}

这里的 _ 不是特殊语法,只是一个参数名。课堂里用它表示"当前匹配到的完整字符串"。在这个例子里,它是 -w。

c 表示第一个捕获组匹配到的内容。在这个例子里,它是 w。

所以回调返回:

js 复制代码
c.toUpperCase()

也就是把 w 变成 W,并用这个返回值替换完整匹配到的 -w。最后得到 helloWorld。

笔记里问了一句:

text 复制代码
他内置了match方法吗?

我的理解是:replace 的回调不是简单"内置调用了 match 方法",而是它本身在执行替换时就会根据正则匹配结果,把完整匹配、捕获组、匹配位置、原字符串等信息按顺序传给回调函数。match 和 replace 都会用到正则匹配机制,但返回形式和用途不一样。

第四个代码文件 4.js 是这个练习的精简版:

js 复制代码
const res = "hello-world".replace(
  /-(\w)/, 
  (_, c)=>{  
    return c.toUpperCase();
}
)
console.log(res);

这段保留了最核心的动作:匹配 -(\w),拿到捕获组 c,转成大写后替换。

模板字符串渲染:把 {{name}} 替换成真实数据

第五个代码文件是 5.js:

js 复制代码
let template = `我是{{name}},我今年{{age}}岁,性别是{{gender}}`;
let person = {
    name: '张三',
    age: 18,
    gender: '男'
}

function render(template, person) {
    const reg = /\{\{(\w+)\}\}/;
    if (!reg.test(template)) {
        return template;
    }
    const name = reg.exec(template)[1];
    const res = template.replace(reg, person[name]);
    return render(res, person);
}

console.log(render(template, person));

// let template = `我是{{name}}, 年龄{{age}}, 性别{{sex}}`;
// let person = {
//   name: '赖庆庆',
//   age: 17,
//   sex: '男'
// }
// function render(template, data) {
//   const reg = /\{\{(\w+)\}\}/ // {} 长度
//   if (reg.test(template)) {
//     const name = reg.exec(template)[1];
//     template = template.replace(reg, data[name])
//     return render(template, data);
//   }
//   return template;
// }
// console.log(render(template, person));

这段代码比 hello-world 更接近真实业务,因为它已经有一点模板引擎的味道了。

模板是:

js 复制代码
let template = `我是{{name}},我今年{{age}}岁,性别是{{gender}}`;

数据是:

js 复制代码
let person = {
    name: '张三',
    age: 18,
    gender: '男'
}

核心正则是:

js 复制代码
const reg = /\{\{(\w+)\}\}/;

这里为什么要写成 \{\{ 和 \}\}?

因为 {} 在正则里本身有特殊含义,比如表示次数。所以如果我要匹配字面量的 { 和 },就要转义。

这段正则可以拆成:

text 复制代码
\{\{:匹配 {{
(\w+):捕获一个或多个单词字符,比如 name、age、gender
\}\}:匹配 }}

所以它可以从 {{name}} 里捕获出 name。

代码里先判断模板中是否还有占位符:

js 复制代码
if (!reg.test(template)) {
    return template;
}

如果没有,就返回最终字符串。

如果有,就拿到捕获组:

js 复制代码
const name = reg.exec(template)[1];

比如第一次拿到的是 name,于是:

js 复制代码
person[name]

就等于:

js 复制代码
person['name']

也就是 张三。

然后替换:

js 复制代码
const res = template.replace(reg, person[name]);

最后递归:

js 复制代码
return render(res, person);

递归的意义是:这个正则没有加 g,一次只替换一个占位符,所以替换完 {{name}} 后,还要继续处理 {{age}} 和 {{gender}}。

被注释掉的版本也保留了同样的思路,只是数据换成了:

js 复制代码
// let template = `我是{{name}}, 年龄{{age}}, 性别{{sex}}`;
// let person = {
//   name: '赖庆庆',
//   age: 17,
//   sex: '男'
// }

这里我看到的是一个很小但完整的规则系统:

  • 用正则识别占位符
  • 用捕获组拿到字段名
  • 用对象属性读取数据
  • 用 replace 替换当前占位符
  • 用递归继续处理剩余占位符

这比单纯背 \w+ 有用。因为它让我看到正则怎么和对象、函数、递归组合起来解决真实问题。

第二条线:HTTP 接口不是一句 fetch 就完了

另一组笔记是 http-demo/readme.md,标题是:

markdown 复制代码
# LLM HTTP 接口

它开头列了两个关键词:

  • openai SDK
  • fetch 请求

然后问了一个很基础但很重要的问题:

markdown 复制代码
## 前端发送 Http 请求,有哪些方式?

答案是:

  • fetch
  • xmlhttprequest

现在我写前端请求经常直接用 fetch,但课堂这里提醒我:fetch 只是方式之一。更旧的方式是 XMLHttpRequest,很多库和历史代码里都还能看到它的影子。

接着笔记问:

markdown 复制代码
### 这是一个什么编程模式?

下面列了几组关键词:

  • 前后端分离(模块化)
  • 异步编程 async/await
    • fetch
  • Browser(浏览器)/ Server 架构
  • Client(客户端)/ Server 架构
    • app android/ios

这里我把它理解成:HTTP 请求不是孤立 API,而是前后端分离之后,浏览器和服务器之间的通信方式。

浏览器负责页面和交互,服务器负责提供数据或服务。它们不在同一个运行环境里,所以要通过 HTTP 协议通信。

Server 架构:IP、端口和 endpoint

笔记里写了:

markdown 复制代码
## Server(服务器) 架构

- 硬件
- 软件 java/nodejs 开发的服务器端
- IP:端口号 http://127.0.0.1:3000

这段把服务器拆成两层:

  • 硬件:真正运行服务的机器
  • 软件:用 Java、Node.js 等开发的服务器端程序

本地练习里,服务器地址是:

text 复制代码
http://127.0.0.1:3000

127.0.0.1 是本机地址,3000 是端口号。

笔记里还有一段解释:

text 复制代码
浏览器是在本地打开,而服务器是在远端运行,所以二者需要通过 http 协议来进行通信

即使这次练习的服务器也跑在本机,逻辑上它仍然是一个独立的 Server。浏览器不能直接读取后端内存里的数据,而是要向某个 URL 发请求。

笔记还问了:

text 复制代码
什么是 IP 地址?

答案是:

  • 唯一的标识符
  • 用于在互联网网络层定位并唯一标识一台计算机,也就是服务器
  • 域名 www.baidu.com 好记
  • DNS 解析就是将域名转换为 IP 地址的过程

这里我第一次把域名、IP、端口、接口地址串起来:

text 复制代码
域名方便人记忆
DNS 把域名解析成 IP
IP 定位服务器
端口定位服务器上的具体服务
endpoint 定位某个 API 请求的终点

笔记里写了:

text 复制代码
api url endpoint , 配置管理
endpoint :api 请求的终点

也就是说,请求不是"随便发到服务器",而是发到某个具体 endpoint。

async/await 控制执行流程

HTTP 笔记里有一句:

text 复制代码
async/await 控制了执行流程

下面解释:

text 复制代码
应该先等到请求完数据接口才执行下面的部分,所以需要通过异步来实现同步

这句话很适合放进这节课。

网络请求是异步的,因为浏览器发出请求以后,服务器需要时间响应。如果我不等数据回来,就直接渲染页面,页面拿到的可能还是空数组。

所以 async/await 的价值不是让网络变快,而是让我用更容易读的顺序表达异步流程:

text 复制代码
先请求
再解析 JSON
再保存数据
再渲染页面

笔记里还写了一个从数据到页面的转换:

text 复制代码
从获取到的 json 数组 -> 在 html 页面显示的tr 字符串数组 使用字符串自带的 map() 方法

这就是后面 main.js 里的核心逻辑。

用 json-server 做一个 /friends 接口

http-demo/demo1/backend/package.json 是:

json 复制代码
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "json-server --watch data.json --port 3000"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "json-server": "1.0.0-beta.15"
  }
}

这里后端没有手写一个 Node 服务,而是用 json-server 直接把 data.json 暴露成接口。

脚本是:

json 复制代码
"dev": "json-server --watch data.json --port 3000"

它表示:

  • 监听 data.json
  • 在 3000 端口启动服务
  • 根据 JSON 数据生成接口

data.json 是:

json 复制代码
{
    "friends": [
       {
        "id": 1,
        "name": "张三",
        "age": 18
        },
       {
        "id": 2,
        "name": "李四",
        "age": 19
        },
       {
        "id": 3,
        "name": "王五",
        "age": 20
        }
    ]
}

所以前端请求的 endpoint 是:

text 复制代码
http://localhost:3000/friends

这也对应了前面讲的 endpoint:它是 API 请求的终点。

目录里还有 pnpm-lock.yaml,说明这个后端依赖已经通过 pnpm 生成过锁文件。

前端页面:表格先等数据回来

前端页面 demo1/frontend/index.html 是:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <header>
        <h1>HTTP 接口</h1>
    </header>
    <main>
        <table>
            <thead>
                <tr>
                    <th>id</th>
                    <th>name</th>
                    <th>age</th>
                </tr>
            </thead>
            <tbody>
                
            </tbody>
        </table>
    </main>
    <footer>
        <p>Copyright &copy; 2023</p>
    </footer>
    <script src="./main.js"></script>
</body>
</html>

页面结构很简单:

  • header 里是标题 HTTP 接口
  • main 里是一个表格
  • 表头有 id、name、age
  • tbody 一开始是空的
  • 底部是 Copyright © 2023
  • 最后引入 main.js

这里的关键是:tbody 初始为空,因为数据要从接口拿回来后再渲染。

main.js 是:

js 复制代码
let friends = []

async function lodaDate() {
    // console.log('加载数据');
    // endpoint 终点
    const endpoint = 'http://localhost:3000/friends';
    // await fetch(endpoint) // 发送请求 异步
    // // 等待响应返回数据
    // // fetch响应体是 json 二进制字符串 要解析为 json 对象
    //     .then(res => res.json())
    //     .then(data => {
    //         friends = data;
    //         console.log(friends);
    //     })
    // res 接受的对象此时是 json 字符串(二进制字符串)也就是文本
    // data 接受的就是 json 数据

    // 异步变同步 每一步都加上 await,等待上一步完成
    const res = await fetch(endpoint);
    const data = await res.json();
    friends = data;
    // console.log(friends);
    return friends;
}

function renderData(friends) {
    console.log('渲染数据');
    const oBody = document.querySelector('table tbody');
    if(friends.length > 0) {
        // .map() 方法用于创建一个新数组,其中的元素是该数组中的每个元素调用一个函数后的返回值。
        // .join() 方法用于将数组中的所有元素连接成一个字符串。
        oBody.innerHTML = friends.map(item => `
        <tr>
            <td>${item.id}</td>
            <td>${item.name}</td>
            <td>${item.age}</td>
        </tr>
    `).join('');
    }

}
// 用异步来控制程序执行流程  异步请求
async function init() {
    console.log('初始化');
    const friends = await lodaDate();
    console.log(friends);
    renderData(friends);
}
init();
console.log('初始化完成');

这段代码有两种异步写法的痕迹。

注释里保留了 then 写法:

js 复制代码
// await fetch(endpoint) // 发送请求 异步
// // 等待响应返回数据
// // fetch响应体是 json 二进制字符串 要解析为 json 对象
//     .then(res => res.json())
//     .then(data => {
//         friends = data;
//         console.log(friends);
//     })

实际使用的是 async/await:

js 复制代码
const res = await fetch(endpoint);
const data = await res.json();
friends = data;
return friends;

注释里说:

text 复制代码
res 接受的对象此时是 json 字符串(二进制字符串)也就是文本
data 接受的就是 json 数据

我的理解是:fetch 返回的是 Response 对象,它的响应体还没有直接变成 JS 对象,需要通过:

js 复制代码
await res.json()

把响应体解析成 JS 数据。

渲染部分用的是:

js 复制代码
oBody.innerHTML = friends.map(item => `
        <tr>
            <td>${item.id}</td>
            <td>${item.name}</td>
            <td>${item.age}</td>
        </tr>
    `).join('');

这正好对应笔记里的:

text 复制代码
从获取到的 json 数组 -> 在 html 页面显示的tr 字符串数组 使用字符串自带的 map() 方法

更准确地说,map() 是数组方法,不是字符串方法。它把每一个 friend 对象变成一个 <tr> 字符串。然后 join('') 把字符串数组拼成一个完整 HTML 字符串,最后塞进 tbody.innerHTML。

这里还有一个执行顺序细节:

js 复制代码
init();
console.log('初始化完成');

init() 是异步函数。它内部会打印 初始化,然后在 await lodaDate() 这里等待请求结果。但外面的 console.log('初始化完成') 不会等整个 init 结束才执行。

这就是异步代码容易让人误会的地方。await 等待的是当前 async 函数内部后续流程,外层如果没有 await init(),外层代码仍然会继续往下跑。

另一个原生模板:没有构建工具的小页面

http-demo/demo1/demo/README.md 里写的是:

markdown 复制代码
# Native Web Starter

一个通用的原生 HTML/CSS/JS 项目模板,适合课程演示、前端基础练习、小型原型和接口调试页面。

文件结构是:

text 复制代码
demo/
├── index.html
├── styles.css
├── script.js
└── README.md

运行方式是直接打开 index.html,也可以用任意静态服务器:

bash 复制代码
npx serve .

说明里写:

  • index.html:页面结构和内容入口
  • styles.css:全局样式、响应式布局和组件样式
  • script.js:原生 JavaScript 交互逻辑
  • README.md:项目说明

这个模板没有引入构建工具和第三方依赖,后续可以按需要继续扩展。

这部分让我看到课程里还有一个思路:不是所有前端练习都要上框架。接口调试、小型原型、课堂演示,用原生 HTML/CSS/JS 就够了。

目录里还有一个 bootstrap.min.css,但这次核心代码没有展开使用它,我只把它记为前端目录里的样式资源。

直接调用 LLM HTTP 接口

demo/index.html 是:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DEEPSEEK接口调用</title>
  <link rel="stylesheet" href="./styles.css">
</head>
<body>
  <div id="replay"></div>
  <script type="module" src="./script.js"></script>
</body>
</html>

页面标题是:

text 复制代码
DEEPSEEK接口调用

页面里有一个:

html 复制代码
<div id="replay"></div>

这里的 id 是 replay,不是常见的 reply。我不改它,因为代码里也是按 replay 获取元素。

脚本使用了:

html 复制代码
<script type="module" src="./script.js"></script>

也就是浏览器端 ES Module。

script.js 是:

js 复制代码
// url + method + http 版本号 请求行
const endpoint = '';
// headers 请求头
const headers = {
  // 授权头
  // Bearer + API_KEY
  // api_key 通过 Authorization 头传递
  'Authorization': `Bearer `,
  // 内容类型头
  // application/json
  'Content-Type': 'application/json',
};

// body体 请求体
const payload = {
  model: 'gpt-5.5',
  messages: [
    {
      role: 'system',
      content: 'You are a helpful assistant.',
    },
    {
      role: 'user',
      content: '你好,GPT',
    }
  ]
};

try {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers,
    // HTTP 请求里面,传输的不能是对象,只能传输字符串甚至到最后是二进制字符串
    // 请求时候的数据,不能直接传递对象,要转换为字符串
    body: JSON.stringify(payload)
  });
  const data = await response.json();
  console.log(data);
  document.getElementById('replay').innerHTML = data.choices[0].message.content;
} catch (error) {
  console.log(error);
}

这段代码把 HTTP 请求结构拆得很清楚。

第一部分是请求行:

js 复制代码
// url + method + http 版本号 请求行
const endpoint = '';

这里 endpoint 还是空字符串,说明接口地址还没有填。

第二部分是请求头:

js 复制代码
const headers = {
  // 授权头
  // Bearer + API_KEY
  // api_key 通过 Authorization 头传递
  'Authorization': `Bearer `,
  // 内容类型头
  // application/json
  'Content-Type': 'application/json',
};

这对应前面笔记里的:

  • 请求头
  • Authorization 权限
  • Bearer + API_KEY
  • Content-Type
  • application/json

第三部分是请求体:

js 复制代码
const payload = {
  model: 'gpt-5.5',
  messages: [
    {
      role: 'system',
      content: 'You are a helpful assistant.',
    },
    {
      role: 'user',
      content: '你好,GPT',
    }
  ]
};

这里保留了一个 system message:

text 复制代码
You are a helpful assistant.

以及一个 user message:

text 复制代码
你好,GPT

最后发送请求:

js 复制代码
const response = await fetch(endpoint, {
  method: 'POST',
  headers,
  body: JSON.stringify(payload)
});

最关键的注释是:

text 复制代码
HTTP 请求里面,传输的不能是对象,只能传输字符串甚至到最后是二进制字符串
请求时候的数据,不能直接传递对象,要转换为字符串

所以要写:

js 复制代码
body: JSON.stringify(payload)

这和前面的 res.json() 正好是一来一回:

  • 发请求时,把 JS 对象 payload 通过 JSON.stringify() 转成 JSON 字符串
  • 收响应时,把响应体通过 response.json() 解析成 JS 对象

最后把模型返回内容放到页面:

js 复制代码
document.getElementById('replay').innerHTML = data.choices[0].message.content;

这段代码当前也有几个真实状态:

  • endpoint 为空,还不能直接请求。
  • Authorization 里只有 Bearer ,还没有填 API Key。
  • model 写的是 gpt-5.5,我这里只按课堂材料保留,不判断它是不是实际可用模型。
  • 代码用顶层 await,所以 HTML 里使用了 type="module"。

这不是一段完整可运行的生产代码,更像是把 LLM HTTP 请求的骨架拆给我看。

API 不需要背,API 需要查询

HTTP 笔记最后有一个小节:

markdown 复制代码
## API 不需要学习,API 需要查询

下面写:

  • 查看 MDN 官方文档,详细了解每个 API 的用法
  • JS、CSS、HTML 等语法查询官方文档 MDN
  • 将 MDN 喂给 AI

这句话对我挺重要。

以前我会下意识觉得"学 API"就是把一堆方法背下来。但真实开发不是这样。API 太多了,而且细节会更新。更重要的是:

  • 知道有什么 API
  • 知道去哪里查
  • 知道文档里哪些字段重要
  • 知道怎么把文档交给 AI 帮我解释
  • 知道最终代码里每个参数为什么这样写

在 Vibe Coding 时代,MDN 和 AI 不是互相替代的关系。MDN 给标准答案,AI 帮我读、帮我对照、帮我生成例子。但如果我完全不知道自己要查什么,AI 也只能带着我在雾里绕。

我现在怎么把这节课串起来

这节课从正则开始,到 HTTP 请求结束,中间看起来跳了很多知识点:

  • / /
  • { }
  • [ ]
  • \d
  • ^
  • $
  • test
  • match
  • g
  • \d+
  • replace
  • ()
  • 捕获组
  • 模板渲染
  • 递归
  • fetch
  • XMLHttpRequest
  • 前后端分离
  • Browser/Server
  • Client/Server
  • IP
  • 端口
  • DNS
  • endpoint
  • async/await
  • json-server
  • Response.json()
  • map()
  • join()
  • JSON.stringify()
  • Authorization
  • Content-Type

但它们其实都在训练同一种能力:把模糊任务拆成明确规则。

手机号校验不是"判断一下手机号对不对",而是拆成:

text 复制代码
1 开头
第二位 3-9
后面 9 位数字
总共 11 位
从开头到结尾完整匹配

模板渲染不是"把变量替换一下",而是拆成:

text 复制代码
识别 {{xxx}}
捕获 xxx
从对象里读取 data[xxx]
替换一次
递归处理下一个

HTTP 请求也不是"调一下接口",而是拆成:

text 复制代码
endpoint 在哪里
method 是什么
headers 里带什么
body 怎么序列化
response 怎么解析
拿到数据后怎么渲染
异步流程怎么等待

LLM 接口也不是"问 AI 一句话",而是拆成:

text 复制代码
请求地址
授权方式
内容类型
模型名
messages
JSON.stringify
response.json
choices[0].message.content

这就是我对这节课的最大收获。

规则不是背出来的,是拆出来的。

正则表达式让我拆字符串规则,HTTP 请求让我拆通信规则,LLM 接口让我拆 AI 服务调用规则。

AI 可以帮我生成一段正则,也可以帮我写一段 fetch。但如果我不知道 ^ 和 $ 为什么要加,不知道 body 为什么要 JSON.stringify(),不知道 await response.json() 等的是什么,那我就只是把 AI 生成的代码复制到项目里。

而这节课让我往前走了一步:我开始能看懂这些规则背后的形状。

这也是 Vibe Coding 时代程序员不能丢的能力。不是和 AI 比谁写得快,而是知道一段代码到底在匹配什么、请求什么、等待什么、替换什么、渲染什么。

相关推荐
lovingsoft1 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly11 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝15 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦16 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
花涧鹤28 分钟前
学完循环我才发现,我写的不是死循环,是死机
后端
shmily麻瓜小菜鸡37 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI43 分钟前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring