1
0
mirror of https://github.com/ZSCNetSupportDept/website.git synced 2025-10-28 08:55:04 +08:00
Files
website/blog/Web的历史2️⃣-动态网页.md
2025-10-09 16:01:47 +08:00

287 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Web的历史2⃣-动态网页
<!-- truncate -->
上篇文章我们已经了解了静态网页是如何工作的但是这样的网页是不能满足大家对互联网的需求的。举例子来说你访问b站首页`bilibili.com`,每次刷新,首页上显示给你的视频都不一样,不同的人访问这个首页,显示的也不一样,按理说大家都是访问一个网址,背后应该都是同一个文件,为什么每个人都不一样呢?这种功能是如何实现的?
淘宝上有数不清的商品在售卖如果淘宝为每一个商品都在服务器目录下面创建一个html文件好让大家通过访问`http://taobao.com/someproduct.html`来查看商品信息,那这个工作量就非常大了。而且,这样的网页,基本上没有交互的功能:我们希望用户可以点击按钮就能购买商品,商家在网页后台上操作就能上传商品。这种功能应该如何实现呢?
暂时先不考虑这些高级的问题,让我们先从最基础的讲起:
## 服务器端内嵌(SSI)
如果你想向网站中插入动态内容SSI是最简单最直接的办法比如我们的wiki有许多页面但是每个页面都有一些共同的元素页面头部的导航栏左侧的列表页脚等。如果为每个页面都复制一份相同的HTML的话那就太麻烦了有没有什么办法可以使HTML一次编写到处渲染呢
SSI(Server Side Includes)就是满足这种需求的一个HTML宏语言。它有点类似于C语言的`# include`宏:
假设这是我们首页的HTML:
```html
<h1>wiki</h1>
<!--#include file="navbar.html" -->
<div class="article"></div>
```
假如`navbar.html`的内容如下:
```html
<tr>
<td>教程</td>
<td>文档</td>
<td>高级</td>
<td><a href="github.com/zscnsd/website">Github</a></td>
</tr>
```
那么用户访问我们首页时就会看到:
```html
<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代码打印出来服务器就会自动收集这些打印的内容然后发回给用户的浏览器。
下面是一个例子:
```bash title="/var/www/cgi-bin/system-info.sh"
#!/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里面使用`<% %>`包裹住代码:
```java
<html>
<body>
<p>当前时间:<%= new java.util.Date() %></p>
</body>
</html>
```
复杂一点的例子:
```java
<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 ?>包裹代码`
```php
<html>
<body>
<h1>欢迎来到我的网站</h1>
<?php
$time = date('Y-m-d H:i:s');
echo "<p>当前时间:$time</p>";
?>
</body>
</html>
```
也可以这样写这样就类似于CGI程序的写法了
```php
<?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负责协调调用上面两个部分。
![MVC](/img/blog/model-view-controller-light-blue.png)
例如,当我们在报修系统中想要查询一个片区的全部报修时,首先我们访问`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`文件上,返回一个静态的文件。
![现代Web后端](/img/blog/web_application_with_html_and_steps.png)
### MVC框架
MVC作为一种编程思想可以被程序员灵活地使用不过也有一些在编程语言基础上编写的MVC框架来约束程序员使用MVC的思想开发后端也简化了开发这种框架有很多而且不少编程语言都有我们介绍几个有代表性的
- Ruby on Rails 约定大于配置
- Django
- Spring Boot
:::info
这是三篇系列文章中的第**2**篇
点击以跳转:
[HTTP](/blog/Web的历史1⃣-HTTP)
**动态网页**(你在看的文章)
[Web应用](/blog/Web的历史3⃣-Web应用)
:::