神啊,求你賜給我平靜的心,去接受我無法改變的事;賜給我勇氣,去做我能改變的事;賜給我智慧,去分辨兩者的不同。 --平靜之禱
追到一個心儀的女生不難,難于如何保持和培養(yǎng)一份真摯的感情;獲得一時的財富也不難,難于如何長久保持收益;創(chuàng)業(yè)的公司很容易博得一時媒體的關(guān)注以及某次天使的投資,但難于如何排除各種障礙、充分利用各方資源發(fā)展成中企業(yè)及至上市公司。
同樣,提供一時的接口很容易,但當我們需要不斷為接口提供升級,以及當我們維護提供一整套接口時,面臨的困難和問題會越來越大。
所以,這是一場持久的戰(zhàn)役。需要我們用穩(wěn)重的心態(tài)、專業(yè)的能力在背后持久支撐、推動。
值得慶幸的是,這些都是問題而不是限制,都是可以被解決的。
以下是結(jié)合 @郭了個浩浩 同學提供的apigee.web_api.pdf文檔,以及我們多年來的項目實際開發(fā)經(jīng)驗為新手提供的一些建議,對老同學相信也會有所幫助。
每個建議通常會包括三部分: 現(xiàn)在主流的做法、PhalApi的做法以及項目的選取。
為了大家查閱和翻看,這里先羅列本章的全部建議:
目前,后臺接口開發(fā)可以用RESTFull風格,也可以用Web Service;可以用SOAP協(xié)議、RPC協(xié)議,也可以用HTTP協(xié)議;可以用短鏈接,也可以使用長鏈接。如果我們希望繼續(xù)進行劃分,還可以分為同步或異步、單個或批量、是否有SDK包、內(nèi)部接口還是開放接口平臺等。
現(xiàn)在看來,大部分大型的企業(yè)以及大多數(shù)的小公司使用的都是HTTP協(xié)議下的接口開發(fā),部分使用RESTFull,但Web Service較少。如:
我們選取了HTTP的協(xié)議,在于其無論是客戶端接入、開發(fā)調(diào)試,還是部署構(gòu)建上都很容易實現(xiàn),而且也符合主流,因為大家都比較熟悉。
這一點是非常重要的:因為簡單,后臺接口開發(fā)的同學才會更容易上手;因為容易,客戶端接入才會更加無壓力而不用擔心處處受挫。
根據(jù)項目不同的項目背景和需求,可以選擇你合適的風格或者協(xié)議。但是即使出于安全、性能或者其他技術(shù)或非技術(shù)的原因而不采用HTTP協(xié)議的情況下,你也可以在PhalApi原有的接口開發(fā)實現(xiàn)時,輕松擴展你需要的協(xié)議。如使用SOAP,PHPRpc或者swoole下的TCP協(xié)議。其中,部分協(xié)議已有擴展類庫提供支持。
首先,有一點是可以肯定的。
接口系統(tǒng)應(yīng)該有自己單獨的域名,而不應(yīng)該附屬于網(wǎng)站或者管理后臺。
顯然,主流做法也是這樣做的。如:
如果可以,盡量讓接口系統(tǒng)使用獨立的域名,并且使用api作為一級域名。如:
//你的網(wǎng)站為:
http://www.demo.com
//則對應(yīng)的接口為:
http://api.demo.com
對于接口的異常處理,在使用HTTP協(xié)議下,可以通過HTTP本身的響應(yīng)狀態(tài)碼來進行區(qū)分。在非HTTP協(xié)議并有SDK包的情況下,異常的處理手段則會更為多樣。
優(yōu)酷接口采用了HTTP響應(yīng)狀態(tài)碼加結(jié)果返回的形式,如:
Request URL:https://openapi.youku.com/v2/videos/show_basic.json
Request Method:GET
Status Code:400 Bad Request
{"error":{"code":1004,"type":"SystemException","description":"Client id null"}}
新浪微博也一樣:
Request URL:https://api.weibo.com/2/statuses/mentions/ids.json
Request Method:GET
Status Code:403 Forbidden
{"error":"auth by Null spi!","error_code":21301,"request":"/2/statuses/mentions/ids.json"}
微信接口則采用了統(tǒng)一200的形式,如:
Request URL:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=
Request Method:GET
Status Code:200 OK
{"errcode":41002,"errmsg":"appid missing"}
為了與HTTP保持一致性,同時降低不必要的復(fù)雜性,我們采用了200/400/500三大接口結(jié)果狀態(tài)碼。
注意,這里所說的三大狀態(tài)碼,是指接口返回結(jié)果中的狀態(tài)碼,而不是HTTP的響應(yīng)狀態(tài)。
也就是說接口全部的結(jié)果返回都應(yīng)該是200,除非接口服務(wù)有內(nèi)部未捕獲的異常,即:
Status Code:200 OK
返回結(jié)果狀態(tài)碼剛是以下幾種:
//正常返回
{
"ret": 200,
"data": {
//...
},
"msg": ""
}
//客戶端非法請求
{
"ret": 400,
"data": [],
"msg": "非法請求:接口服務(wù)Default.Test不存在"
}
//服務(wù)端內(nèi)部錯誤
{
"ret": 500,
"data": [],
"msg": "服務(wù)器運行錯誤: can not connect to database db_demo"
}
####項目的選取
你可以根據(jù)你的需要,擴展400和500這兩系列的錯誤,如401表示登錄失敗等。
此外,在data里面,你也可以添加一個code來表示業(yè)務(wù)級的操作碼,以及客戶端根據(jù)不同的業(yè)務(wù)場景做出不同和反應(yīng)、交互或引導(dǎo)提示。
對外的命名,是指外部看得到的命名,如接口參數(shù)的名字,接口返回的結(jié)果節(jié)點名字,以及數(shù)據(jù)庫的表名、字段名。
新浪微博采用了小寫加下劃線的做法,如:
//URL
https://c.api.weibo.com/2/friendships/followers/trend_count.json
//請求參數(shù)
source
access_token
//返回結(jié)果
{
"uid": 10438,
"result": [
{
"days": "2012-04-04",
"follower_count_online":"15", //粉絲數(shù)
"active_follower":"14", //活躍粉絲數(shù)
"loyal_follower":"0" //互動粉絲數(shù)
},
....
]
}
Amazon采用了首字母大寫且無下劃線的做法,如:
//Responses
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3
<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://doc.s3.amazonaws.com/2006-03-01">
<LoggingEnabled>
<TargetBucket>mybucketlogs</TargetBucket>
<TargetPrefix>mybucket-access_log-/</TargetPrefix>
<TargetGrants>
<Grant>
<Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="AmazonCustomerByEmail">
<EmailAddress>user@company.com</EmailAddress>
</Grantee>
<Permission>READ</Permission>
</Grant>
</TargetGrants>
</LoggingEnabled>
</BucketLoggingStatus>
我們提倡使用全部小寫加下劃線的命名,因為這樣更符合客戶端的使用,如:接口參數(shù):
//正確的
&user_id=888
//錯誤的
&userId=888
返回字段:
//正確的
"device_type": "cube",
//錯誤的
"deviceType": "cube",
數(shù)據(jù)庫字段:
//正確的
`user_id` bigint(20) DEFAULT '0' COMMENT '創(chuàng)建者的用戶ID',
//錯誤的
`userId` bigint(20) DEFAULT '0' COMMENT '創(chuàng)建者的用戶ID',
不管是使用全部小寫,還是全部大寫,項目都應(yīng)該保持一致的命名風格,而不是混合凌亂的風格。
與對外命名對應(yīng)的則是對內(nèi)的命名規(guī)則,這里又回歸到了老生常談的PHP代碼風格。
這里不作過多的說明,只是稍作提及。
我們建議使用PEAR包的命名風格,和駝峰法,如下為一個接口示例:
$ vim ./Api/Default.php
<?php
/**
* 默認接口服務(wù)類
*
* @author: dogstar <chanzonghuang@gmail.com> 2014-10-04
*/
class Api_Default extends PhalApi_Api {
public function getRules() {
return array(
'index' => array(
'username' => array('name' => 'username', 'default' => 'PHPer', ),
),
);
}
public function index() {
return array(
'title' => 'Default Api',
'content' => T('Hello {name}, Welcome to use PhalApi!', array('name' => $this->username)),
'version' => PHALAPI_VERSION,
'time' => $_SERVER['REQUEST_TIME'],
);
}
}
你可以選擇你喜歡的風格,但團隊應(yīng)該保持一致。
即便你不喜歡PhalApi約定的PEAR命名,你也可以自行實現(xiàn)內(nèi)部的類加載機制。
當有多個項目或者多個模塊并存時,可以添加模塊名前綴來作區(qū)分,如:
$ tree
.
├── Demo
│ └── Api
│ └── DUser.php
├── MyApp
│ └── Api
│ └── MUser.php
└── Task
└── Api
└── TUser.php
$ head */*/*
==> Demo/Api/DUser.php <==
<?php
class Api_DUser extends PhalApi_Api {
}
==> MyApp/Api/MUser.php <==
<?php
class Api_MUser extends PhalApi_Api {
}
==> Task/Api/TUser.php <==
<?php
class Api_TUser extends PhalApi_Api {
}
其它的Domain層和Model層等也類似,這樣可以避免類名沖突,或者IDE開發(fā)環(huán)境下的混淆。
既然采用HTTP協(xié)議,那么安全方面就需要接口自身進行保證。
所幸,現(xiàn)在可用的加密手段有多種選擇。
對于接口簽名,我們可以使用非對稱的驗簽方式,如md5;也可以用對稱的方式,如RSA。
最后,為每一個接入的客戶端分配app_key和app_secrect即可。
當然,更好的安全是接口系統(tǒng)再提供登錄態(tài)的驗證,即通常所說的token。這兩者的相合,會為接口增加更好的安全保障。
我們不提供具體的接口簽名方案,是因為把這種決策移交給項目應(yīng)用本身進行定制。
而定制也是非常簡單的,只需要簡單的兩步即可:
對于token,雖然框架沒有提供內(nèi)置的實現(xiàn),但可以從PhalApi的擴展類庫尋找這種支持,這一點已經(jīng)User擴展類庫支持。
正如PhalApi提供的自由空間,項目可以自行實現(xiàn)接口簽名,和根據(jù)需要是否采用User擴展類庫,或者自行實現(xiàn)token的處理。
在 [1.14.1 統(tǒng)一返回的格式]一節(jié)中,已經(jīng)對JSON的返回格式作了說明,這里不再贅述,也只是稍作提及。
目前采用了JSON的格式返回的有:
采用了XML格式返回的有:
我們默認采了JSON的格式返回。
項目可以輕松擴展成其他格式的返回。
先從項目內(nèi)部的文件劃分說起,通常最為常見的情況是,很多開發(fā)人員都喜歡把很多很多很多接口都塞到一個接口文件里面。
這樣的文件,通常會有2K到3K左右。
我覺得這是一種極端,而且是一種不好的極端。因為文件過大的話,會帶來很多問題。
但與之對立的有另一種做法,即一個文件,一個接口。
這一點,在我之前就職的一家出名的游戲公司中得到了廣泛的認可和遵循。如:
//?service=UserInfo.Go
<?php
class Api_UserInfo extends PhalApi_Api {
public function go() {
//TODO
}
}
//?service=GroupInfo.Go
<?php
class Api_GroupInfo extends PhalApi_Api {
public function go() {
//TODO
}
}
雖然也是一種極端,但卻很好地做到了接口隔離,即不用擔心修改此接口的實現(xiàn)而影響到其他接口服務(wù)。
最后,我們再來聊URL規(guī)則,就更順暢了。如果我們采用一個文件對應(yīng)一個接口,則我們可以省略Action(全部都為go()方法),簡寫成:?service=XXX。
再進一步,我們可以利用接口服務(wù)器(如Nginx)的規(guī)則Rewrite來提供更好的URL規(guī)則,同時盡量隱藏我們的接口內(nèi)部實現(xiàn)細節(jié),如:
//原始地
http://api.demo.com/?service=UserInfo.Go
//簡化地
http://api.demo.com/?service=UserInfo
//再進一步
http://api.demo.com/UserInfo
//或者
http://api.demo.com/UserInfo.json
還有一點需要關(guān)注的就是接口的版本,當有v1,v2,v3等不同的版本時,我們也需要在接口URL中體現(xiàn)這些版本的不同。
目前而言,PhalApi在URL規(guī)則和路由這塊還比較欠缺,沒有像其他網(wǎng)站一樣提供強大的路由支持。
但我們在代碼實現(xiàn)的層面,可以提供不同的入口,以開放給不同的終端(內(nèi)部的或者外部的), 以及不同的版本支持。如:
$ tree Public/
Public/
├── v1
│ └── index.php
├── v2
│ └── index.php
└── v3
└── index.php
3 directories, 3 files
則對應(yīng)的版本URL則可以為:
//v1版本
http://api.demo.com/v1/?service=Default.Index
//v2版本
http://api.demo.com/v2/?service=Default.Index
//v3版本
http://api.demo.com/v3/?service=Default.Index
項目可以結(jié)合不同的入口,以及接口服務(wù)器的URL規(guī)則Rewrite作一些自定的URL路由。
目前移動開發(fā)主要有iOS、Android、Windowns Phone、網(wǎng)站等不同的終端,各種終端又有不同的語言,如果我們需要提供SDK包,不僅僅需要考慮到縱向的版本升級,還需要維護橫向的多樣性。
而且,如果我們使用的是HTTP協(xié)議,則不必要擔心這些維護的成本,同時給客戶端提供一個自由的空間進行調(diào)用 -- 即客戶端可以自己編寫本身的接口客戶端。
很多國內(nèi)的開放平臺接口都是不提供SDK包的,但有些安全度高的則會,如支持寶。
以下是一些提供了SDK的平臺 :
我們暫時沒有提供SDK包,但對于PHP,有一個簡單的客戶端類,可見: [1.13]-統(tǒng)一的接口請求方式
出于公司產(chǎn)品簇的項目考慮,項目可以內(nèi)部提供SDK給同類的客戶端使用,如分為iOS版的客戶端SDK,以及Android版的客戶端SDK。
有一點是非常重要的,千萬不要讓不懂PHP語言的人去開發(fā)提供PHP的SDK包,更不要使用所謂的工具自動轉(zhuǎn)換生成SDK包代碼。
在我曾經(jīng)做過的一個項目中,因為需要接入一個接口系統(tǒng),而這個接口是由專業(yè)的JAVA團隊維護的,但他們對PHP語言則是非常薄弱,以致他們使用了工具來生成PHP語言的SDK包。
這就導(dǎo)致了我在接入一個簡單的接口時,卻開發(fā)聯(lián)調(diào)耗費了兩天、測試聯(lián)調(diào)時耗費了在接口調(diào)用超時問題排查上。
而最后找到的原因卻是因為app_key不對而導(dǎo)致服務(wù)端異常,而在SDK包卻隱藏了這一異常錯誤信息,反而給出了time out超時的提示,嚴重誤導(dǎo)了排查的方向!
而當我嘗試深入去調(diào)試SDK時,得到卻又是既沒有code又沒有message的異常!最讓人難以忍受的是,他們提供的SDK包竟然和JAVA的企業(yè)系統(tǒng)一樣復(fù)雜的結(jié)構(gòu)(正如他們是使用工具來生成轉(zhuǎn)換的)!
想象一下,PHP代碼下有\(zhòng)com\sina\webo\sdk\Constants.php這樣類似JAVA的文件結(jié)構(gòu),PHP的同學會作何感想?用JAVA的世界的方式來開發(fā)PHP,顯然是走不通的??!
而執(zhí)意要走的話,到最后就是各種接入的痛苦,稍微按奈不住的同學難免就會因為情緒問題而大開爭論了。而這一切,只是因為非PHP人員使用了自動生成工具。
我覺得,這是一種不負責任的做法,希望大家不要效仿。
(場外音:通過沐浴法理清了頭緒,繼續(xù)回來執(zhí)筆編寫)。
就我個人經(jīng)歷而言,markdown就是一個開始你會拒絕,接著你會越來越喜歡,到最后會愛不釋手的一個工具。
如果你或者你的團隊還在使用郵件或者work文檔來傳遞共享接口文檔,那就太不應(yīng)該了;如果你正在使用某個WIKI系統(tǒng)進行文檔的維護但卻不喜歡它的編輯或者展示方式時,你可以嘗試使用一下markdown。
正如你現(xiàn)在正在查看的文檔也是通過markdown編寫的。
作為開放接口平臺,文檔肯定是以網(wǎng)站的形式提供。但很多時候,對于我們內(nèi)部的接口或者小項目來說,顯然這樣的成本太大了。
接口,從簡單開始。
我們理應(yīng)一直堅持這一點,所以文檔也是一樣,我們應(yīng)該尋求一種在內(nèi)部快速共享最新接口文檔的途徑。如:
你可以根據(jù)項目的需要,或者公司以往的做法,但至少不要再使用郵件或者word文檔。
單元測試,在PhalApi里面不只一次提到了,這里再次進行說明,是希望能引起大家的關(guān)注,去嘗試體驗一下。
我們都知道,在開發(fā)一個新功能時、新接口時,修復(fù)一個BUG或者作一些大的調(diào)整或者重構(gòu)工作,我們是毫無壓力的,而且這時的成本很低,僅在于開發(fā)人員本身的時間和精力的消耗。
當提測后進入測試階段,測試人員發(fā)現(xiàn)一個BUG后,有些團隊會以禪道或者Bugzilla或其他方式來紀錄和追蹤BUG。這時我們開發(fā)會覺得一個這么小的問題還需要去紀錄、去登記很不值得。然后,我們應(yīng)當注意到這時修復(fù)一個BUG會涉及到測試人員資源的開銷。
當進入了回歸測試階段,特別是多系統(tǒng)交互、跨團隊合作時,一個BUG就會從一個人傳到另一個人,從這個團隊流到那個團隊,這時成本就會逐漸增大。
最后,上線后,當一個奇怪的問題出現(xiàn)后,我們需要定位原因就更加困難重重了。
我曾經(jīng)就經(jīng)歷這樣一番:有用戶發(fā)現(xiàn)游戲的道具減少了。我們一開始以為是某些運營配置、或者數(shù)據(jù)以及用戶的等級限制所引發(fā)的,但在排除了各種業(yè)務(wù)的問題后,到最后卻發(fā)現(xiàn)是PHP中array使用“+”運算而引發(fā)的血案!
在正常情況下,我們都知道array_merge()函數(shù)對于數(shù)值的下標則會追加并重新生成下標序列,即會合并;而數(shù)組+則會去掉相同下標的元素。
但實際情況下,線上BUG所產(chǎn)生的影響不在于排查和修復(fù)的時間成本,而在于在這段時間內(nèi)所損失的金額、數(shù)據(jù)等成本。
當然,從測試的角度上看,測試并不能保證我們的系統(tǒng)沒有BUG,只能說暫時未發(fā)現(xiàn)BUG。
單元測試也一樣,作為開發(fā)人員,我們應(yīng)當在最低成本的時期就及時發(fā)現(xiàn)我們直覺覺得可能會出現(xiàn)的問題并進行修復(fù)。
對我們親手所編寫的代碼負責,并且用客觀的方式來證明我們的代碼目前未發(fā)現(xiàn)問題,而不是主觀認為“我寫的代碼沒有問題”。更不應(yīng)該一次又一次地犯下各種低級或者重復(fù)的錯誤,而讓團隊其他成員對我們喪失信任。
PhalApi一直很注重單元測試,也很注重自動化,為了減輕大家重復(fù)編寫單元測試骨架代碼的痛苦,我們提供了一個可以生成單元測試代碼的腳本。
假設(shè)我們有這么一個類:
<?php
class Api_Default extends PhalApi_Api {
public function index() {
//TODO
}
}
那么,我們可以這樣生成測試代碼:
$ cd .//Demo/Tests
$ phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php
<?php
/**
* PhpUnderControl_ApiDefault_Test
*
* 針對 ../Api/Default.php Api_Default 類的PHPUnit單元測試
*
* @author: dogstar 20150514
*/
require_once dirname(__FILE__) . '/test_env.php';
if (!class_exists('Api_Default')) {
require dirname(__FILE__) . '/../Api/Default.php';
}
class PhpUnderControl_ApiDefault_Test extends PHPUnit_Framework_TestCase
{
public $apiDefault;
protected function setUp()
{
parent::setUp();
$this->apiDefault = new Api_Default();
}
protected function tearDown()
{
}
/**
* @group testGetRules
*/
public function testGetRules()
{
$rs = $this->apiDefault->getRules();
}
/**
* @group testIndex
*/
public function testIndex()
{
$rs = $this->apiDefault->index();
}
}
溫馨提示:
- 可以先執(zhí)行:ln -s /path/to/PhalApi/phalapi-buildtest /usr/bin/phalapi-buildtest
- test_env.php為測試環(huán)境初始化文件,可以在里面引用init.php文件,并作一些調(diào)整
- 輸出的測試代碼可以重定向到./Demo/Tests/Api/Api_Default_Test.php,讓測試代碼與產(chǎn)品代碼對齊
最后,我們就可以這樣執(zhí)行單元測試了:
$ phpunit ./Api_Default_Test.php
更多建議: