在框架盛行的今天,MVC 也不再是神話。 經(jīng)常聽到很多程序員討論哪個框架好,哪個框架不好, 其實(shí) 框架只是工具,沒有好與不好,只有適合與不適合,適合自己的就是最好的。
每次我面試應(yīng)屆生時(shí)都會問他使用過什么框架,并談?wù)剬@些框架的理解。 當(dāng)面試有經(jīng)驗(yàn)的程序員時(shí),會讓他自己寫一個框架出來。 其實(shí)也不是讓他編碼,只要有思路就 OK 了。 我覺得,如果一個有一年經(jīng)驗(yàn)的程序員連一個 Framework v0.0.1 都開發(fā)不出來的話,肯定是沒有深入理解一個框架。
前幾天 @phoenixg 說要自己寫個 MVC 框架。 而且他也確實(shí)不僅僅是說說而已,短短一個周末,這個框架雛形就神奇的出現(xiàn)在了 github 上。
這篇博文的名字是『自己動手設(shè)計(jì) PHP MVC框架』, 所以本文不會涉及太多的編碼,文中出現(xiàn)的任何代碼片段都是我直接在 vim 里面敲的, 沒做任何測試,如果想使用文中代碼需自行測試。
跟隨本教程,將從零開始設(shè)計(jì)一個屬于自己的 MVC 框架。
我使用過 ZendFramwork、CodeIgniter,每個框架都有自己的優(yōu)點(diǎn)和不足。 在寫本文之前,我又看了 Symfony、cakephp、MooPHP、doitphp 等的核心源碼, 下面說說我將把我的框架設(shè)計(jì)成什么樣子,這一章主要討論 URL 的設(shè)計(jì)。
1. REST
在這個 REST 橫行的時(shí)代,如果一個框架不支持 REST,肯定被前衛(wèi)程序員所瞧不起,所以本框架也要支持 REST。
第一個設(shè)計(jì)準(zhǔn)則: 所有東西都是資源,資源有多種表現(xiàn)形式。
不管實(shí)際上存在的,還是抽象上的, 所有資源都會有一個不變的標(biāo)識(ID),對資源的任何 API 操作都不應(yīng)該改變資源的標(biāo)識。
事實(shí)上,上面的這些完完全全是按照互聯(lián)網(wǎng)的特性提出來的。
互聯(lián)網(wǎng)中,一個 URL 就是一個資源;
資源的內(nèi)容就是 HTML 頁面;
不管怎么改 HTML 內(nèi)容,URL 都不會改變;
資源之間通過 HTML 里的連接聯(lián)系起來;
每次獲取的時(shí)候,獲取到的都是完整的 HTML 內(nèi)容。
比如
GET http://niutuku.com/users // 所有用戶
GET http://niutuku.com/users/phper // 標(biāo)識為phper的用戶
2. 擴(kuò)展名
在此我不討論擴(kuò)展名和文件類型之間的關(guān)系,以及“擴(kuò)展名只是約定,而文件類型記錄在文件頭”。
我通常把擴(kuò)展名理解為“約定”,而不是文件類型。 當(dāng)我們請求一個 news.html 時(shí),我們并不能確信它就是一個存在于服務(wù)器上的news.html文件, 它也可能是php文件,也可能是jsp文件,在nodejs流行的今天,它也可能是一個js文件。 但不管頁面是如何生成的,有一點(diǎn)是明確的——最終我們得到了一個html文檔。
雖然rest不要求使用擴(kuò)展名,但有人告訴我,如果在一個女生名字后面加一個.rmvb 的擴(kuò)展名,將變得非?!虼吮究蚣軐⒅С?jǐn)U展名,但是擴(kuò)展名并是資源的一部分。
什么意思呢?
還是前面的例子,所有用戶這個資源該如何表示呢? 用 url http://niutuku.com/users 就可以唯一標(biāo)識, 而 擴(kuò)展名可以用來標(biāo)識資源的不同表現(xiàn)形式。
a、當(dāng)我們請求 http://niutuku.com/users 時(shí),框架將返回一個html文檔, 數(shù)據(jù)可能在表格中,也可能在 form 中,也可能在 div 中(如下圖)。
b、當(dāng)我們請求 http://niutuku.com/user.json 時(shí),將返回 json 格式數(shù)據(jù)。
[
{
"firstName" : "just",
"lastName" : "javac",
"userName" : "@niutuku"
},
{
"firstName" : "Tom",
"lastName" : "Cat",
"userName" : "@tomcat"
},
……
]
c、當(dāng)我們請求 http://niutuku.com/user.xml 時(shí), 將返回 xml 格式的數(shù)據(jù),xml 文檔可由 DTD 或者 XSD 定義。
d、如果我們想把所有用戶的列表發(fā)給管理員,或者打印出來呢?
可以直接訪問 http://niutuku.com/user.xls,框架將會返回 Excel 電子表格。 當(dāng)我們高高興興把文件下載下來,卻發(fā)現(xiàn)電腦沒有安裝 Excel,怎么辦? 沒關(guān)系,我們還可以訪問http://niutuku.com/user.jpg,畢竟看圖工具我們還是有的。
用過 Google 短網(wǎng)址服務(wù)的同學(xué)都知道, 比如我的網(wǎng)站 http://niutuku.com 的短網(wǎng)址是http://goo.gl/JMQJ8, Google 還提供了二維碼表示法,只需要在后面添加 .qr 例如http://goo.gl/JMQJ8.qr。
taourl 也提供了一個很方便的功能,例如 我們想查看網(wǎng)址 http://taourl.com/7c1ug 的訪問情況,那么只需要在網(wǎng)址最后面添加一個+號就可以了。
總之,不管用了什么擴(kuò)展名,將返回同一個資源,只是表現(xiàn)形式不同罷了。 這也就是經(jīng)常所說的 數(shù)據(jù) + 模板 = 輸出。
如果沒有擴(kuò)展名呢?返回 HTML 文檔?
別忘了 http 請求的 Accept。 設(shè)置請求頭的 Accept: application/x-excel 我們依然可以得到一個電子表格。
甚至當(dāng)我們訪問某個用戶時(shí), http://niutuku.com/user/niutuku,我們可以使用 Accept: text/x-vcard,如果不知道嘛意思,自己Google去。
下面說說設(shè)計(jì)模式,在這個功能上,可以用一個適配器模式,根據(jù)不同的擴(kuò)展名選擇不同的適配器,執(zhí)行不同的功能,最后提供相同的接口,具體實(shí)現(xiàn)就不多說了。
3. 多語言支持
@TODO 多語言支持的 url 結(jié)構(gòu)設(shè)計(jì)
4. 充分利用 HTTP
和請求有關(guān)的錯誤和其他重要的狀態(tài)信息怎么辦呢?
簡單,使用 HTTP 的狀態(tài)碼! 通過使用 HTTP 狀態(tài)碼,你不需要為你的接口想出 error/success 規(guī)則,它已經(jīng)為你做好。
比如:假如一個消費(fèi)者提交數(shù)據(jù)(POST)到 /api/users,
你需要返回一個成功創(chuàng)建的消息,此時(shí)你可以簡單的發(fā)送一個 201 狀態(tài)碼(201=Created)。
如果失敗了,服務(wù)器端失敗就發(fā)送一個 500(500=內(nèi)部服務(wù)器錯誤),
如果請求中斷就發(fā)送一個 400(400=錯誤請求)。
也許他們會嘗試向一個不接受 POST 請求的接口提交數(shù)據(jù),你就可以發(fā)送一個 501 錯誤(未執(zhí)行)。
又或者你的 MySQL 服務(wù)器掛了,接口也會臨時(shí)性的中斷,發(fā)送一個503錯誤(服務(wù)不可用)。
幸運(yùn)的是,你已經(jīng)知道了這些,假如你想要了解更多關(guān)于狀態(tài)碼的資料,可以在維基百科上查找。
HTTP 支持客戶端緩存,在HTTP響應(yīng)里利用 Cache-Control,Expires,Last-Modified 三個頭字段, 我們可以讓瀏覽器緩存資源一段時(shí)間。
REST 也可以利用這些頭,告訴客戶端在一定時(shí)間內(nèi)不需要再次請求資源。 這對提高性能有很大好處。Expires、Last-Modified 以及 ETag 可以通過資源的屬性提供,這個在有關(guān) Model 層的設(shè)計(jì)中再詳細(xì)介紹。
5. 測試與調(diào)試
PHP 的靈活使得自動化測試或者 TDD 變得困難,至少和 Java 比就差了好大一截。 在框架中,將很自由的開啟調(diào)試,比如我的設(shè)計(jì)是通過添加 url 參數(shù):
http://niutuku.com/user/niutuku?DEBUG=2
通過添加 DEBUG 參數(shù)告訴框架開啟調(diào)試模式,后面的參數(shù)值是調(diào)試的級別 level。 類似的,你也可以加入 LOG 參數(shù)來啟動日志。
這樣設(shè)計(jì)還有一個好處就是,不需要修改配置文件,而且還可以 針對某一個頁面來開啟或者關(guān)閉。 當(dāng)我用 CI 時(shí),每次我發(fā)現(xiàn)程序中的問題,都在配置文件中將 log 級別設(shè)置為 all, 再重新打開頁面,當(dāng)我再看 log 文件時(shí),居然已經(jīng)幾百行了,因?yàn)槲以L問的每個頁面都被記錄到了日志里面。
測試和 url 好像沒有多大關(guān)系,測試放在單獨(dú)的章節(jié)討論。 我為測試約定的 url 是添加 test,比如為控制器 niutuku.controller.php 寫的測試用例(Test Case)可以通過http://niutuku.com/test/user/niutuku 訪問。
但我還是比較喜歡在命令行測試,畢竟當(dāng)你手動點(diǎn)擊瀏覽器,并手動輸入 url, 手動敲回車鍵時(shí),已經(jīng)違背了自動化測試。
6. Ajax
@TODO 應(yīng)用于單頁 Ajax 的 url 結(jié)構(gòu)設(shè)計(jì)