Skip to content

Commit d3a385e

Browse files
committed
new file: _posts/2017-12-03-donot-fuckup-the-url.md
1 parent 4659d6a commit d3a385e

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: 合理地设计 URL
3+
tags: HTTP URL 路由
4+
---
5+
6+
URL 是 Web 三大基石之一,在 Web 开发、运维和使用过程中随处可见。
7+
而且在 Web 开发中遇到的第一个设计问题,可能就是 URL 的设计。
8+
在很多 MVC 架构下的开发者眼中它的作用却只是路由到一个控制器,这正是一切罪恶的开端。
9+
10+
<!--more-->
11+
12+
# 常见的失败设计
13+
14+
这是一篇关于 REST 的文章,但是在介绍这种设计之前我们先来探讨糟糕的 URL 设计会带来哪些问题。
15+
下文的案例有的来自创业公司和外包公司,也有的来自 BAT 等公司的 Web 站点,
16+
但进行了一次抽象假装不针对任何人。
17+
18+
## 滥用路径
19+
20+
URL 分为协议、域名、路径、查询字符串、锚点几部分,其中锚点只由浏览器使用不发往服务器。
21+
Internet 早期 URL 路径会映射到文件系统路径,对应于资源位置,
22+
MVC 架构的动态站点中路径直接对应于控制器而不是静态文件。
23+
这使得 URL 路径事实上可以做任何事情而不只是定位资源。
24+
25+
比如用 URL 承载数据:
26+
27+
```
28+
/from=harttle/to=alice/message
29+
```
30+
31+
这个 URL 表示冲 Harttle 发往 Alice 的一条消息,消息内容可能在 HTTP body 中。
32+
这确实是一个[合法的 URL][url],但它承载了动态的数据内容。这意味着:
33+
34+
* 流量转发和负载均衡配置会更复杂(比如 [正则][reg] 而不是前缀),同时业务统计相关实现也会更困难。
35+
* 基于路径的 Web 技术不可用。比如 [Cookie][cookie] 的 Path 机制、带 Scope 的 [Service Worker][sw]
36+
* 给路径相关的前端 JavaScript 带来困难。虽然 OP 可能不关心 JavaScript 代码如何编写,业务 JavaScript 也不应依赖当前路径,但是一旦前端 JavaScript 需要判断路径(比如前端路由配置)这样的 URL 设计将会带来噩梦。
37+
38+
## 不表示 anything
39+
40+
URL 是用来定位资源的,偏有人设计 URL 不表示 anything。
41+
把用来定位资源的数据放到别的地方,比如 Cookie 中。看这个例子:
42+
43+
```
44+
URL: /admin-search
45+
Cookie: word=harttle
46+
```
47+
48+
这是一个搜索页面,搜索词是 `harttle`
49+
这样 HTTP 缓存会很难做,也让页面无法被收藏和分享。总之破坏了 Web:超链接不通了。
50+
51+
## 随意命名
52+
53+
比起滥用路径,命名带来的问题会小很多:最坏情况就是没人看得懂,但仍然机器可读。
54+
但命名问题仍然值得探讨,至少会让开发者和用户直观地看到它指向的资源。设想你的登录页面 URL 是:
55+
56+
```
57+
/page3/process0.html
58+
```
59+
60+
那么要打开或者指向一个登录页面一定会很头疼,设计成 `/account/login` 则会轻松很多。
61+
命名上的建议非常简单:使用可读的名词。
62+
63+
* 可读。要别人能看懂,不然要 URL 干嘛。Harttle 甚至觉得拼音也 OK。
64+
* 名词。`/user-list``/show-users` 要好。设想你 POST 到 `/show-users` 语义如何解释呢?
65+
66+
# REST 架构风格
67+
68+
Web 最初用于共享静态的科学数据文件,随着用户和商业涌入,尤其是 Web Application 的出现,
69+
开发者和用户对 Web 的使用方式逐渐超出了 Web 的基础架构。
70+
R. T. Fielding 是 [HTTP 1.0][http] 和 Apache httpd 的作者,
71+
他在 [2000 年的博士论文][rt] 中提出的 REST 架构风格重申了 Web 应有的架构风格。
72+
73+
这篇论文总结了 Web 早期架构中那些合理的架构约束,
74+
并提出国际互联网的超媒体系统还应具有的架构约束,形成 REST 架构风格。
75+
REST 被广泛用于指导 HTTP 和 URI 设计。
76+
77+
## 相关概念
78+
79+
REST(Representational State Transfer)是指使用对应的 **representation** (表示)来操作 **resource**(资源)。与其他架构风格的核心区别在于所有组件采用统一的接口,
80+
这在 Web 应用架构中要求 URL 的设计(资源定位),HTTP 方法(资源的操作)都符合标准的语义。
81+
82+
**resource** 在 REST 中是指信息的抽象,任何信息都可以称作一个资源。比如一个图片,一个文档,甚至现在的天气。
83+
84+
**representation** 在 REST 中是指资源(*resource*)的当前状态或目标状态。包括一些字节,以及对应的 *representation metadata*,比如我们说的 HTML 就是资源的一个 representation。
85+
86+
## URL/Web Server 设计
87+
88+
REST 的思想被用于 URI、HTTP、HTML 等标准的定义,以及众多 http 服务器和客户端的实现。
89+
在实践中 REST 表现出充分的简单性、可扩展性以及伸缩性(这里省略 10 个架构优势 orz)。
90+
那么 Web 站点的开发者如何根据 REST 风格进行设计呢?
91+
92+
* 一个 URL 表示一个资源。实践中还有好多设计,比如 URL 层级表示资源的层级等。
93+
* HTTP 方法表示对资源的操作。比如 GET 表示获取,POST 表示创建,PUT 表示修改,DELETE 表示删除。
94+
95+
HTTP 方法的语义有明确的 [定义][http-spec],比如 GET 不应对资源进行修改,
96+
PUT 操作应该幂等,等等。
97+
它们的可缓存性在 [HTTP 标准][http-spec] 中也有明确的定义,
98+
你在设计 Web Server 时也需要满足这些要求。
99+
100+
# 优秀的设计
101+
102+
在说了这么多抽象的东西之后,在这里列举一些 Harttle 喜欢的 URL 设计,以供参考。
103+
104+
## 资源层级
105+
106+
URL 层级表示资源的层级,这给用户和开发者都带来很大的方便。
107+
108+
* 对于用户。可以方便地手动拼 URL, 或者从 URL 了解到当前资源的位置。
109+
* 对于开发者。可以方便地从 URL 定位对应的模块。
110+
111+
这里以 Github 为例。Github 中有两个重要的概念:用户和仓库。
112+
所有仓库必须属于某个用户(或组),每个仓库又有 Issue、Wiki 等资源。
113+
Github 是这样设计的:
114+
115+
* 用户: <https://github.com/harttle>
116+
* 用户的仓库: <https://github.com/harttle/liquidjs>
117+
* 用户的 Pages:<https://harttle.github.io>
118+
* 仓库的 pages:<https://harttle.github.io/liquidjs>
119+
120+
这里要提一个重要前提:产品和 URL 设计是强相关的。
121+
假如 PM 说我就要一个仓库不属于任何用户,你怎么破?
122+
123+
## 资源与 ID
124+
125+
在 Web 的 Stateless-Client-Server 架构中,每个请求都需要携带足够的信息,
126+
这时 ID 的重要性不言而喻。比如用来标识一个用户,一个图片,或者一条评论等等。
127+
128+
一般情况下使用顺序的 ID (比如递增的数字)或用户指定的 ID (比如 username)较为可读。
129+
比如 Github 使用顺序的 Issue ID,既简单又方便:
130+
131+
* Issue 的 ID:48
132+
* Issue 的 URL:<https://github.com/harttle/liquidjs/issues/48>
133+
* 在 Commit Message 中添加 `fix #48` 可以关闭该 Issue。
134+
* 在其他讨论中,可以使用 `#48` 来引用这个 Issue。
135+
136+
不敢想象如果 ID 使用 Hash 字符串,或者干脆存在 Session 中会让使用和开发变得多复杂。
137+
138+
## HTTP 方法
139+
140+
用 HTTP 方法来表示对资源的操作,这会很大程度上简化 HTML 和服务器实现。
141+
因为 Web 就是这样设计的。常见的 CRUD 操作对应的 HTTP 方法如下:
142+
143+
方法 | 操作
144+
--- | ---
145+
GET | 查询
146+
POST | 创建
147+
PUT | 修改
148+
DELETE | 删除
149+
150+
例如创建 Github Issue 的表单:
151+
152+
```html
153+
<form accept-charset="UTF-8" action="/harttle/liquidjs/issues" method="post">
154+
<div class="discussion-create clearfix">
155+
<input type="text" class="form-control input-block" name="issue[title]" placeholder="Title" value="">
156+
<textarea class="form-control input-block" name="issue[body]" rows="5" placeholder="Leave a comment"></textarea>
157+
<button type="submit" class="btn btn-block" data-disable-with="Submitting…">Submit new issue</button>
158+
</div>
159+
</form>
160+
```
161+
162+
只需定义 `method=post``button[type=subimit]` 即可。
163+
HTTP `<button>` 的语法和语义请参考 [表单提交:button input submit 的区别](/2015/08/03/form-submit.html)
164+
如果感兴趣表单的编码方式,请移步 [HTTP 表单编码 enctype](/2016/04/11/http-form-encoding.html)
165+
166+
由于历史原因 HTML 的 `<form>` 只支持 GET 和 POST,`XMLHttpRequest` 也不支持所有的 HTTP 方法。
167+
Harttle 通常会引入一个 [method overide][method-override] 来 Workaround,
168+
虽然引入了 Trick 但至少保持了设计上的简单。
169+
170+
[rt]: https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation_2up.pdf
171+
[url]: https://url.spec.whatwg.org/
172+
[sw]: /2017/04/09/service-worker-now.html
173+
[cookie]: /2015/08/10/cookie-session.html
174+
[reg]: /2016/02/23/javascript-regular-expressions.html
175+
[http]: /2014/10/01/http.html
176+
[http-spec]: https://www.w3.org/Protocols/HTTP/1.0/spec.html
177+
[method-override]: https://github.com/expressjs/method-override

0 commit comments

Comments
 (0)