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

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

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

这节课的材料分成两组。

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

第二组在 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,不能是 012

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

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

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,可能只拿到第一个数字片段。有了 gmatch 会继续往后找,所以 价格是100元,数量是80 里能提取到 10080

这里我开始意识到,正则的"难"不只是符号多,而是符号之间会组合。一个 \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 方法",而是它本身在执行替换时就会根据正则匹配结果,把完整匹配、捕获组、匹配位置、原字符串等信息按顺序传给回调函数。matchreplace 都会用到正则匹配机制,但返回形式和用途不一样。

第四个代码文件 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 里是一个表格
  • 表头有 idnameage
  • 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 比谁写得快,而是知道一段代码到底在匹配什么、请求什么、等待什么、替换什么、渲染什么。

相关推荐
Mintopia1 小时前
从意图到评估:理解用户操作产品的完整行动链路
前端
惜缘破军1 小时前
基于 Spring Boot 3 和 Spring Cloud 2023 的微服务基础框架 hdfk7-boot
spring boot·后端·微服务
竹林8181 小时前
从报错到跑通:我用 @solana/web3.js 在 React 中实现 Solana 钱包连接的全过程
前端
Asize1 小时前
重生之我在 Vibe Coding 时代当程序员:第十六课,从模拟队列到原型链
前端·javascript·后端
vim怎么退出1 小时前
Dive into React——高级特性
前端·react.js·源码阅读
冰暮流星1 小时前
javascript之this关键字
开发语言·前端·javascript
百度Geek说1 小时前
CodingAgent 的原始森林困境:一张地图能解决什么?
开发语言·javascript·ecmascript·coding agent
余大大.1 小时前
SystemVerilog-参数宏与拼接符的使用
前端
羸弱的穷酸书生1 小时前
跟AI学一手之前端导出
前端·文件导出