web编程30年进化史

一个"在线人数"的 30 年进化史

从静态 HTML 到前后端分离,用最简单的例子讲透一切


引子:一个看似简单的问题

假设你是某网站开发者,老板对你说:

"页面上加一个'当前在线人数',显示现在有多少人在线。"

对用户来说,这只是页面上的一个数字。但为了显示这个数字,整个 Web 技术走了 30 年。

下面跟着这个数字,走完它的一生。


第一幕:1991-1993,静态 HTML 时代

你的代码

<!DOCTYPE html>
<html>
<body>
  <h1>欢迎来到我的网站</h1>
  <p>当前在线人数:5</p>   <!-- 手写的 5 -->
</body>
</html>

逐行解释

<!DOCTYPE html>

告诉浏览器这是 HTML5 文件。

<p>当前在线人数:5</p>

这里的 5 是死的。 你现在打开看到 5,明天打开还是 5,永远不变。

为什么不行?

因为"在线人数"是动态的,每分钟都在变。静态 HTML 没有"变"的能力。

问题:怎么让页面上的数字变起来?


第二幕:1993,CGI 时代——第一次让页面"动"起来

核心思想

1993 年,NCSA 发明了 CGI(Common Gateway Interface)。它的思路是:

服务器遇到请求时,不光能返回静态文件,还能调用一个程序,把程序输出的内容当响应返回。

这是关键的一步:页面内容从"写死的"变成"程序生成的"。

你的代码(Perl 版)

文件放在 /cgi-bin/online.cgi:

#!/usr/bin/perl

# ============ 第一步:算出"在线人数" ============
my $count = 0;
open(my $fh, '<', '/var/data/online_users.txt') or die "打不开文件";
while (my $line = <$fh>) {
    chomp($line);
    $count++ if $line ne '';
}
close($fh);

# ============ 第二步:输出 HTTP 响应 ============
print "Content-Type: text/html\n\n";
print "<!DOCTYPE html>";
print "<html><body>";
print "<h1>欢迎来到我的网站</h1>";
print "<p>当前在线人数:$count</p>";
print "</body></html>";

逐行解释

my $count = 0;

初始化计数器。my 声明局部变量,$ 表示标量(单个值)。

open(my $fh, '<', '/var/data/online_users.txt') or die "打不开文件";
  • open 打开文件
  • < 是"只读"模式
  • or die "..." 打开失败就报错退出
