最近為 YxdIocp 開源庫增加了這個輕量級的 MVC 支持。其實說是 MVC ,但由於現在還沒有配套的視圖模版引擎,所以在V上面很虛弱。不過本着做數據服務為主的思想,現在已經基本夠用了。下面來簡單介紹一下它吧。
要使用 MVC 功能非常簡單, 引用 iocp.Http.MVC 單元,或者在窗體上放入一個 TIocpHttpMvcServer 組件即可。然后就是編寫業務代碼 (可參考開源庫中 demo\IcopHttpSvrMVC 中的代碼)。iocp.Http.MVC 引入了多個屬性標注,它是 MVC 的靈魂,要用好 MVC, 必須了解這些屬性標注的功能和意義。MVC引擎會使用掃描器根據這些標注自動掃描注入,響應客戶請求。
在此之前,我們需要注意一點,在 Delphi 中, 屬性標注定義的名稱尾部如果是 "Attribute", 在使用時可以直接省略后面的 "Attribute" ,比如定義了“ServiceAttribute”標注,使用時可以只輸入 "[Service]" 即可。下面講到的屬性標注,都是省略掉了 "Attribute" 。
一、屬性標注說明
[Service]
業務層標識性標注,為指定類添加此標注后,此類才會被MVC引擎識別,從而具備業務請求處理能力。
uses iocp.Http.MVC, iocp.Http, SysUtils, Classes; type [Service] THelloMvc = class(TObject) public end;
在上面的示例代碼中,由於加入了 [Service] 標識,會被 MVC 掃描器發現該類,並自動創建一個單例用於響應業務請求。
[Controller]
控制層標注。增加此標注后,指定類才具備處理業務請求的能力。此標注和 [Service] 功能一樣,只是方便在一些情況下區分業務類別。
uses iocp.Http.MVC, iocp.Http, SysUtils, Classes; type [Controller] THelloMvc = class(TObject) public end;
[Autowired]
自動裝配標注。為一個類中的變量增加此標注后,MVC 引擎會在實例化此類時,為這個成員變量自動注入適當的值。(目前由於暫無IOC實現,只能自動注入TIocpWebSocketServer、Ser ver)
type [Controller] THelloMvc = class(TObject) protected [Autowired] FServer: TIocpHttpServer; public end;
程序運行后,會自動將 FServer 初始化為 HttpMVC.Server 。
[RequestMapping]
請求地址映射標注。此標注可用於類級別,也可用於類中的函數或方法上。
此標注有如下幾個屬性:
- Value: 指定請求的實際地址。
- Method: 指定請求的method類型, 它是 TIocpHttpMethod 類型的枚舉值,可以是 GET、POST、PUT、DELETE 等。
- Consumes: 指定處理請求的提交內容類型(Content-Type),例如application/json, text/html。
- Produces: 指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回。
- Params: 指定request中必須包含某些參數值時,才讓該方法處理。
- Headers: 指定request中必須包含某些指定的header值,才能讓該方法處理請求。
注意: 在類級別使用此標注時, Params 屬性無效。類級別主要用於設置URL的基址,以及一些默認值。比如在類級別指定 Method 為 Post,那么類中的函數或方法映射時,如果未指定 Method, 那么默認就是 Post。另外,在函數或方法被觸發時,以下類型的參數會被自動注入:
- TIocpHttpRequest: 自動注入為當前Request對象。
- TIocpHttpResponse: 自動注入會當前Response對象。
- TIocpHttpServer: 自動注入會當前Server對象。
- TIocpWebSocketServer: 啟用 WebSocket 時,自動注入當前 Server 對象。
- TIocpHttpWriter: 自動注入 Response.GetOutWriter 對象,用於直接輸出返回數據。
- TIocpHttpConnection、TIocpClientContext:自動注入當前連接對象(Request.Connection)。
- TMemoryStream 或基於 TStream: 自動注入當前請求的數據流對象(Request.Data)。
另外,如果參數名是 “RequestData”, 且為字符串類型時,自動注入 Request.DataString 。
示例:
unit Unit2; interface uses iocp.Http.MVC, iocp.Http, SysUtils, Classes; type [Controller] [RequestMapping('/hello')] THelloMvc = class(TObject) public // 映射地址:/hello/view1 // 響應所有類型的請求(Get, Post, Put 等等) [RequestMapping('/view1')] function View1(Request: TIocpHttpRequest): string; // 映射地址:/hello/view2 // 響應 Get 請求 [RequestMapping('/view2', http_GET)] function View2(Request: TIocpHttpRequest): string; // 映射地址:/hello/view3/aaa?uid=123 // 響應 Get 請求, 並且必須是包含參數uid,值為123時才響應 // 如果參數中不包括uid,或者uid的值不為123,會返回405錯誤 [RequestMapping('/view3/aaa', http_GET, 'uid=123')] function View3(Request: TIocpHttpRequest): string; // 映射地址:/hello/view4?uid=123 // 必須是 Get 請求, Content-Type必須包含application/json, // Accept必須是"*.*"或者包含“application/json” , // 必須包含參數uid=123 // 如果不符合條件會返回405錯誤 [RequestMapping('/view4', http_GET, 'application/json', 'application/json', 'uid=123')] function View4(Request: TIocpHttpRequest): string; end; implementation { THelloMvc } function THelloMvc.View1(Request: TIocpHttpRequest): string; begin Result := 'httpPostTest.html'; end; function THelloMvc.View2(Request: TIocpHttpRequest): string; begin Result := 'httpPostTest.html'; end; function THelloMvc.View3(Request: TIocpHttpRequest): string; begin Result := 'httpPostTest.html'; end; function THelloMvc.View4(Request: TIocpHttpRequest): string; begin Result := 'httpPostTest.html'; end; initialization THelloMvc.RegToMVC; end.
如果服務器Host是: http://127.0.0.1:8080,
那么,
URL: http://127.0.0.1:8080/view1
HTTP: 不限(GET, POST, PUT...)
觸發: THelloMvc.View1
URL: http://127.0.0.1:8080/view2
HTTP: GET
觸發: THelloMvc.View2
URL: http://127.0.0.1:8080/view3/aaa?uid=123
HTTP: GET
觸發: THelloMvc.View3
URL: http://127.0.0.1:8080/view3/aaa?uid=789
HTTP: GET
觸發: 無,返回405錯誤
URL: http://127.0.0.1:8080/view3
HTTP: GET
觸發: 無,返回404錯誤
URL: http://127.0.0.1:8080/view4?uid=123
HTTP: GET
觸發: 無,返回405錯誤
原因: 沒有在Http Header 的"Content-Type"屬性中添加 "application/JSON",需要使用類似下面的請求頭才可以正常訪問:
GET /hello/view4?uid=123 HTTP/1.1
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0
Content-Type: application/JSON; charset=GB2312
Accept-Encoding: gzip, deflate
Content-Length: 0
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Connection: Keep-Alive
使用上面的請求頭,會觸發 THelloMvc.View4 。
[Download]
標識請求返回頁面采用下載方式。加入此標注后,會在Http響應頭中加入文件下載標識,瀏覽會以文件下載的方式讀取數據,一般會提示用戶保存文件。
type [Controller] THelloMvc = class(TObject) public [Download] function FileDownload(Request: TIocpHttpRequest): string; end;
[WebSocket]
WebSocket 請求處理標注。當類中的一個函數或方法要用來處理 WebSocket 請求時,需要添加此標注。此標注不能與 [RequestMapping] 混用。
此標注有如下幾個屬性:
- Data: 指定僅當Data為指定內容的文本消息時才響應。
// WebSocket 請求處理,直接返回字符串內容 [WebSocket] function HelloWebSocket(): string; // WebSocket 請求處理, 只有接收到文本信息且內容是 'hello' 時才響應 [WebSocket('hello')] procedure HelloWebSocket2(Response: TIocpWebSocketResponse);
[PathVariable]
這是一個參數級的屬性標注,用來獲得請求url中的動態參數,並綁定到處理函數中指定參數上。如果參數類型與實際傳入的內容不符,會產生 500 錯誤。
此標注有如下幾個屬性:
- Name: 當方法參數名稱和需要綁定的uri template中變量名稱不一致時, 用於指定uri template的名稱。如果一致,可以省略。
URL中的動態參數,需要以 "{}" 包圍起來。
[RequestMapping('/view/{uid}/{uname}', http_GET)] procedure ViewTest1( [PathVariable('uid')] UID: Integer; [PathVariable('uname')] const UName: string; Response: TIocpHttpWriter);
示例中,{uid}、{uname} 都是url級的動態參數。通過 PathVariable 標注將它們分別綁定到了處理函數 ViewTest1 的參數 UID 和 UName 上。在觸發 ViewTest1 時,UID 的值就是 {uid} , UName 就是 {uname} 。
[RequestParam]
這是一個參數級的屬性標注。用於將請求參數區數據映射到功能處理方法的參數上。如果實際URL中參數不存在時,會將參數置為空(如數字型會是0,字符串會是空串)。如果參數類型與實際傳入的內容不符,會產生 500 錯誤。
此標注有如下幾個屬性:
- Name: 參數名稱。
// 返回一個網頁名稱, 以下載方式 // 處理 URL: /demo/view4?uid=123456 [RequestMapping('/view4', http_GET)] function ViewTest4([RequestParam('uid')] UID: Integer): string;
示例中,當 url 是 http://host/demo/view4?uid=123456 時, 觸發 ViewTest4 時,參數 UID 的值會是 123456。
[RequestBody]
這是一個函數(方法)級的屬性標注。它的作用如下:
1. 讀取Request請求的body部分數據,使用系統默認配置的Converter(轉換器)進行解析,然后把相應的數據綁定到要返回的對象上;
2. 再把Converter返回的對象數據綁定到controller中方法的參數上。
示例:
type TUserData = record UID: Integer; Age: Integer; Name: string; Nick: string; end; ...... // 提交用戶數據 // 處理 URL: /demo/person/profile/reguser [RequestMapping('/person/profile/reguser')] function RegUser([RequestBody] Data: TUserData): string;
在觸發 RegUser 函數時, 轉換器會將請求的數據注入 Data 中。在編寫轉換器時,GET請求一般需要單獨處理。
轉換器示例:
設置 HttpMvc.OnDeSerializeData 事件,在事件中進行處理:
// 反序列化處理 function TForm1.DoDeSerializeData(Sender: TObject; const Value: string; const Dest: TValue; IsGet: Boolean): Boolean; var Json: JSONObject; S: AnsiString; begin if not IsGet then begin Json := JSONObject.ParseObject(Value, False); try if Assigned(Json) then TYxdSerialize.ReadValue(Json, Dest); finally FreeAndNil(Json); end; end else begin // Get 請求單獨處理 Json := JSONObject.Create; try S := AnsiString(Value); HttpMvc.Server.DecodeParam(PAnsiChar(S), Length(S), DoReadedItem, Json); TYxdSerialize.ReadValue(Json, Dest); finally FreeAndNil(Json); end; end; Result := True; end;
在上面的反序列化操作中, Dest 實際上就是標注了 RequestBody 的響應函數中的正要注入的參數,在示例中就是 Data: TUserData。
如果有這樣的一個Post請求:
POST /demo/person/profile/reguser HTTP/1.1
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0
Content-Type: text/html; charset=GB2312
Accept-Encoding: gzip, deflate
Content-Length: 59
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Connection: Keep-Alive
{"UID":666,"Age":30,"Name":"yangyxd","Nick":"\u55B5\u55B5"}
在觸發 RegUser 時, 參數 Data 的值會是:
Name Value
Data (666, 30, 'yangyxd', '喵喵')
[ResponseBody]
這是一個函數(方法)級的屬性標注。它的作用如下:
將Controller的方法返回的對象,通過適當的Converter的Adapter轉換對象, 將內容轉換為指定格式后,寫入到Response對象的body數據區。
// 根據UID查詢用戶信息。 // 處理 URL: /demo/person/profile/123456 [RequestMapping('/person/profile/{id}', http_GET)] [ResponseBody] function Porfile([PathVariable('id')] UID: Integer): TPerson;
在觸發 Porfile 函數時, 返回值 TPerson 會被轉換器轉換序列化為指定格式,然后寫入 Response 對象, 返回給客戶端。
設置 HttpMvc.OnSerializeData 事件,在事件中進行處理:
// 序列化處理 function TForm1.DoSerializeData(Sender: TObject; const Value: TValue): string; var Json: JSONObject; begin Json := JSONObject.Create; try TYxdSerialize.WriteValue(Json, '', Value); finally Result := Json.ToString(); Json.Free; end; end;
如果在 Porfile 中的返回內容是:
function TMvcDemo.Porfile(UID: Integer): TPerson; begin Result.UID := UID; Result.Name := 'Admin'; Result.Status := 100; end;
通過使用上面的轉換器, 在觸發 Porfile 函數后,瀏覽器收到的數據會是:
{"UID":123456,"Name":"Admin","Status":100}
二、映射函數返回值
普通WEB請求:
無返回值時, 會返回客戶端 200 狀態。
返回字符串時, 會返回一個 Prefix + 返回值 + Suffix 的文件。如果文件不存在,則返回 404 錯誤。
返回整數時, 認為是錯誤代碼。比如返回 404, 則瀏覽器會收到 404 錯誤。
返回對象(Class) 或記錄(Record)時,如果標注了ResponseBody,會使用轉換器轉換后輸出給客戶端。如果沒有標注 ResponseBody, 則直接返回 200 狀態。如果返回的是對象,會自動釋放對象。
WebSocket 請求:
無返回值時, 服務器不作任何響應。
返回字符串時,會直接將字符串發送給客戶端。
返回數據時,會將數據轉為字符串后發送給客戶端。
返回對象(Class) 或記錄(Record)時,如果標注了ResponseBody,會使用轉換器轉換后輸出給客戶端。如果沒有標注 ResponseBody, 服務器不作任何響應。
三、 MVC 服務器配置
在窗口上放置 TIocpHttpMvcServer 組件時, 通過組件屬性面板進行設置。
不使用 TIocpHttpMvcServer 組件時, 引用 iocp.Http.Mvc , 會自動開啟 MVC 服務。此時會自動加載配置文件 "http_mvc_setting.xml"。
配置文件應當放於服務程序相同目錄中,文件名為 "http_mvc_setting.xml"。
配置文件示例:
<?xml version="1.0" encoding="UTF-8"?> <xml> <ListenPort>8081</ListenPort> <Active>true</Active> <Charset>UTF-8</Charset> <UseWebSocket>false</UseWebSocket> <ContentLanguage/> <WebPath>.\Web\</WebPath> <GzipFileTypes>.htm;.html;.css;.js;.txt;.xml;.csv;.ics;.sgml;.c;.h;.pas;.cpp;.java;</GzipFileTypes> <AutoDecodePostParams>true</AutoDecodePostParams> <UploadMaxDataSize>2097152</UploadMaxDataSize> <AccessControlAllow> <Enabled>false</Enabled> <Origin>*</Origin> <Methods>POST, GET, OPTIONS</Methods> <Headers>X-Requested-With, Content-Type</Headers> </AccessControlAllow> </xml>
配置文件說明:
ListenPort: 服務器監聽端口
Active: 是否在程序運行后自動開始服務
Charset: 默認字符集
UseWebSocket: 是否使用WebSocket服務
ContentLanguage: 默認內容語言
WebPath: WEB服務文件根目錄
Prefix: 返回視圖的前綴名稱,默認為空
Suffix: 返回視圖的后綴名稱,默認為空
UriCaseSensitive: URI是否大小寫敏感
BindAddr: 服務器端口綁定地址,默認為“0.0.0.0”
GzipFileTypes: 自動使用gzip壓縮數據的文件類型
AutoDecodePostParams: 自動解析Post方式傳入的參數
UploadMaxDataSize: 上傳文件的最大大小(字節)。
AccessControlAllow: WEB請求跨域控制選項。
Enabled: 是否啟用跨域控制
Origin: 允許哪些url可以跨域請求到本域
Methods: 允許的請求方法
Headers: 允許哪些請求頭可以跨域