“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
分布式 API 设计(下):REST
前言
接上篇,我们聊完了 gRPC 和 Thrift 这些偏二进制的 RPC 方案,今天来聊聊 REST。说实话,REST 可能是这些年被误解得最深的“架构风格”了——很多人觉得“只要用了 HTTP + JSON 就是 REST”,但事实上,REST 的核心思想跟 HTTP 动词、状态码这些具体实现关系不大,它更关心的是资源的表述和超媒体驱动。
这篇笔记我会从实际开发者的视角出发,把 REST 的设计原则、常见误区、以及怎么在分布式系统里用好它,掰开了揉碎了讲清楚。代码示例会尽量贴近真实场景,不会搞那种“Hello World”级别的玩具代码。
1. REST 到底是什么?先纠正几个常见误解
1.1 REST 不是协议,是架构风格
很多人把 REST 跟 HTTP 绑定得太死了,甚至觉得“RESTful API = HTTP API”。其实 Fielding 博士论文里定义的 REST,核心约束是:
- 客户端-服务器分离:解耦 UI 和数据存储
- 无状态:每个请求包含所有必要信息,服务端不存客户端上下文
- 可缓存:响应隐式或显式标记是否可缓存
- 分层系统:客户端不需要知道是直接连服务器还是通过中间件
- 统一接口:这是最关键的,包括资源标识、通过表述操作资源、自描述消息、以及超媒体作为应用状态引擎(HATEOAS)
你看,这里面根本没提 JSON 还是 XML,也没说必须用 GET/POST。HTTP 只是 REST 的一种常见实现载体,但 REST 完全可以运行在其他协议上(比如 CoAP 用于物联网)。
1.2 最常见的错误:把 REST 当成 CRUD 映射
很多团队的“RESTful API”长这样:
GET /users -> 查询用户列表
POST /users -> 创建用户
GET /users/{id} -> 获取单个用户
PUT /users/{id} -> 更新用户
DELETE /users/{id} -> 删除用户
这其实是资源式 RPC,不是 REST。真正的 REST 关注的是资源的状态转移,而不是对数据库的增删改查。比如“下单”这个操作,CRUD 思维会设计成 POST /orders 然后返回订单 ID,但 REST 的思维是:你先获取购物车的表述,然后通过超链接“提交订单”来转移状态。
2. 核心概念深度拆解
2.1 一切皆资源,但资源不是数据表
资源是一种概念映射,可以是实体(用户、订单)、集合(用户列表)、甚至过程(计算器里的加法操作)。每个资源有唯一的标识符(URI),并且可以有多种表述(representation)——比如同一个用户资源,可以用 JSON、XML、HTML 甚至图片格式返回。
关键点:资源的状态由服务端管理,客户端只能通过表述来操作资源。你不能直接修改服务端的内存或数据库,只能通过发送新的表述来请求状态变更。
2.2 自描述消息:让每个请求都“带说明书”
REST 要求消息是自描述的,意思是客户端看到消息就能知道怎么处理,不需要额外文档。具体手段包括:
- Content-Type:告诉客户端怎么解析 body(
application/json、application/xml) - Link 头:给出相关资源的链接,比如分页时的
rel="next"、rel="prev" - 状态码:比如 201 Created 告诉客户端资源已创建,Location 头给出新资源 URI
举个例子,一个标准的 REST 响应应该长这样:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/12345
Link: </orders/12345/payment>; rel="payment"
{
"orderId": "12345",
"status": "pending",
"total": 29.99,
"_links": {
"self": { "href": "/orders/12345" },
"payment": { "href": "/orders/12345/payment" },
"cancel": { "href": "/orders/12345/cancel" }
}
}
注意这里的 _links 字段——这就是 HATEOAS 的体现。客户端不需要事先知道“取消订单”的 URL 是什么,直接从响应里拿就行。
2.3 HATEOAS:被绝大多数人忽略的核心
HATEOAS(Hypermedia As The Engine Of Application State)是 REST 的终极约束,也是实际项目里最容易被砍掉的部分。它的意思是:服务器通过超媒体(链接和表单)告诉客户端接下来能做什么。
想象一下你在浏览网页:你打开一个商品页面,页面上有“加入购物车”按钮和“查看详情”链接,你不需要提前知道这些 URL 是什么,浏览器渲染出来你点就行了。REST API 也应该这样——客户端不应该硬编码任何业务 URL,而是从初始入口(比如 /)开始,跟着链接走。
实际应用场景:
- 订单状态机:当订单状态是“pending”时,响应里只包含“支付”和“取消”的链接;当状态变成“paid”后,这些链接消失,出现“申请退款”的链接
- 分页:不返回
page=2这种参数,而是返回rel="next"的链接,客户端直接请求那个链接即可
不过说实话,在移动端和微服务场景下,HATEOAS 的实现成本很高,很多团队选择妥协。但如果你真的想做纯正的 REST,这是绕不过去的。
3. 实际设计中的关键决策
3.1 版本管理:别在 URI 里写版本号
我看到太多 API 长这样:/api/v1/users。这在 REST 哲学里是不推荐的,因为版本应该体现在 Content-Type 或请求头里,而不是 URI。URI 代表的是资源,不是接口版本。
推荐做法:
Accept: application/vnd.mycompany.v1+json
或者用自定义头:
X-API-Version: 1
这样当你升级到 v2 时,旧的客户端仍然可以通过 Accept 头请求 v1 格式,而 URI 保持不变。不过现实是,很多网关和客户端库对自定义 Content-Type 支持不好,所以实际项目中用 URI 版本号也不是不行,只是要知道这不是 RESTful 的做法。
3.2 查询、过滤与分页
REST 里查询参数应该用来过滤、排序、分页,而不是用来定义动作。比如:
GET /orders?status=paid&sort=-created_at&page=2&per_page=20
这里 status 是过滤条件,sort 是排序(负号表示降序),page 和 per_page 是分页。注意 page 不是资源标识符的一部分,所以放在 query string 里没问题。
分页响应应该包含链接:
{
"data": [...],
"_links": {
"self": { "href": "/orders?page=2" },
"first": { "href": "/orders?page=1" },
"prev": { "href": "/orders?page=1" },
"next": { "href": "/orders?page=3" },
"last": { "href": "/orders?page=10" }
},
"meta": {
"total": 200,
"per_page": 20,
"current_page": 2
}
}
3.3 异步操作的处理
有些操作耗时很长(比如生成报表、处理视频),不能同步返回结果。REST 的做法是:
- 客户端发起请求,服务器返回 202 Accepted,并在 Location 头里给一个“状态查询”的 URL
- 客户端轮询这个 URL,得到 200 OK 时表示完成,或者 303 See Other 重定向到最终结果
示例:
POST /reports
Content-Type: application/json
{
"type": "monthly_sales",
"month": "2024-01"
}
响应:
HTTP/1.1 202 Accepted
Location: /operations/abc123
Content-Type: application/json
{
"operationId": "abc123",
"status": "processing",
"_links": {
"self": { "href": "/operations/abc123" }
}
}
客户端轮询 /operations/abc123,得到:
HTTP/1.1 200 OK
Content-Type: application/json
{
"operationId": "abc123",
"status": "completed",
"resultUrl": "/reports/monthly_sales_2024_01.pdf",
"_links": {
"result": { "href": "/reports/monthly_sales_2024_01.pdf", "type": "application/pdf" }
}
}
4. 与 gRPC 的对比:什么时候选 REST?
| 维度 | REST | gRPC |
|---|---|---|
| 数据格式 | JSON/XML/HTML 等文本为主 | Protocol Buffers 二进制 |
| 性能 | 文本解析慢,Payload 大 | 二进制高效,Payload 小 |
| 浏览器支持 | 原生支持(fetch/XMLHttpRequest) | 需要 gRPC-Web 代理 |
| 流式传输 | 有限(SSE、Chunked) | 原生支持四种流模式 |
| 工具生态 | 极其丰富(Postman, curl 等) | 需要 protoc 编译,工具较少 |
| 学习曲线 | 低,HTTP 基础即可 | 需要理解 protobuf、HTTP/2 |
| 契约定义 | 无强制契约(OpenAPI 是文档) | 强契约(.proto 文件) |
| 适合场景 | 对外 API、浏览器客户端、异构系统 | 内部微服务、高性能场景、流处理 |
我的经验是:
- 对外公开的 API:首选 REST,因为客户端多样性太高,JSON 的普适性最好
- 内部服务间调用:如果对性能要求高(比如每秒上万次调用),或者需要流式传输,gRPC 更合适
- 混合架构:很多公司用 gRPC 做服务间通信,然后用 REST API Gateway 暴露给外部
5. 实际项目中的坑与经验
5.1 不要滥用 200 OK
很多团队不管成功失败都返回 200,然后在 body 里放一个 code 字段:
{
"code": 1001,
"message": "参数错误",
"data": null
}
这完全违背了 HTTP 语义。正确的做法是:
- 成功:2xx(200, 201, 204)
- 客户端错误:4xx(400, 404, 409)
- 服务端错误:5xx(500, 502, 503)
这样客户端可以根据状态码做初步处理,而不需要解析 body。
5.2 注意幂等性
HTTP 方法有天然的幂等性语义:
- GET、HEAD、OPTIONS:幂等且安全(不改变资源状态)
- PUT、DELETE:幂等但不安全(多次调用结果相同)
- POST:既不幂等也不安全(每次可能创建新资源)
如果你用 POST 做更新操作,需要自己保证幂等性(比如通过幂等键 Idempotency-Key 头)。
5.3 链接管理:别让客户端硬编码
即使不完全实现 HATEOAS,也建议在响应里包含常用链接。比如用户登录后返回:
{
"userId": 123,
"name": "张三",
"_links": {
"self": "/users/123",
"orders": "/users/123/orders",
"profile": "/users/123/profile"
}
}
这样当以后 /users/123/orders 路径发生变化时,只需要服务端改,客户端不需要更新代码(前提是客户端实现了链接发现逻辑)。
6. 总结:REST 的精髓不在 HTTP,而在架构思想
写到最后,我想说:REST 是一种设计哲学,不是一套技术规范。它的核心价值在于:
- 解耦:客户端和服务端通过统一接口交互,各自可以独立演进
- 可发现性:超媒体让客户端像浏览网页一样浏览 API
- 可伸缩性:无状态和缓存机制让系统可以水平扩展
但现实是,大多数“REST API”其实只是 HTTP API with JSON。这没什么丢人的——如果你的系统不需要 HATEOAS 带来的灵活性,那简化版本完全够用。关键是要清楚自己在做什么选择,以及为什么。
最后给个实用建议:如果团队里没人能说清楚 REST 和 HTTP API 的区别,那就别标榜自己“RESTful”,老老实实叫 HTTP API 就行。少一个被误解的术语,多一份代码的清晰。