while (my $line = <$fh>) {
  • <$fh> 每次从文件读一行
  • while 循环到文件末尾为止
    chomp($line);

去掉行尾的换行符。比如 "1001\n" 变成 "1001"。

    $count++ if $line ne '';
  • if $line ne '' 如果这行不是空行
  • $count++ 计数器加一
  • 这是 Perl 的后置 if 写法,等价于 if ($line ne '') { $count++; }
print "Content-Type: text/html\n\n";

关键行。 这行输出一个 HTTP 响应头:

  • Content-Type: text/html 告诉浏览器响应体是 HTML
  • \n\n 是两个换行,表示头结束了,后面开始是响应体
print "<p>当前在线人数:$count</p>";

把 $count 的值(比如 5)拼进字符串。这就是"服务器动态生成 HTML"。

发生了什么?

浏览器  →  GET /cgi-bin/online.cgi
Apache  →  启动 Perl 解释器
Perl    →  打开文件、数行数、拼 HTML
Perl    →  把 HTML 写到 stdout
Apache  →  把 stdout 当 HTTP 响应发回
浏览器  →  渲染 <p>当前在线人数:5</p>

这时代码和 HTML 混在一起,但这是第一次"页面能变"。


第三幕:1995,PHP 混排时代——你熟悉的起点

核心思想

PHP 让"程序代码"和"HTML"能写在一个文件里,比 Perl CGI 更自然。

你的代码(online.php)

<?php
// ============ 第一部分:算数据 ============
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$count = count($lines);
?>
<!DOCTYPE html>
<html>
<body>
  <h1>欢迎来到我的网站</h1>
  <p>当前在线人数:<?php echo $count; ?></p>
</body>
</html>

逐行解释

<?php

告诉服务器:这之后是 PHP 代码,需要执行。

$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  • file() 一次读所有行到数组
  • FILE_IGNORE_NEW_LINES 去掉每行的换行符
  • FILE_SKIP_EMPTY_LINES 跳过空行
  • $lines 变成 ['1001', '1002', '1003', '1004', '1005']
$count = count($lines);

count() 数数组元素个数。$count = 5。

?>

结束 PHP 代码。后面的内容原样输出给浏览器。

<p>当前在线人数:<?php echo $count; ?></p>
  • <?php ... ?> 又切回 PHP,输出 $count 的值
  • 结果:<p>当前在线人数:5</p>

这一段 PHP 代码的本质

如果你在浏览器"查看源代码",看到的只是:

<p>当前在线人数:5</p>

PHP 代码已经被服务器执行完,浏览器收到的是执行结果。

问题

每次打开页面,整个 HTML 重新生成一遍,整个页面刷新一次。体验不流畅。


第四幕:2005,Ajax 时代——局部刷新

核心思想

2005 年,Ajax 技术普及。让 JS 只请求数据,然后手动更新页面的一小块。

不再让服务器返回整个页面。

拆成两个文件

文件 1:count.php(只返回数字)

<?php
// 只返回一个数字,不返回 HTML
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
echo count($lines);

文件 2:index.html(页面 + JS)

<!DOCTYPE html>
<html>
<body>
  <h1>欢迎来到我的网站</h1>
  <p>当前在线人数:<span id="count">--</span></p>

  <script>
    // 每 5 秒自动刷新一次
    setInterval(async () => {
      // 向 count.php 发 GET 请求
      const res = await fetch('count.php');
      // 把响应当纯文本读取
      const text = await res.text();
      // 更新页面上 id="count" 的那个 span
      document.getElementById('count').textContent = text;
    }, 5000);
  </script>
</body>
</html>

逐行解释

echo count($lines);
  • PHP 只输出一个数字(比如 5)
  • 不输出 HTML,没有任何标签
  • 响应体就是字符串 "5"
<p>当前在线人数:<span id="count">--</span></p>
  • <span id="count"> 给一个位置占位
  • 初始显示 --,等 JS 填充
setInterval(async () => {

setInterval 每隔一段时间执行一次函数。

  const res = await fetch('count.php');
  • fetch 向 count.php 发 HTTP 请求
  • await 等请求完成
  • res 是响应对象
  const text = await res.text();
  • res.text() 把响应体读成字符串
  • 这里是 "5"
  document.getElementById('count').textContent = text;
  • 找到页面上 id 为 count 的元素
  • 把它的文字内容改成 "5"

关键点

  • 页面不刷新。只更新了 <span> 里的文字。
  • count.php 只返回数据("5"),不返回 HTML。
  • 浏览器和服务器之间传来传去的,只是纯数据。

这就是"前后端分离"的雏形。


第五幕:2006+,JSON 时代——数据不再只是数字

需求升级

老板说:"不仅显示在线人数,还要显示最多人用的浏览器、平均停留时间。"

一个数字不够了,需要返回一个结构化的对象。

文件 1:stats.php(返回 JSON)

<?php
// 读文件
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// 假装有一些统计数据
$stats = [
    'online'   => count($lines),
    'browser'  => 'Chrome',
    'avg_time' => 320,
];

// 告诉浏览器:我返回的是 JSON,不是纯文本
header('Content-Type: application/json; charset=utf-8');

// 把 PHP 数组转成 JSON 字符串输出
echo json_encode($stats, JSON_UNESCAPED_UNICODE);

逐行解释

$stats = [
    'online'   => count($lines),
    'browser'  => 'Chrome',
    'avg_time' => 320,
];

定义一个 PHP 关联数组,相当于 JS 里的对象。

header('Content-Type: application/json; charset=utf-8');

关键行。 告诉浏览器:"我返回的响应体是 JSON 格式"。浏览器拿到后能自动识别。

echo json_encode($stats, JSON_UNESCAPED_UNICODE);
  • json_encode 把 PHP 数组转成 JSON 字符串
  • 结果:{"online":5,"browser":"Chrome","avg_time":320}
  • JSON_UNESCAPED_UNICODE 防止中文被转义

文件 2:index.html(前端请求 + 渲染)

<!DOCTYPE html>
<html>
<body>
  <h1>欢迎来到我的网站</h1>
  <p>当前在线人数:<span id="count">--</span></p>
  <p>最多浏览器:<span id="browser">--</span></p>
  <p>平均停留:<span id="avg">--</span> 秒</p>

  <script>
    async function loadStats() {
      // 1. 发请求
      const res = await fetch('stats.php');

      // 2. 解析响应为 JS 对象
      // res.json() 内部做了:JSON.parse(响应字符串)
      const data = await res.json();

      // 3. 渲染到页面上
      document.getElementById('count').textContent   = data.online;
      document.getElementById('browser').textContent = data.browser;
      document.getElementById('avg').textContent     = data.avg_time;
    }

    loadStats();                // 打开页面立即加载
    setInterval(loadStats, 5000); // 每 5 秒刷新
  </script>
</body>
</html>

逐行解释

const res = await fetch('stats.php');

向 stats.php 发请求。

const data = await res.json();
  • res.json() 把响应体当 JSON 解析
  • 内部逻辑等价于:JSON.parse('{"online":5,"browser":"Chrome","avg_time":320}')
  • data 变成 JS 对象 {online: 5, browser: 'Chrome', avg_time: 320}
document.getElementById('count').textContent = data.online;
  • 从 data 里取 online 字段
  • 更新对应的 DOM 元素

关键点

数据格式从"一个数字"进化到"一个 JSON 对象"。

这是现代前后端的标准做法。JSON 成为前后端的通用语言。


第六幕:现代——前后端分离

项目结构

wwwroot/demo/
├── index.html           ← 前端页面
└── api/
    └── stats.php        ← 后端 API

后端:api/stats.php

<?php
// ========== 1. 设置响应头 ==========
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');   // 允许跨域(开发时)

// ========== 2. 读数据 ==========
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// ========== 3. 组织结果 ==========
$result = [
    'code' => 200,
    'data' => [
        'online'   => count($lines),
        'browser'  => 'Chrome',
        'avg_time' => 320,
    ],
];

// ========== 4. 输出 JSON ==========
echo json_encode($result, JSON_UNESCAPED_UNICODE);

逐行解释:

header('Content-Type: application/json; charset=utf-8');

告诉浏览器返回 JSON。

header('Access-Control-Allow-Origin: *');

CORS 头。 允许任何域名跨域请求这个 API。开发时用 *,生产要改成具体域名。

$result = [
    'code' => 200,
    'data' => [...],
];

标准化响应结构。前端永远先看 code,再看 data。

echo json_encode($result, JSON_UNESCAPED_UNICODE);

输出 JSON 字符串。

前端:index.html

<!DOCTYPE html>
<html>
<body>
  <h1>欢迎来到我的网站</h1>
  <p>当前在线人数:<span id="count">--</span></p>
  <p>最多浏览器:<span id="browser">--</span></p>
  <p>平均停留:<span id="avg">--</span> 秒</p>

  <script>
    async function loadStats() {
      try {
        // 1. 发请求到后端 API
        const res = await fetch('/api/stats.php');

        // 2. 检查 HTTP 状态码
        if (!res.ok) {
          throw new Error('HTTP ' + res.status);
        }

        // 3. 解析响应为 JS 对象
        const json = await res.json();

        // 4. 检查业务状态码
        if (json.code !== 200) {
          throw new Error(json.msg || '未知错误');
        }

        // 5. 渲染
        const data = json.data;
        document.getElementById('count').textContent   = data.online;
        document.getElementById('browser').textContent = data.browser;
        document.getElementById('avg').textContent     = data.avg_time;

      } catch (e) {
        console.error('加载失败:', e.message);
      }
    }

    loadStats();
    setInterval(loadStats, 5000);
  </script>
</body>
</html>

逐行解释

if (!res.ok) {

res.ok 是布尔值。HTTP 状态码在 200-299 之间时为 true。否则为 false。

throw new Error('HTTP ' + res.status);
  • throw 主动抛出异常
  • new Error(...) 创建错误对象
  • 会被下面的 catch 捕获
if (json.code !== 200) {

业务层面的检查。 HTTP 状态码是 200(传输成功),但业务层可能返回"用户未登录"之类的错误码。

const data = json.data;

从响应里取出 data 字段。前端和 PHP 约定好:数据永远在 data 字段里。

} catch (e) {
  console.error('加载失败:', e.message);
}

捕获任何错误(网络错误、解析错误、业务错误),打印到控制台。

关键点

  • 后端只管数据:读文件、组织 JSON、输出。
  • 前端只管显示:请求、解析、渲染、错误处理。
  • 双方约定:URL 是 /api/stats.php,返回格式是 {code, data, msg}。

这就是前后端分离:两个文件,两套代码,一份合同。


第七幕:数据从哪来?——"5"的一生

前面所有例子都依赖 /var/data/online_users.txt。这个文件是怎么来的?

真相:每次用户请求时,被一点点写进去的

写文件的代码(login.php 或其他任何用户请求的页面):

<?php
// 假设用户已登录,用户 ID 是 1001
$userId = 1001;

// 把用户和时间戳追加到文件
// time() 是当前 Unix 时间戳
file_put_contents(
    '/var/data/online_users.txt',
    $userId . ':' . time() . "\n",
    FILE_APPEND
);

逐行解释:

$userId = 1001;

用户 ID。真实场景从 session 或 token 里取。

time();

PHP 内置函数,返回当前时间戳(从 1970-01-01 00:00:00 UTC 到现在的秒数)。

$userId . ':' . time() . "\n";
  • . 是 PHP 字符串拼接
  • 结果是 "1001:1735689600\n"
file_put_contents(..., FILE_APPEND);
  • file_put_contents 写文件
  • FILE_APPEND 标志:追加到文件末尾,不覆盖

清理过期用户

<?php
// clean_online.php,由 cron 每分钟执行
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES);
$keep = [];

foreach ($lines as $line) {
    // 按冒号拆分:"1001:1735689600"
    [$uid, $last] = explode(':', $line);
    
    // 如果 5 分钟内有活动,保留
    if (time() - (int)$last < 300) {
        $keep[] = $line;
    }
}

file_put_contents('/var/data/online_users.txt', implode("\n", $keep) . "\n");

逐行解释:

[$uid, $last] = explode(':', $line);
  • explode(':', '1001:1735689600') 拆成 ['1001', '1735689600']
  • [$uid, $last] = ... 数组解构,把两个值分别赋给两个变量
if (time() - (int)$last < 300) {
  • (int)$last 把字符串转成整数
  • time() - $last 是"上次活动距今多少秒"
  • < 300 表示 5 分钟(300 秒)内还有活动
$keep[] = $line;

把这一行加入"保留数组"。

file_put_contents(..., implode("\n", $keep) . "\n");
  • implode("\n", $keep) 把数组拼成多行字符串
  • 写回文件

时间线

时刻        触发              文件内容
──────────────────────────────────────────────
10:00      空文件             (不存在)

10:01      用户 1001 登录     "1001:1735689660"
10:03      用户 1002 登录     "1001:1735689660
                              1002:1735689780"
10:05      用户 1001 活动     "1001:1735689900" ← 时间戳更新
                              1002:1735689780"
10:10      清理脚本跑         "1001:1735689900" ← 保留
                              (1002 被删,因为超 5 分钟)

结论:文件不是"有人一次生成"的,是每一次用户请求一点一点追加的。


第八幕:为什么能"分离"?——五种语言的同一件事

同样的"读文件、返回 JSON",用不同语言写:

语言 读文件 转 JSON 输出
PHP file() json_encode() echo
C fgets() 手动拼接字符串 printf
Go os.Open() + Scanner json.Marshal() w.Write()
Python open() json.dumps() wfile.write()
Node.js fs.readFile() JSON.stringify() res.end()

看起来完全不同,但底层做的是同一件事:

  1. 读数据:打开文件,读内容
  2. 算结果:数行数,算在线人数
  3. 序列化:把结果变成 JSON 字符串
  4. 写字节:把字符串写入 HTTP 响应

浏览器的感受完全一样。 它收到的永远是:

HTTP/1.1 200 OK
Content-Type: application/json

{"code":200,"data":{"online":5}}

前后端能分离的根本原因:所有语言最终都产生相同的 HTTP 字节流。


第九幕:核心原理总结

1. HTTP 就是字节流

HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
\r\n
{"code":200,"data":{"online":5}}
  • 状态行、响应头、空行、响应体
  • 全部是字节
  • 任何语言写后端,本质上都是"把字节写进 socket"

2. 数据不是"存在"的,是"算出来"的

  • "在线人数 5"不是服务器"知道"的
  • 是每次请求时,程序打开文件、读内容、数有效行算出来的
  • 数据源(文件、数据库)也是用户请求时一点点写进去的

3. 前后端的"合同"

双方约定:

  • URL:/api/stats.php
  • 方法:GET
  • 请求格式:(GET 无请求体)
  • 响应格式:{code: 200, data: {online: 5}}

只要遵守合同,前端用 Vue、React、原生 JS 都行;后端用 PHP、Go、Python 都行。

4. 分离的三个层次

层次 内容
文件分离 index.html 和 stats.php 是两个文件
职责分离 前端管显示,后端管数据
部署分离 前端可以放 CDN,后端放服务器;独立升级

第十幕:从静态到分离的完整时间线

1991 静态 HTML        →  手写"当前在线 5"(永远不变)
1993 CGI              →  Perl 读文件、拼 HTML(页面能变)
1995 PHP 混排         →  PHP + HTML 写一起(更好写)
2005 Ajax             →  只请求数据、局部刷新(不刷新页面)
2006 JSON             →  数据从数字进化到对象
2010 SPA 框架         →  Vue/React 独立渲染页面
2015 前后端分离       →  前端项目 + 后端项目,独立部署
2020 微前端 / BFF     →  前端再分化,后端加聚合层
现在 你在做的事       →  Vue3 + PHP API

每一次演进,都是因为旧方案遇到了瓶颈。

  • 静态 HTML → 页面不能变 → CGI
  • CGI 难写 → PHP 混排
  • 混排不流畅 → Ajax
  • Ajax 只返回数字 → JSON
  • 页面逻辑爆炸 → 前端框架
  • 前后端耦合 → 分离

结语:从"5"看到整个 Web

现在你再看到页面上"当前在线人数:5"这个数字,应该能想到:

  1. 用户行为:某个人打开了网页(HTTP 请求)
  2. 后端写入:login.php 把用户和时间戳追加到文件
  3. 数据存储:文件 /var/data/online_users.txt 一行行累积
  4. 定时清理:clean_online.php 删掉超时用户
  5. 后端查询:stats.php 数出有效行数
  6. 序列化:json_encode 转成 JSON 字符串
  7. 字节输出:通过 HTTP 响应发给浏览器
  8. 前端接收:fetch 收到 JSON 字符串
  9. 前端解析:res.json() 转成 JS 对象
  10. 前端渲染:document.getElementById('count').textContent = 5

这个数字不是"存在"于某个地方的,它是整条链路上所有程序协作的产物。

这就是 Web 的本质:数据的采集、存储、查询、传输、渲染。前端和后端不是两个孤立的系统,是同一个数据流的两端。

理解了这一点,无论用什么语言,你都能看懂它背后的逻辑。

posted @ 2026-10-13 17:12  华腾智算  阅读(9)  评论(0)    收藏  举报
https://damo.alibaba.com/ https://tianchi.aliyun.com/course?spm=5176.21206777.J_3941670930.5.87dc17c9BZNvLL