16 KiB
Web的历史2️⃣-动态网页
上篇文章我们已经了解了静态网页是如何工作的,但是这样的网页是不能满足大家对互联网的需求的。举例子来说:你访问b站首页bilibili.com,每次刷新,首页上显示给你的视频都不一样,不同的人访问这个首页,显示的也不一样,按理说大家都是访问一个网址,背后应该都是同一个文件,为什么每个人都不一样呢?这种功能是如何实现的?
淘宝上有数不清的商品在售卖,如果淘宝为每一个商品都在服务器目录下面创建一个html文件,好让大家通过访问http://taobao.com/someproduct.html来查看商品信息,那这个工作量就非常大了。而且,这样的网页,基本上没有交互的功能:我们希望用户可以点击按钮就能购买商品,商家在网页后台上操作就能上传商品。这种功能应该如何实现呢?
暂时先不考虑这些高级的问题,让我们先从最基础的讲起:
服务器端内嵌(SSI)
如果你想向网站中插入动态内容,SSI是最简单,最直接的办法,比如我们的wiki有许多页面,但是每个页面都有一些共同的元素:页面头部的导航栏,左侧的列表,页脚等。如果为每个页面都复制一份相同的HTML的话,那就太麻烦了,有没有什么办法,可以使HTML一次编写,到处渲染呢?
SSI(Server Side Includes)就是满足这种需求的一个HTML宏语言。它有点类似于C语言的# include宏:
假设这是我们首页的HTML:
<h1>wiki</h1>
<!--#include file="navbar.html" -->
<div class="article"></div>
假如navbar.html的内容如下:
<tr>
<td>教程</td>
<td>文档</td>
<td>高级</td>
<td><a href="github.com/zscnsd/website">Github</a></td>
</tr>
那么用户访问我们首页时就会看到:
<h1>wiki</h1>
// highlight-start
<tr>
<td>教程</td>
<td>文档</td>
<td>高级</td>
<td><a href="github.com/zscnsd/website">Github</a></td>
</tr>
// highlight-end
<div class="article"></div>
如果导航栏的界面有变化,那么只需要修改navbar.html即可,不用修改网站中的每一个页面。
没错,SSI的功能就是简单地把指定的内容插入进HTML里。这对一些重复的元素(例如每个网页的页头,页脚,侧边栏)还有一些需要更新的内容很实用。
当然,SSI并没有解决动态网页的问题,它只是把需要手动更新的地方单独拿了出来,使维护静态网站更容易,所以程序员们又发明了CGI技术。
CGI
CGI(Common Gateway Interface) 是第一个真正实现动态网页的技术,它允许Web服务器执行外部程序来生成网页内容。
CGI的工作原理是:当用户访问特定URL时,服务器不是返回静态文件,而是执行一个程序,并将程序的输出作为HTTP响应返回给用户。
(举个天气预报的例子):
Web服务器通常会把能执行的程序(除开静态文件)放在一个叫cgi-bin的特殊目录里。假设我们服务器的这个文件夹里有一个查询天气的Python程序weather.py,当用户访问http://example.org/cgi-bin/weather.py?city=中山&date=2025-06-25时,我们的HTTP服务程序会自动执行放在路径中/cgi-bin/weather.py的这个Python脚本,并且将客户端的请求头和请求体传递给脚本;
脚本解析请求头中city=中山&date=2025-06-25这个参数,在数据库中查询这个日期的天气,然后返回一个HTML给HTTP服务程序,再把这个HTML返回给客户端。
如果没有设置CGI,那么服务程序只会返回给客户端TodayWeather.py这个脚本文件的代码本身。
其实,CGI是一个接口格式,它定义了我们编写程序与HTTP服务程序之间如何交互。通常,HTTP服务程序给CGI程序的输入就是环境变量,输出就是标准输出。
CGI的巧妙之处在于,服务器不是用什么复杂的方式和脚本沟通,而是把请求信息(比如URL参数里的城市)变成程序很轻松就能读到的环境变量。而程序也不需要复杂的操作进行IO,它只需要把生成的HTML代码打印出来,服务器就会自动收集这些打印的内容,然后发回给用户的浏览器。
下面是一个例子:
#!/bin/bash
# HTTP响应头
echo "Content-type: text/html"
echo ""
# HTML内容
echo '<html>'
echo '<head><title>系统信息</title></head>'
echo '<body>'
echo '<h1>服务器系统信息</h1>'
echo '<p>当前时间:'$(date)'</p>'
echo '<p>内存使用情况:</p>'
echo '<pre>'
free -h
echo '</pre>'
echo '</body>'
echo '</html>'
每次用户访问这个页面,都会看到实时的系统信息,真正实现了动态内容。
虽然CGI现在很少见了,但它建立了一个重要概念:将URL请求映射到程序函数,而不是静态文件。这个思想成为了现代Web开发的基础。
嵌入式脚本
随着动态网页需求的增长,纯CGI编程变得复杂。程序员们希望能够在HTML中直接编程动态代码,这样既保持了HTML的可读性,又能实现动态功能。
这个就是嵌入式脚本,顾名思义就是把脚本和HTML混在一起,在HTML中嵌入脚本;
但是这种脚本和今天的前端JavaScript不同,它是由后端解释执行的,在返回HTML响应之前,HTTP服务程序会检查这个HTML里面有没有可以执行的脚本内容,有的话就执行这些脚本,并且把脚本的输出嵌入到HTML里面。任何有效的HTML也是有效的这类脚本语言。
从CGI到嵌入式脚本的另外一个关键驱动力是性能。CGI每来一个请求,服务器就得创建一个新进程去运行CGI程序,完成后再销毁,开销很大。而嵌入式脚本通常则是直接作为服务器的一部分运行,效率远高于CGI。
JSP
举个例子吧,你可以轻松使用Java来创建动态网页,只需要把Java代码嵌入到HTML里面,使用<% %>包裹住代码:
<html>
<body>
<p>当前时间:<%= new java.util.Date() %></p>
</body>
</html>
复杂一点的例子:
<html>
<body>
<h1>欢迎访问我们的网站</h1>
<p>当前服务器时间:<%= new java.util.Date() %></p>
<p>您是第 <%= session.getAttribute("visitCount") %> 位访客</p>
<%-- 这是JSP注释,不会出现在最终HTML中 --%>
<%
// 这里可以写复杂的Java逻辑
String userName = request.getParameter("user");
if (userName != null) {
out.println("<p>欢迎您," + userName + "!</p>");
}
%>
</body>
</html>
:::info[session和cookie]
在这段JSP代码中有一个对象叫做session,这是什么呢?实际上,因为HTTP是无状态的协议,意味着两次请求之间是完全独立的,一次请求不应该依赖另一次请求。这显得有点不灵活,于是人们会在HTTP的请求体上夹带一些额外的参数,用于表明用户的身份信息,比如在用户登录网站之后,服务器会给客户端一个密钥,下一次客户端请求页面时带上这个密钥,服务器就知道这是某个用户的请求。在这种模式下,服务器需要为每个用户维护信息,比如最简单地需要维护密钥是对应哪个用户的,这些信息就叫做session。
:::
类似于这样的脚本叫做JSP(JavaServer Pages),它在后端返回时被转换成Java Servlet代码来执行,本质上,JSP是Java Servlet的一种语法糖。至于JSP和Java Servlet都是什么,自行了解吧。
PHP
比JSP更灵活的就是PHP,PHP就是一门纯正的脚本语言了,它的用法与JSP类似,使用<?php ?>包裹代码:
<html>
<body>
<h1>欢迎来到我的网站</h1>
<?php
$time = date('Y-m-d H:i:s');
echo "<p>当前时间:$time</p>";
?>
</body>
</html>
也可以这样写,这样就类似于CGI程序的写法了:
<?php
echo "<html><body>";
echo "<h1>欢迎来到我的网站</h1>";
$time = date('Y-m-d H:i:s');
echo "<p>当前时间:$time</p>";
echo "</body></html>";
?>
LAMP
这种动态网页的编写方法流行了很多年,形成了一个叫做"LAMP"的套路:Linux+Apache+MySQL+PHP;就是将电脑装上Linux系统,运行Apache这个HTTP服务端,使用PHP作为动态脚本语言,使用MySQL来存储和访问业务数据。
需要注意的是,这四个都是开源免费的软件,LAMP的兴起,是开源软件运动的标志之一。开源软件使得部署网站的成本极大地降低,推动了互联网的繁荣。如果你想建站,那时候互联网上到处都是"LAMP一键安装脚本"之类的东西,现在也能搜到不少。一个下午就能上线一个完备的网站。这些技术的出现,使得开网站不再局限于大企业才能办得到的事情,一时间互联网上到处都是个人或者小单位的网站,甚至后来出现了诸如Wordpress之类的方案,不会写代码也能开网站。繁荣的生态,网页上丰富的动态内容,形成了被我们称为“Web 2.0”的时代。
LAMP的一个典型反面是微软全家桶:Windows Server+IIS+SQL Server+ASP,这套技术方案需要给微软缴纳高额的授权费用,在当时基本上只限于追求稳定和售后服务的企业使用。我们的文章也没有怎么介绍这些技术。不过IIS对于个人用自己的电脑建站还是非常方便的。(当然国内没有公网IP那是另一回事了╮( ̄▽ ̄)╭)
MVC架构
随着网页的不断发展,出现了复杂的业务逻辑,并且页面也越来越复杂;这时候,把页面和程序逻辑混在一起的嵌入式脚本在庞大的复杂代码情况下变得难以维护。
而且它们都有一个特点:依赖于具体的某个HTTP服务程序,PHP依赖于Apache的mod_php或Nginx的FastCGI支持,JSP依赖于Servlet容器例如Tomcat,这增加了开发与部署的耦合度,更使得项目难以管理。嵌入式脚本难以复用已有的代码,这些代码的测试也需要模拟HTTP环境,难以测试。
此时兴起了一种新的Web后端编程思想,它就是MVC(Model-View-Controller)
简单来说,根据大量的开发经验累积,人们发现一个动态网页的后端通常需要做到这3件事情:
- Model:使用面向对象的方法为业务建模,把数据对应到编程语言中的对象,把对数据的操作对应到对象的方法。负责对业务数据进行实际的操作。
- View:输入数据,负责把数据变成用户可以直观看懂的HTML。
- Controller:负责协调,调用上面两个部分。
例如,当我们在报修系统中想要查询一个片区的全部报修时,首先我们访问http://wwbx.zsxyww.com/QueryTickets.php?zone=朝晖&status=pending
然后服务器根目录下的QueryTickets.php程序就会接受到我们的请求(在MVC时期的PHP程序已经不像嵌入式脚本那时混写HTML和PHP,整个文件就是以<?php开头的一整个脚本,没有HTML)
QueryTickets.php脚本就是MVC中的Controller,脚本解析到我们想寻找朝晖片区所有待解决的报修,于是它调用一个函数Ticket.Query(),但是Controller知道朝晖片区在数据库对应的编号是10,待解决状态的编号是0,于是他把URI参数中的朝晖和pending改成10和0传递给函数;
这个函数会去数据库(比如前面提到的MySQL)里查询数据,最终执行类似于SELECT * FROM tickets WHERE zone=10 AND status=0;这样的SQL语句,然后把查询的每一行都对应一个Ticket对象,返回一个Ticket的数组。这个函数,以及Ticket类的定义就是Model负责的部分。
:::info
SQL(Structured Query Language)是进行数据库操作的标准途径,你可以简单地把数据库理解成有更多功能和性能更高的Excel表格,即使这个表格有上亿行,数据库也能在不到几秒内精准执行复杂的数据读取或写入。
刚才的那个SQL语句,就是让数据库找到所有在10号片区(朝晖)未解决的工单,给出这些工单的所有信息。是不是非常直观方便?
数据库往往是后端的核心。许多后端系统,可以说就是SQL数据库的套壳,它们的业务逻辑不会超过"CRUD",这也是为什么例如Supabase之类的产品能够如此的流行。
:::
Model是程序的核心。它不关心页面长什么样,只负责处理和业务相关的数据。在我们报修系统的例子里,它定义了一张报修单应该包含哪些信息(如ID、地点、状态),并提供了操作这些数据的方法(如从数据库查询报修单、更新报修单状态等)。这是网站业务逻辑的体现。
当Controller获得Model返回的数据时,它就把这些数据移交View函数渲染,调用View函数。View首先把Ticket对象里面的10和0改成朝晖和待解决,然后检查客户端的UA,如果是电脑的话就在一行显示更多数据,如果是手机的话就返回紧凑的界面。根据不同的访问设备,预先在系统中存放了一些模板HTML文件,view读取这些文件,然后将数据放到里面,返回给用户;
这样用户访问QueryTickets.php时,就会看到一个根据后台数据实时更新的一个页面。这就是一个简单的MVC架构页面的例子。
:::tip[提示]
哈哈,这个例子其实是骗你的,我们的报修系统既不是用的PHP,也不是用的MVC架构,甚至路由都是虚拟的。这个例子只是让你比较好懂~
:::
虚拟路由
实际上,程序员们认为基于传统服务器文件的路由架构严重阻碍了后端系统的灵活设计。也就是说,用户输入的URI,必须对应根目录里面一个实际存在的文件,比如上面的QueryTickets.php,在后端就是一个实际存在的脚本。由HTTP程序负责调用这个脚本,并且把脚本的输出发送过去。
MVC架构,包括更新的设计,都采用虚拟路由。也就是说,URI不再匹配根目录里的一个文件,什么URI匹配什么现在完全取决于程序员希望它匹配什么,比如匹配程序里的某个函数(不再一定是脚本语言,可以编译语言例如C语言的函数),把函数的输出传给用户。
通常的MVC设计下,在MVC之前程序还有一个路由层,客户端的HTTP请求首先到达这里,经过这里解析后,转交给不同的地方
举例来说明,当用户访问http://wwbx.zsxyww.com/Tickets/朝晖/pending的时候,程序员可以在路由层定义:所有以/Tickets开头的URI,全部转交Query()函数处理,Query()看到转交过来的请求头URI在/Tickets后面是/朝晖/pending,就去数据库查询朝晖的待解决工单,然后调用渲染函数返回HTML。在服务器根目录下面是没有/Tickets/朝晖/pending这个文件的。嗯...其实连根目录都不需要了,
发展到后来,连HTTP服务器也没有了:因为程序员们觉得每次都要配置一个独立的HTTP服务器(比如Apache)再来运行自己的程序有点麻烦。于是,很多现代的Web框架干脆自己内置了一个迷你的HTTP服务器功能。启动程序时,这个内置的服务器也一起启动了,让开发和部署变得更简单。
当然,这种模式下,URI也可以传统地绑定到某个文件上。例如,我们的官网www.zsxyww.com在接受到任何无效的URI时,都会将请求路由到文件404.html上,表示没有找到你请求的东西,比如,www.zsxyww.com/hahaha,和www.zsxyww.com/aaa/bbb都没有在程序路由中规定,一切超出规定的URI全部都路由到404.html文件上,返回一个静态的文件。
MVC框架
MVC作为一种编程思想可以被程序员灵活地使用,不过也有一些在编程语言基础上编写的MVC框架,来约束程序员使用MVC的思想开发后端,也简化了开发;这种框架有很多,而且不少编程语言都有,我们介绍几个有代表性的:
- Ruby on Rails 约定大于配置
- Django
- Spring Boot
:::info
这是三篇系列文章中的第2篇
点击以跳转:
动态网页(你在看的文章)
:::

