RestQL:现代化的 API 开发方式
youbin1129
8年前
<p>koa-restql 已经在 <a href="/misc/goto?guid=4959676767544135363" rel="nofollow,noindex">github</a> 开源并在 <a href="/misc/goto?guid=4959676767651925488" rel="nofollow,noindex">npm</a> 发布。感兴趣的同学可以前往围观一下。欢迎 Pull Request,同时热烈欢迎 Star。</p> <p>在现代的业务系统中,后端开发工作基本上可以被拆分为三项:</p> <ul> <li><strong>接口鉴权</strong> 。例如拍段是不是当前系统的用户,以及该用户是否有权限访问接口。</li> <li><strong>与其他系统的交互</strong> 。例如调用第三方的服务,或内部搭建的其他服务。</li> <li><strong>数据操作</strong> 。基本上所有需要持久化存储的系统都会在这项工作上耗费大量时间。</li> </ul> <p>本文将介绍如何利用 RestQL 来非常有效地减少「数据操作」相关的工作量。</p> <h2>现状与挑战</h2> <p>我们先来做个假设。</p> <ul> <li>假设系统中有 60 张表,每张表对应的接口都要有四种 CRUD 的 API。那么就需要后端工程师写 60 * 4 = 240 个API。</li> <li>假设上述 60 张表中,40 张表存的是资源类的数据,其余 20 张表为关系类的数据,也就是说每张表和 20 张表都要进行关联,每个关联也需要四种 CRUD 操作,那么又要增加 40 * 20 * 4 = 3200 个API。</li> </ul> <p>所以在上述假设场景中,后端工程师要编写 3200 + 240 = 3440 个 API。而且这还不是全部。假如后端代码需要 100% 的测试覆盖,那么工程师们就要写至少 3440 个测试!</p> <p>60 张表 = 3440 个 API + 3440 + 个单元测试</p> <p>众所周知,数据操作 API 的实现过程基本上是重复的,有的同学甚至认为这是低端的,体现不出工程师价值的工作,纯粹的「体力活」。但是却没有一个能真正解放生产力的方案。</p> <h2>解决思路</h2> <p>尽管我们把数据库抽象成了「关系型」数据库,把操作数据的命令抽象成了 SQL ,同时我们也有了 MySQL 客户端,甚至是 sequelize 这种非常方便的库,也有「RESTful」API 命名规则,但是接口的实现从来都是需要工程师们自己用手敲出来的。</p> <p>如果说我看得比别人远,那是因为我站在巨人的肩膀上。</p> <p>所以我们在现有的技术基础上再抽象,把已有的东西重新组合起来,拼装成一个新的工具,帮助工程师从「体力活」中解脱出来,解放生产力。</p> <h3>什么样的工具</h3> <p>最开始的时候,我们最先需要明确的问题就是:「我们需要什么样的工具?」或者说「这种工具要帮我们解决什么问题?」。</p> <p>实际上我们从刚才的假设中,已经可以得出结论:我们希望有一个工具可以让工程师免于编写数据操作 API, <strong> <em>把数据库操作直接映射到 HTTP RESTful API 上</em> </strong> 。</p> <h2>调用方式</h2> <h3>如何请求</h3> <p>为了解释「如何请求」,我们先从一些公认的规则出发,举一个例子,然后再从例子中抽象出一些规则。</p> <p>注意:为了更便于理解,我们把所有的命名从客户端一直穿透到数据库,所以请不要纠结于我们在定义一个 API 时名词单复数的问题。</p> <p>基本用例</p> <p>几乎所有的系统都会有一个用户表(user)。根据 RESTful 规则的约定,我们应该把访问 user 表的 API 路径定义为 /user ,并把 CRUD 的访问方法映射到 HTTP 协议中的四种方法: GET 、 POST 、 PUT 、 DELETE 。</p> <p>比如:</p> <ul> <li>GET /user :获取用户列表,应该返回一个数组。</li> <li>GET /user/:id :获取指定的用户,应该返回一个对象。</li> <li>POST /user :创建一个用户,应该返回被存储的对象,状态码应该为 201(Created)。</li> <li>PUT /user :修改一个用户的信息,应该返回修改后的对象。</li> <li>DELETE /user/:id :删除一个用户,状态码应该为 204(No Content)。</li> </ul> <p>如果 user 表有一个关系表 feed,那么我们的路径就会再复杂一点:</p> <ul> <li>GET /user/:id/feed 或 GET /feed?user_id=:id :获取某个用户的帖子,应该返回一个数组。</li> <li>GET /user/:id/feed/:feed_id 或 GET /feed/:id :获取指定的帖子,应该返回一个对象。</li> </ul> <p>上述的例子中还会衍生出其他的数据操作,不仅仅只有 GET ,这里不一一列举了。</p> <p>抽象出规则</p> <p>上一节中,列举了要提供一个表的数据访问 API,大概要实现哪些路由。从这些枚举中,可以找出其中的规律,总结出一套规则。最终我们在「 <strong>把能实现的路由,全部实现</strong> 」的原则基础上,开发了 RestQL 的 koa 版本。</p> <p>支持的 HTTP 方法:</p> <table> <thead> <tr> <th>HTTP verb</th> <th>CRUD</th> </tr> </thead> <tbody> <tr> <td>GET</td> <td>Read</td> <td> </td> </tr> <tr> <td>POST</td> <td>Create</td> <td> </td> </tr> <tr> <td>PUT</td> <td>Create/Update</td> <td> </td> </tr> <tr> <td>DELETE</td> <td>Delete</td> <td> </td> </tr> </tbody> </table> <p>支持的带有 body 的 HTTP 方法:</p> <table> <thead> <tr> <th>HTTP verb</th> <th>List</th> <th>Single</th> </tr> </thead> <tbody> <tr> <td>POST</td> <td>Array/Object</td> <td>×</td> <td> </td> </tr> <tr> <td>PUT</td> <td>Array/Object</td> <td>Object</td> <td> </td> </tr> </tbody> </table> <p>说明:</p> <ul> <li>List 路径为返回值为数组的路径,包括: <ul> <li>/resource</li> <li>/resource/:id/association , association 为 1:n 关系</li> <li>/resource/:id/association , association 为 n:m 关系</li> </ul> </li> <li>Single 路径为返回值为单个对象的路径,包括: <ul> <li>/resource/:id</li> <li>/resource/:id/association , association 为 1:1 关系</li> <li>/resource/:id/association/:id , association 为 1:n 关系</li> <li>/resource/:id/association/:id , association 为 n:m 关系</li> </ul> </li> </ul> <h3>如何使用</h3> <p>我们已经开源了 koa-restql,koa 应用开发者可以通过 npm 安装它:</p> <pre> npm install koa-restql</pre> <p>然后在 koa 应用的代码中引用 RestQL:</p> <pre> const koa = require('koa') const RestQL = require('koa-restql') let app = koa() // Build APIs from `sequelize.models` let restql = new RestQL(sequelize.models) app.use(restql.routes())</pre> <h2>常见问题</h2> <h3>修改参数</h3> <p>用户可以通过 querystring 来修改参数。强烈建议使用 qs 对 querystring 进行解析,例如:</p> <pre> qs.stringify({a: 1, b:2}) // => a=1&b=2</pre> <p>RestQL 中的 querystring 仅有 3 条规则:</p> <ul> <li> <p>所有不以 _ 开头的建,都会被放进 sequelize#query() 的 where 参数中。例如:</p> <pre> // query { name: "Li Xin" } // option for sequelize { where: { name: "Li Xin" } }</pre> </li> <li> <p>所有以 _ 开头的建,都会被放进 sequelize#query() 的参数中,和 where 保持平级。例如:</p> <pre> // query { _limit: 10 } // option for sequelize { limit: 10 }</pre> </li> <li> <p>当需要使用关系时,可以用关系名称的字符串代替关系对象传入。例如需要使用 include 时:</p> <pre> // query { _include: ['friends'] } // option for sequelize { include: [ models.user.association.friends ] }</pre> </li> </ul> <h3>访问控制</h3> <p>通常来说,我们有两种方法实现访问控制:</p> <p>通过中间件</p> <p>在 koa 应用挂载 RestQL 的 router 之前,可以实现一个鉴权中间件,控制用户的访问权限:</p> <pre> app.use(authorizeMiddleware) app.use(restql.routes())</pre> <p><img src="https://simg.open-open.com/show/544462ab4f79944c58fb66badec9cb00.jpg"></p> <p>通过 restql 参数</p> <p>在使用 sequelize 定义关联时,我们可以设定 restql 参数,实现访问控制。例如:</p> <ul> <li> <p>禁止通过 restql 访问关联:</p> <pre> models.user.hasOne( models.privacy, { restql: { ignore: true } } )</pre> </li> <li> <p>禁止通过 restql 使用指定的 HTTP 方法访问关联</p> <pre> models.user.hasOne( models.privacy, { restql: { ignore: ['get'] } } )</pre> </li> </ul> <p>其他语言/框架</p> <p>目前我们仅实现了基于 node 和 koa 的版本,还没有其他语言/框架的实现版本。欢迎开发者提交其他语言/框架的实现到 <a href="/misc/goto?guid=4959676767747608649" rel="nofollow,noindex">RestQL 组</a> 。</p> <h2>参考链接</h2> <ul> <li>GitHub, <a href="/misc/goto?guid=4958854326152945436" rel="nofollow,noindex">koajs/koa</a></li> <li>GitHub, <a href="/misc/goto?guid=4959615068530574391" rel="nofollow,noindex">sequelize/sequelize</a></li> <li>GitHub, <a href="/misc/goto?guid=4959676767544135363" rel="nofollow,noindex">meituan-dianping/koa-restql</a></li> </ul> <p> </p> <p>来自:http://tech.meituan.com/koa-restql.html</p> <p> </p>