深入理解前后端分离
最基础的前后端分离:一个文件夹,两个文件
忘掉 Vue、忘掉框架。只有两个文件:一个 HTML,一个 PHP。放在同一个文件夹,用同一个域名 127.0.0.1 访问。
一、准备:建一个空文件夹
mkdir /media/HUAWEI/D/wwwroot/demo
cd /media/HUAWEI/D/wwwroot/demo
这个文件夹里什么都是空的。 接下来只建两个文件。
二、文件 1:index.html(前端页面)
为什么叫 index.html?
因为浏览器访问一个目录时,默认找 index.html 或 index.php。所以 http://127.0.0.1:8000/ 会自动打开这个文件。
为什么是 .html 不是 .php?
这个文件不需要 PHP 处理,它就是纯静态页面。浏览器下载后直接渲染,PHP 服务器不需要碰它。
手动新建 index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>最基础的前后端分离</title>
</head>
<body>
<h1>登录</h1>
<p>
用户名:<input type="text" id="username" value="admin">
</p>
<p>
密码:<input type="password" id="password" value="123456">
</p>
<button id="btn">登录</button>
<p id="result"></p>
<script>
document.getElementById('btn').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const res = await fetch('login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, password: password })
});
const data = await res.json();
const result = document.getElementById('result');
if (data.code === 200) {
result.textContent = '登录成功!你好,' + data.nickname;
result.style.color = 'green';
} else {
result.textContent = data.msg;
result.style.color = 'red';
}
});
</script>
</body>
</html>
逐行讲解
<!DOCTYPE html>
告诉浏览器:这是 HTML5 文件。
<html lang="zh-CN">
lang 是语言,zh-CN 是简体中文。方便浏览器选择字体、辅助工具读屏。
<meta charset="UTF-8">
字符编码。不加这行,中文可能显示成乱码。
<input type="text" id="username" value="admin">
type="text":文本框id="username":给 JS 定位用。JS 通过document.getElementById('username')拿到这个元素value="admin":默认值
<input type="password" id="password" value="123456">
type="password":输入内容显示成圆点。
<button id="btn">登录</button>
给这个按钮一个 id="btn",JS 才能找到它。
<p id="result"></p>
一个空段落。JS 会往里面填结果。
下面是 JS 部分,逐行看:
document.getElementById('btn').addEventListener('click', async () => {
document.getElementById('btn'):从页面上找到id="btn"的按钮.addEventListener('click', ...):监听「点击」事件,点击时执行后面的函数async () => {}:异步箭头函数。因为里面要用await
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
- 从两个输入框里读出用户输入的值
.value是输入框的当前内容
const res = await fetch('login.php', {
- 核心行。
fetch是浏览器自带的函数,用来发 HTTP 请求 'login.php'是相对路径。因为当前页面是http://127.0.0.1:8000/index.html,所以浏览器会发到http://127.0.0.1:8000/login.phpawait等请求完成res是服务器返回的响应对象
method: 'POST',
请求方法用 POST。不写默认是 GET。因为要提交数据,所以用 POST。
headers: { 'Content-Type': 'application/json' },
请求头。告诉服务器:「我发过来的请求体是 JSON 格式」。
这一行非常关键,它决定了 PHP 那边怎么读数据。
body: JSON.stringify({ username: username, password: password })
JSON.stringify把 JS 对象转成字符串{ username: 'admin', password: '123456' }变成'{"username":"admin","password":"123456"}'body必须是字符串,不能直接传对象
});
请求配置结束。
const data = await res.json();
res.json()把响应体(字符串)解析成 JS 对象- 内部做的其实是
JSON.parse(响应体字符串) await因为res.json()也是异步的
const result = document.getElementById('result');
拿到页面上那个空段落,准备往里面填内容。
if (data.code === 200) {
result.textContent = '登录成功!你好,' + data.nickname;
result.style.color = 'green';
} else {
result.textContent = data.msg;
result.style.color = 'red';
}
根据服务器返回的结果,显示不同文字。
三、文件 2:login.php(后端接口)
为什么叫 login.php?
因为 index.html 里的 fetch('login.php', ...) 就是发的这个名字。名字必须对上。
为什么是 .php 不是 .html?
因为这个文件需要 PHP 处理(读请求、查数据、返回结果)。它不输出 HTML,只输出 JSON。
手动新建 login.php:
<?php
// 告诉浏览器:我返回的是 JSON,不是 HTML
header('Content-Type: application/json; charset=utf-8');
// 读原始请求体(就是 fetch 里 body 那段字符串)
$raw = file_get_contents('php://input');
// 把 JSON 字符串转成 PHP 数组
$input = json_decode($raw, true);
// 拿出用户名和密码
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
// 假装查数据库(真实场景用 PDO 查 MySQL)
if ($username === 'admin' && $password === '123456') {
echo json_encode([
'code' => 200,
'nickname' => '管理员'
], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode([
'code' => 401,
'msg' => '用户名或密码错误'
], JSON_UNESCAPED_UNICODE);
}
逐行讲解
<?php
告诉 PHP 服务器:这行之后是 PHP 代码。
header('Content-Type: application/json; charset=utf-8');
- 告诉浏览器:响应体是 JSON 格式,编码是 UTF-8。
- 不加这行,浏览器会当成 HTML 处理,可能出现解析问题。
charset=utf-8保证中文不乱码。
$raw = file_get_contents('php://input');
php://input是 PHP 的原始请求体流。file_get_contents是「读取文件内容」的函数,php://input被当成一个特殊文件。$raw是字符串:'{"username":"admin","password":"123456"}'
关键理解: 为什么不用 $_POST?
$_POST只对Content-Type: application/x-www-form-urlencoded的请求自动填充。- 我们这里发的
Content-Type: application/json,PHP 不认识 JSON,所以$_POST是空的。 - 必须自己读
php://input。
$input = json_decode($raw, true);
json_decode把 JSON 字符串转成 PHP 数组- 第二个参数
true表示「返回数组」,不写返回对象 $input现在是:['username' => 'admin', 'password' => '123456']
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
??是「空合并运算符」:如果键不存在,用''代替。- 防止 PHP 报「未定义键」的警告。
if ($username === 'admin' && $password === '123456') {
- 判断用户名密码。真实场景这里应该查数据库。
===全等,类型和值都要一样。
echo json_encode([
'code' => 200,
'nickname' => '管理员'
], JSON_UNESCAPED_UNICODE);
json_encode把 PHP 数组转成 JSON 字符串JSON_UNESCAPED_UNICODE防止中文被转义成\uXXXX(不加这个,'管理员' 会变成\u7ba1\u7406\u5458)echo输出字符串,这就是响应体
} else {
echo json_encode([
'code' => 401,
'msg' => '用户名或密码错误'
], JSON_UNESCAPED_UNICODE);
}
失败时返回错误信息。
四、两个文件放在一起
/media/HUAWEI/D/wwwroot/demo/
├── index.html ← 前端页面
└── login.php ← 后端接口
就这么简单。
五、启动服务器
在 demo 文件夹里执行:
cd /media/HUAWEI/D/wwwroot/demo
php -S 127.0.0.1:8000
注意:这次没有 -t public。 因为 index.html 和 login.php 直接在 demo/ 里,demo/ 就是网站根目录。
终端会显示:
PHP 8.x.x Development Server (http://127.0.0.1:8000) started
六、访问测试
打开浏览器,访问:
http://127.0.0.1:8000/
会发生什么?
- 浏览器请求
http://127.0.0.1:8000/ - PHP 服务器发现根目录有
index.html,直接把它返回(不需要 PHP 处理) - 浏览器渲染页面,显示登录表单
点「登录」按钮:
- JS 读输入框的值
fetch('login.php', ...)发 POST 到http://127.0.0.1:8000/login.php- PHP 服务器发现是
.php文件,调用 PHP 解释器执行 login.php读出 JSON 请求体,比对用户名密码- 返回 JSON 响应
- 浏览器收到响应,
res.json()解析 - 页面显示「登录成功!你好,管理员」
七、用 F12 看整个过程
按 F12 → Network 面板。点登录按钮。
你会看到一个叫 login.php 的请求。 点开它:
Headers 标签:
Request URL: http://127.0.0.1:8000/login.php
Request Method: POST
Status Code: 200
Content-Type: application/json
Payload 标签(你发给服务器的):
{"username":"admin","password":"123456"}
Response 标签(服务器返回给你的):
{"code":200,"nickname":"管理员"}
这三块内容,就是整个通信过程的核心。
- Request URL:请求地址
- Request Method:POST
- Content-Type:请求体是什么格式
- Payload:请求体内容
- Status Code:响应状态
- Response:响应体内容
八、为什么这叫「前后端分离」?
同一个域名 127.0.0.1:8000,但两个文件各管各的:
index.html |
login.php |
|
|---|---|---|
| 管什么 | 页面显示 | 数据处理 |
| 返回什么 | HTML | JSON |
| 谁来处理 | 浏览器 | PHP |
| 谁调谁 | 主动发请求 | 被动响应 |
核心:
- 页面和数据分开。页面只管展示,数据由 PHP 提供。
- 通过 HTTP 通信。HTML 里的 fetch 发请求,PHP 返回 JSON。
- 同一个域。都是
127.0.0.1:8000,所以不需要处理跨域。
这就是最小化的前后端分离。
九、把它和 Vue 项目对照
Vue 项目其实就是这样:
| 这个 demo | Vue 项目 |
|---|---|
index.html |
src/views/Login.vue |
login.php |
backend/public/index.php + AuthController.php |
fetch('login.php') |
http.post('/auth/login') |
JSON.stringify({...}) |
request.ts 里 JSON.stringify(options.body) |
res.json() |
request.ts 里 return res.json() |
data.code === 200 |
userStore.login 里的判断 |
同一个域名 127.0.0.1:8000 |
开发时 Vite 代理到 127.0.0.1:8000,生产时 Nginx 同域 |
区别只有:
- Vue 把页面拆成了组件(
Login.vue、Dashboard.vue) - Vue 用路由做页面跳转(
router.push) - Vue 用 Pinia 存登录状态
- Vue 用 Vite 编译
.vue/.ts/.scss
但 HTTP 通信的部分完全一样:还是 fetch,还是 POST,还是 JSON。
十、总结
一个文件夹、两个文件,就是最小化前后端分离:
demo/
├── index.html ← 前端:显示页面 + 发请求
└── login.php ← 后端:收请求 + 返回数据
通信过程:
浏览器打开 index.html
↓
用户点击「登录」
↓
JS 发 POST 到 login.php
↓
PHP 收到 JSON,处理,返回 JSON
↓
JS 收到 JSON,更新页面
核心知识点:
fetch发请求JSON.stringify对象 → 字符串file_get_contents('php://input')读原始请求体json_decode字符串 → PHP 数组json_encodePHP 数组 → 字符串res.json()字符串 → JS 对象
理解了这个 demo,就理解了 Vue 项目的所有通信逻辑。 Vue 只是把 index.html 拆成组件、把 login.php 拆成控制器,本质没变。

浙公网安备 33010602011771号