阅读了阮一峰的 理解 RESTful 架构RESTful API 设计指南,对一些关键的概念做一个简单的备忘。

RESTful 概念理解

REST,全称 “Representational State Transfer”,阮翻译成 “表现层状态转化”,补全主语就是 “资源表现层状态转化”。主要特性:功能强、性能好、适宜通信。符合这样规范的 API 架构就可以称为 RESTful API。

资源是网络服务(HTTP)的主体,表现层是资源的存在方式和表现形式,状态变化是网络服务对资源的影响。

常见的表示 http 操作方式的动词有四种:GET、POST、PUT、DELETE。GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。

常见的 RESTful 设计误区

URI 中包含动词。RESTful 规范将动作放到请求类型中表示,所以 URI 中应该只包含资源信息。

可能之前的请求是这样的:

# 文章列表第一页
/api/article/list/1
# 文章编号为 1 的文章
/api/article/view/1
# 修改文章编号为 1 的文章
/api/article/update/1
# 删除文章编号为 1 的文章
/api/article/delete/1

这是请求 URI 经过(.htaccess 或者框架)改写之后的形式,最原始的请求可能直接就在 GET 中加几个参数,m 表示模块,c 表示控制器,a 表示方法。

RESTful 规范的 URI 应该是这样的:

# 文章列表第一页
/api/article ?page=1 GET
# 文章编号为 1 的文章
/api/article/1 GET
# 修改文章编号为 1 的文章
/api/article/1 PUT
# 删除文章编号为 1 的文章
/api/article/1 DELETE

php 框架在处理 uri 时会提供一个 pathinfo 模式,从服务端几个变量中获取 pathinfo,并自动解析处理 URI 中的参数。

URI 中包含版本号。这个我感觉跟 RESTful 规范并没有冲突,v2 版本的资源和 v1 版本的资源可能在 “状态转化” 方面是有所不同的。从这个角度考虑,可以认为这是两种不同的服务。且添加了版本号的 URI 比较直观。如果觉得版本号冗余也可以将其添加到 header 的 accept 中。

我在处理多个版本的 API 时,小版本的修改直接做代码兼容,不去定义版本号;大的变动则直接做一个新的 git 版本分支,然后去解析一个新的二级域名,指向新的 API,客户端只需要修改请求 API 地址即可。

简单的无框架 RESTful API 实现

使用的是菜鸟教程中的 PHP RESTful,测试时发现一些小的 bug,做了一点改动。

主要有这些文件:RestController.phpSimpleRest.phpSiteSiteRestHandler.php 以及 .htaccess

因为是无框架,所以需要改写请求 URI。

.htaccess

RewriteEngine On

# URI rewrite
RewriteRule ^site/list/?$   /api/restful/RestController.php?view=all [nc,qsa]
RewriteRule ^site/list/([0-9]+)/?$   /api/restful/RestController.php?view=single&id=$1 [nc,qsa]

.htaccess 一般只作用于 Apache 服务器。

控制器 RestController.php

require_once("SiteRestHandler.php");

$view = "";
if (isset($_GET["view"]))
    $view = $_GET["view"];

/**
 * RESTful service 控制器
 * URL 映射
 */
switch ($view) {
    case "all":
        // 处理 REST Url /site/list/
        $siteRestHandler = new SiteRestHandler();
        $siteRestHandler->getAllSites();
        break;
    case "single":
        // 处理 REST Url /site/show/<id>/
        $siteRestHandler = new SiteRestHandler();
        $siteRestHandler->getSite($_GET["id"]);
        break;
    case "":
        // 404 - not found
        break;
    default:
        break;
}

逻辑处理层 SiteRestHandler.php

require_once("SimpleRest.php");
require_once("Site.php");

class SiteRestHandler extends SimpleRest {

    function getAllSites() {

        $site = new Site();
        $rawData = $site->getAllSite();

        if(empty($rawData)) {
            $statusCode = 404;
            $rawData = array('error' => 'No sites found!');
        } else {
            $statusCode = 200;
        }

        $contentType = $this->getContentType($_SERVER['HTTP_ACCEPT']);
        $this -> setHttpHeaders($contentType, $statusCode);

        if(strpos($contentType,'application/json') !== false){
            $response = $this->encodeJson($rawData);
            echo $response;
        } else if(strpos($contentType,'text/html') !== false){
            $response = $this->encodeHtml($rawData);
            echo $response;
        } else if(strpos($contentType,'application/xml') !== false){
            $response = $this->encodeXml($rawData);
            echo $response;
        }
    }

