# Web 基础 [toc] ## PHP:超文本预处理器的本质与动机 ### 动机:让网页“活”起来 最早的网页是纯 HTML,内容写死。1994 年,Rasmus Lerdorf 想在自己的个人主页统计访客、处理表单,纯 HTML 做不到;当时的 CGI 方案要把程序逻辑和 HTML 分开写,又麻烦又割裂。于是他写了一个小工具,允许把动态逻辑直接嵌进 HTML——这就是 PHP 的雏形,最初叫 Personal Home Page Tools,后来改名为 PHP: Hypertext Preprocessor(递归缩写:PHP = PHP: Hypertext Preprocessor)。 ### 本质:服务器端先加工页面,再发给浏览器 PHP 是在服务器上执行的、专门用来生成 HTML 的脚本语言: 1. 浏览器请求 `.php` 页面; 2. 服务器让 PHP 解释器执行 `` 之间的代码; 3. 把执行结果和静态 HTML 拼成纯 HTML; 4. 浏览器只收到成品 HTML,永远看不到 PHP 源码。 所以叫“预处理器”:在超文本真正送出去之前先加工一遍。它更像页面模板引擎,而不是独立应用。 ```text 浏览器请求 /index.php ↓ 服务器 → PHP 解释器执行代码,输出 HTML ↓ 浏览器收到纯 HTML,渲染页面 ``` 特点: - 嵌入而非分离:HTML 与动态逻辑写在一个文件里; - 服务器端执行:用户浏览器接触不到逻辑; - Web 原生:`$_GET` / `$_POST` / `$_SESSION`、内置数据库接口,天生为网页请求设计; - 门槛低:LAMP(Linux + Apache + MySQL + PHP)一套环境就能跑。 一句话:PHP 不生产网页,它是网页的加工车间。今天新项目很少首选它,但 WordPress 等大量存量网站仍由它支撑。 ## 新项目用什么替代 PHP 没有单一继任者,按场景分化: | 方向 | 代表 | 定位 | |---|---|---| | 低门槛全栈 | Node.js / TypeScript(Express、NestJS、Next.js) | 一门语言同时管前后端;Next.js 把 PHP 式的服务端渲染页面带回来了 | | 好写、生态大 | Python(Django 全家桶、FastAPI 写 API) | 语法干净,适合逻辑不复杂的应用;优势在生态和易维护 | | 性能优先 | Go(Rust / Axum 等) | 编译成二进制、并发好、部署简单,适合 API 服务和中规模后端 | | 老牌复杂业务 | Ruby on Rails、Java / Kotlin(Spring) | Rails 曾代表更优雅的开发体验;Java/Kotlin 在大公司稳居主力 | | 个人网站 / 博客 | 静态站生成器(Astro、Hugo)或继续用 WordPress | PHP 最初的地盘,如今很多人不再需要动态后端 | ### 范式变化:前后端分离 + 静态托管 PHP 时代的形态是“一个服务器把页面整个渲染好发给你”。现在很多新项目是:前端用 React/Vue 打包成静态页面,后端只提供 JSON API(Node/Python/Go 都行),部署到 Vercel、Netlify、Cloudflare 这类平台,甚至不用自己管服务器。 一句话总结:低门槛全栈 → Node/Next.js;好写 → Python;性能 → Go;大厂复杂业务 → Java/Kotlin。 ## REST:Web API 的设计约定 REST(Representational State Transfer,表现层状态转移)不是协议,而是一套基于 HTTP 的 API 设计风格。核心思路:把业务对象抽象成「资源」,用 URL 定位资源,用 HTTP 方法表达操作,用状态码表达结果。 ### 核心概念:资源、表现、无状态 - 资源(Resource):一个可以被命名的业务对象,例如用户、订单、文章。 - URL 是名词不是动词:`GET /users/42` 表示“用户 42”,而不是 `/getUser?id=42`;URL 层级表达从属,如 `/users/42/posts`。 - 表现(Representation):服务器返回的是资源的某种表现形式(通常 JSON),同一资源可以有多种表现。 - 无状态(Stateless):每个请求自带全部上下文,服务器不保存客户端会话;登录态由客户端通过 Token / Cookie 携带。 - 可缓存 / 分层:GET 通常可缓存;客户端一般不关心请求是被应用服务器还是网关处理的。 ### HTTP 方法与幂等 | 方法 | 语义 | 是否幂等 | 典型场景 | |---|---|---|---| | GET | 读取资源 | 是 | 查询列表 / 详情 | | POST | 新建资源(或触发动作) | 否 | 创建订单 | | PUT | 整体替换资源 | 是 | 用完整数据覆盖更新 | | PATCH | 部分更新 | 协议不保证,常约定为可重复 | 只改一个字段 | | DELETE | 删除资源 | 是 | 删除订单 | 幂等 = 同一个请求执行一次和多次结果一致。网络重试时只有幂等方法可以安全重放:GET / PUT / DELETE 可以,POST 不行(会创建重复资源),所以创建接口常用「客户端幂等键」或唯一约束兜底。 ### 状态码速查 | 范围 | 含义 | 常见例子 | |---|---|---| | 2xx | 成功 | 200 OK、201 Created、204 No Content | | 4xx | 客户端错误 | 400 参数错、401 未登录、403 无权限、404 不存在、409 冲突、422 语义校验失败 | | 5xx | 服务端错误 | 500 内部错误、502 网关错误、503 服务不可用、504 网关等待上游超时 | 设计惯例:错误响应也要有统一结构(error code + message + 可选的字段级详情),方便前端和调用方处理。 **504 是网关 / 代理的等待结果,不是根因名称。** 它表示完成请求所需的上游响应没有及时到达;不能单凭状态码断言是 DNS、连接建立、应用处理还是某个具体节点出错,也不能据此认定上游从未收到请求或已经停止执行。客户端自己触发的 timeout 异常,与收到 HTTP 504 响应是不同事件。[RFC 9110 §15.6.5](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.5) ### REST 不是万能模板 - REST ≠ HTTP API:很多自称 RESTful 的接口只是把 CRUD 机械映射到 HTTP,没有真正按资源建模。 - 复杂动作放不进 CRUD 时,用「动作子资源」:`POST /orders/1/cancel` 比 `POST /orders/1?action=cancel` 更清晰。 - 需要按需取字段、跨实体聚合时,GraphQL 更合适;强类型、高性能的服务间调用,gRPC 更合适;REST 胜在简单、通用、浏览器和工具链天然支持。 - 团队要自己补的约定:版本化(`/v1/`)、分页(`?page=&size=`)、过滤排序、认证方式、错误格式——REST 本身不规定这些。 接口契约与前后端联调见 [Software-Engineering:前后端协作与接口联调](./Software-Engineering.md#前后端协作与接口联调)。 ## BFF:Backend For Frontend BFF(Backend For Frontend,服务于前端的后端)指:不为所有客户端维护一个通用 API 后端,而是让每类客户端(Web、iOS、Android、第三方、机顶盒……)各有一个自己的服务端组件,专门负责这一端的数据聚合、裁剪与协议适配。 这个模式出自 SoundCloud 2015 年的实践:早期只有一个单体 API 同时服务 Web、Android、iOS 和外部合作方,共享 API 变成了所有平台的公约数,加功能要等所有人对齐,越来越慢;后来他们让驱动功能的前端团队自己实现一个 feature-specific facade,放在单体之外,这个新层归前端团队所有,可以按平台和功能自由裁剪数据与交互。 ### 通用 API 后端为什么失效 - **需求异质**:移动端要更少的往返、更小的包体、更弱的网络假设(省电、省流量);桌面 Web 场面更大、想要更多字段、可以承受多次请求。一份响应很难同时讨好两端。 - **组织瓶颈**:一个通用后端同时服务多个客户端团队,任何改动都要排队、都要兼容所有调用方,成为所有人共享的发布瓶颈;它又不属于任何业务领域,容易长成一块「智能中间件」。 - **逻辑外溢**:没有 BFF 时,客户端专属逻辑会散落到各个客户端里;多端技术栈不同,重复反而不容易被发现。 ### 职责边界 | 该做 | 说明 | |---|---| | 聚合 | 一次客户端请求扇出到多个下游服务,合并成一个 payload | | 裁剪与整形 | 只返回这个界面要用的字段;按界面需要分页、反规范化、格式化 | | 协议适配 | 对外 REST / GraphQL / SSE,对内 gRPC、内部 REST 混用 | | 降级 | 部分下游失败时返回部分数据(如去掉库存角标),而不是整体报错 | | 客户端横切 | 这一端特有的会话 / token、缓存、埋点 | 不该做:领域规则(属于下游服务)、与客户端无关的通用网关职责(统一鉴权、限流、TLS——更适合上游网关层,除非多一跳的延迟不可接受)。BFF 里堆业务逻辑,等于把「胖后端」换个位置。 ```text 客户端一次请求 GET /api/wishlist ↓ BFF 并行扇出 ├── wishlist 服务(条目 id) ├── catalog 服务(名称、价格) └── inventory 服务(库存) ↓ 合并成一个 JSON,只返回这一端要用的字段 ``` ### 几个 BFF,归谁所有 - 「one experience, one BFF」:体验接近的端可以共用一个——SoundCloud 的 iOS / Android 听歌端曾共用一个 BFF(后来他们自己也表示可能重新考虑);差异大就拆——REA 按平台各配一个。 - 决定数量的其实是团队边界:一个移动团队配一个 BFF;iOS / Android 分属两个团队就各配一个。团队结构比系统结构更容易变,所以宁可先拆开(又一次 Conway's Law)。 - BFF 应该由写这个前端的团队拥有并发布:API 随 UI 一起改,不用跨团队排期;某块功能放客户端还是服务端,也可以由这一个团队自己决定。 这里体现了[康威定律](./Software-Engineering.md#康威定律组织沟通与系统边界):实际沟通与所有权会影响接口边界。按团队划分 BFF 是一种实践选择,不是定律要求“一团队一服务”;还要结合端的体验差异、共享成本与独立发布需求判断。 ### BFF / API Gateway / GraphQL | | 定位 | 典型职责 | |---|---|---| | API Gateway | 所有流量的统一入口 | 路由、鉴权、限流、TLS、灰度、监控 | | BFF | 一类客户端的服务端组件 | 聚合、裁剪、协议适配、客户端专属降级 | | GraphQL 统一端点 | 客户端自己描述要什么数据 | 由客户端驱动字段选择,能替代一部分「裁剪」职责;但聚合怎么编排、缓存怎么设、下游挂了怎么降级,仍然要有人负责 | ### 什么时候用 值得引入: - 客户端类型多,且交互 / 体验差异大(移动 vs 桌面 vs 第三方) - 一个页面要扇出多个下游服务做聚合 - 前端迭代快,不想被后端 API 排期卡住 - 某个客户端更适合另一种语言 / 技术栈 不值得: - 只有一个客户端,或各客户端请求几乎相同——通用 API 就够 - 团队很小,多维护一个可部署单元的成本大于收益 - 只是需要一个地方塞业务逻辑 ### 代价与常见坑 - 多一跳、多一个部署单元:延迟、测试、监控、发布面都变大,不是白拿的。 - 万能层回潮:需求一个个塞进来,BFF 退化成必须服务所有人的通用后端。 - 重复代码:多个 BFF 会重复聚合逻辑。跨服务重复可以容忍;抽共享库要谨慎(会引入耦合),更好的做法是把聚合下沉到领域服务,或遵循「写到第三次再抽象」。 - 缓存:聚合结果的过期时间必须取所有组成部分中最短的那个。 - 能力要求:前端团队要自己处理并发、超时、缓存、可观测性。 - 今天很多 SSR 框架(Next.js 的 Route Handler / Server Components、Nuxt 的 server routes)本身就自带这一层,BFF 从额外架构层变成框架默认能力;判断标准不变——它是否只服务这一类客户端、能否由该客户端团队自主发布。 一句话:BFF 把「这个界面需要什么数据、以什么代价拿到」的决策权交回给拥有该界面的团队;代价是多一个部署单元,收益是界面迭代不再等后端排期。 > 参考:Sam Newman, [Backends For Frontends](https://samnewman.io/patterns/architectural/bff/)(模式定义、BFF 数量与团队边界、复用与横切关注点);SoundCloud 工程师在 Thoughtworks 博客上的复盘 [BFF @ SoundCloud](https://www.thoughtworks.com/insights/blog/bff-soundcloud)(模式起源与动机);[Azure Architecture Center: Backends for Frontends pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends)(适用 / 不适用场景、与网关的分工)。 ## 相关笔记 - [通信与网络](./通信与网络.md):HTTP 与网络层基础 - [Software-Engineering:前后端协作与接口联调](./Software-Engineering.md#前后端协作与接口联调) - [Database](./Database.md):数据存储