上一节课,我把 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 SDKfetch 请求
然后问了一个很基础但很重要的问题:
markdown
## 前端发送 Http 请求,有哪些方式?
答案是:
fetchxmlhttprequest
现在我写前端请求经常直接用 fetch,但课堂这里提醒我:fetch 只是方式之一。更旧的方式是 XMLHttpRequest,很多库和历史代码里都还能看到它的影子。
接着笔记问:
markdown
### 这是一个什么编程模式?
下面列了几组关键词:
- 前后端分离(模块化)
- 异步编程
async/awaitfetch
- 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 © 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^$testmatchg\d+replace()- 捕获组
- 模板渲染
- 递归
fetchXMLHttpRequest- 前后端分离
- Browser/Server
- Client/Server
- IP
- 端口
- DNS
- endpoint
async/awaitjson-serverResponse.json()map()join()JSON.stringify()AuthorizationContent-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 比谁写得快,而是知道一段代码到底在匹配什么、请求什么、等待什么、替换什么、渲染什么。