    public function encodeHtml($responseData) {

        $htmlResponse = "<table border='1'>";
        foreach($responseData as $key=>$value) {
            $htmlResponse .= "<tr><td>". $key. "</td><td>". $value. "</td></tr>";
        }
        $htmlResponse .= "</table>";
        return $htmlResponse;
    }

    public function encodeJson($responseData) {
        $jsonResponse = json_encode($responseData);
        return $jsonResponse;
    }

    public function encodeXml($responseData) {
        // 创建 SimpleXMLElement 对象
        $xml = new SimpleXMLElement('<?xml version="1.0"?><site></site>');
        foreach($responseData as $key=>$value) {
            $xml->addChild($key, $value);
        }
        return $xml->asXML();
    }

    public function getSite($id) {

        $site = new Site();
        $rawData = $site->getSite($id);

        if(empty($rawData)) {
            $statusCode = 404;
            $rawData = array('error' => 'No sites found!');
        } else {
            $statusCode = 200;
        }

        $contentType = $this->getContentType($_SERVER['HTTP_ACCEPT']);
        $this -> setHttpHeaders($contentType, $statusCode);

        if(strpos($contentType,'application/json') !== false){
            $response = $this->encodeJson($rawData);
            echo $response;
        } else if(strpos($contentType,'text/html') !== false){
            $response = $this->encodeHtml($rawData);
            echo $response;
        } else if(strpos($contentType,'application/xml') !== false){
            $response = $this->encodeXml($rawData);
            echo $response;
        }
    }
}

逻辑处理层基础类 SimpleRest.php

class SimpleRest {

    private $httpVersion = "HTTP/1.1";

    public function setHttpHeaders($contentType, $statusCode) {

        $statusMessage = $this->getHttpStatusMessage($statusCode);

        header($this->httpVersion . " " . $statusCode . " " . $statusMessage);
        header("Content-Type:" . $contentType);
    }

    public function getContentType($requestContentType, $selectedContentType = '') {
        $contentTypes = array('application/json','text/html','application/xml');
        if ($selectedContentType && in_array($selectedContentType, $contentTypes)) return $selectedContentType;
        $defaultContentType = $contentTypes[0];
        foreach ($contentTypes as $contentType) {
            if (strpos($requestContentType, $contentType) !== false) {
                $defaultContentType = $contentType;
                break;
            }
        }
        return $defaultContentType;
    }

    public function getHttpStatusMessage($statusCode){
        $httpStatus = array(
            100 => 'Continue',
            101 => 'Switching Protocols',
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            306 => '(Unused)',
            307 => 'Temporary Redirect',
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Timeout',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Long',
            415 => 'Unsupported Media Type',
            416 => 'Requested Range Not Satisfiable',
            417 => 'Expectation Failed',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout',
            505 => 'HTTP Version Not Supported'
        );
        return isset($httpStatus[$statusCode]) ? $httpStatus[$statusCode] : $httpStatus[500];
    }
}

数据层 - 模型 site.php

/*
 * 菜鸟教程 RESTful 演示实例
 * RESTful 服务类
 */
class Site {

    private $sites = array(
        1 => 'TaoBao',
        2 => 'Google',
        3 => 'Runoob',
        4 => 'Baidu',
        5 => 'Weibo',
        6 => 'Sina',
        7 => '海滨擎蟹'
    );

    public function getAllSite(){
        return $this->sites;
    }

    public function getSite($id) {

        return isset($this->sites[$id]) ? array($id => $this->sites[$id]) : array();
    }
}

因为展示内容比较简单,且 API 一般只返回数据,所以不需要视图层。

框架一般不需要通过改写去设置路由,而是有独立的路由设置,tp 中路由定义 可以单条注册,亦可以数组形式批量注册路由规则,yii2、Laravel 都有类似的用法,其中 Laravel 甚至可以 Route::resource() 定义所有的 RESTful 规范的路由。