Perfect
說(shuō)明
本文Demo是使用 Swift3.0 基于perfect框架用swift寫服務(wù)端的文章。對(duì)于perfect框架入門和配置方面不做過(guò)多的講解,想要了解該方面的大佬們請(qǐng)參考以下學(xué)習(xí)資料,而客戶端方面則使用RxSwift框架來(lái)寫,關(guān)于客戶端的內(nèi)容在有機(jī)會(huì)再進(jìn)行介紹,本文著重講解服務(wù)端Demo。
首先獻(xiàn)上本文Demo
GitHub:服務(wù)端Demo(Perfect)
GitHub:客戶端Demo(RxSwift+Moya)
接著再獻(xiàn)上參考的學(xué)習(xí)資料
GitHub地址,過(guò)萬(wàn)的start
官方中文文檔,教練我要學(xué)這個(gè)
perfect框架入門比較不錯(cuò)文章,配置方面講得很詳細(xì)
本文實(shí)戰(zhàn)Demo主要的參考來(lái)源,對(duì)官方文檔有一些重要的補(bǔ)充說(shuō)明,入門講解很詳細(xì)
Perfect是什么東西呢?
Perfect是一組完整、強(qiáng)大的工具箱、軟件框架體系和Web應(yīng)用服務(wù)器,可以在Linux、iOS和macOS (OS X)上使用。該軟件體系為Swift工程師量身定制了一整套用于開發(fā)輕量、易維護(hù)、規(guī)??蓴U(kuò)展的Web應(yīng)用及其它REST服務(wù)的解決方案,這樣Swift工程師就可以實(shí)現(xiàn)同時(shí)在服務(wù)器和客戶端上采用同一種語(yǔ)言開發(fā)軟件項(xiàng)目。
性能對(duì)比
一篇性能對(duì)比的文章:不服跑個(gè)分
至于是什么原因讓我想學(xué)習(xí)perfect呢?
作為一名剛接觸ios開發(fā)沒(méi)多久的小白,回憶起當(dāng)初加入學(xué)校一個(gè)軟件開發(fā)團(tuán)隊(duì)的時(shí)候,為了能與團(tuán)隊(duì)其他方面的人相互協(xié)作,了解其他方面的一些基本知識(shí)是有必要的。就好比前后端交互,作為移動(dòng)端方面也要了解后端知識(shí),這樣在前后端交互的時(shí)候就會(huì)少很多麻煩事。于是在加入團(tuán)隊(duì)初期,師兄便要求我自己寫一個(gè)demo,服務(wù)端你用什么寫都可以。對(duì)于當(dāng)時(shí)的我來(lái)說(shuō),真的是件麻煩事了,因?yàn)閷W(xué)習(xí)ios并不像學(xué)習(xí)Android,Android使用java語(yǔ)言,Android與java服務(wù)器相互協(xié)作,所以在學(xué)習(xí)Android的同時(shí)或多或少也會(huì)學(xué)到一些后端的知識(shí)。??雖然最后我用python寫了一個(gè)很爛的后端,但是那時(shí)候便在想為啥不能寫Android那樣,能用同一種語(yǔ)言也寫后端。直到前些日子發(fā)現(xiàn)了perfect,倍感歡喜,于是便琢磨了一番。接下來(lái)實(shí)戰(zhàn)Demo! GO! GO! GO!
第一部分:Demo演示
由于簡(jiǎn)書限制了圖片的大小,所以只能分開進(jìn)行演示。(內(nèi)心的憂傷你們應(yīng)該懂吧)
注冊(cè):
登錄:
添加筆記:
修改筆記:
刪除筆記:
數(shù)據(jù)庫(kù)中直接操作:
第二部分:初始化項(xiàng)目結(jié)構(gòu)
首先讓我們按部就班的完成初始化工作,我們的工程名就叫iNoteServer好了,所以我們創(chuàng)建一個(gè)iNoteServer,在iNoteServer里我們使用終端創(chuàng)建Package.swfit文件和一個(gè)Sources文件夾,在Sources文件夾里創(chuàng)建一個(gè)main.swift文件。你的項(xiàng)目結(jié)構(gòu)在iNoteServer文件里看起來(lái)是這樣的:
緊接著在Package.swfit文件中,寫入需要使用的倉(cāng)庫(kù)。
import PackageDescription
let versions = Version(0,0,0)..<Version(10,0,0)
let urls = [
"https://github.com/PerfectlySoft/Perfect-HTTPServer.git", //服務(wù)端核心框架
"https://github.com/SwiftORM/MySQL-StORM.git", //對(duì)象關(guān)系型數(shù)據(jù)庫(kù)
]
let package = Package(
name: "iNoteServer",
targets: [],
dependencies: urls.map { .Package(url: $0, versions: versions) }
)
然后我們?cè)诮K端中輸入swift build。(該過(guò)程等待的時(shí)間挺久的,畢竟網(wǎng)速慢,文件也不小...)fetch完成之后,輸入swift package generate-xcodeproj命令創(chuàng)建iNoteServer.xcodeproj文件。打開iNoteServer.xcodeproj文件,在Build Settings中Library Search Paths檢索項(xiàng)目軟件庫(kù)中增加(不單單是編譯目標(biāo))
$(PROJECT_DIR) - Recursive
最后,我們劃分一下目錄:
DataBase目錄里存放含管理數(shù)據(jù)庫(kù)的類(DatabaseManager
),一些ORM對(duì)象的數(shù)據(jù)模型(User
對(duì)象,NoteContent
對(duì)象)
NetworkServer目錄里存放接口API(iNoteAIP
)以及HTTPServer類(NetworkServerManager
)
因此在main文件中,我們只要簡(jiǎn)單的通過(guò)HTTPServer類來(lái)調(diào)用start方法就可以直接啟動(dòng)服務(wù)器了
NetworkServerManager.share.serverStart()
至此,我們項(xiàng)目的初始化已經(jīng)完成了,可喜可賀,可喜可賀。
第三部分:創(chuàng)建各功能模塊的接口
在這里,我創(chuàng)建一個(gè)iNoteAPI文件用來(lái)管理各模塊的接口
enum iNoteAIP: String {
case base = "/iNote"
case register = "/register" //注冊(cè)頁(yè)面
case login = "/login" //登錄頁(yè)面
case contentList = "/contentList" //獲取筆記列表
case addNote = "/addNote" //添加筆記
case deleteNote = "/deleteNote" //刪除筆記
case modifyNote = "/modifyNote" //修改筆記
}
第四部:創(chuàng)建HTTP服務(wù)器管理類
在此之前我還是先提一下使用perfect框架構(gòu)建服務(wù)器的基本流程,詳細(xì)的還是請(qǐng)看學(xué)習(xí)參考資料。
在main文件中直接寫入以下代碼:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
// 創(chuàng)建HTTP服務(wù)器
let server = HTTPServer()
// 監(jiān)聽(tīng)8181端口
server.serverPort = 8181
//創(chuàng)建路由組,用來(lái)存放各個(gè)路由
var routes = Routes()
//注冊(cè)您自己的路由和請(qǐng)求/響應(yīng)句柄 (請(qǐng)求方法,地址,請(qǐng)求處理)
routes.add(method: .get, uri: "test") { (request, response) in
response.setBody(string: "hello word!")
response.completed()
}
// 將路由注冊(cè)到服務(wù)器上
server.addRoutes(routes)
// 啟動(dòng)服務(wù)器
do {
try server.start()
} catch PerfectError.networkError(let code, let msg) {
print("network error:\(code) \(msg)")
} catch {
print("unknow network error: \(error)")
}
command? + R 跑起來(lái)~~~??
接著打開瀏覽器,輸入localhost:8181/test,一按回車
成功的顯示響應(yīng)句柄,我們的服務(wù)器成功的跑起來(lái)了,可喜可賀,可喜可賀。
這就是最基本的構(gòu)建流程。
回到本文的Demo中,我們?cè)趍ain文件中,只是簡(jiǎn)單的通過(guò)startServer
方法啟動(dòng)服務(wù)器,因此我們創(chuàng)建NetworkServerManager
類來(lái)封裝以上的流程。
class NetworkServerManager {
// 創(chuàng)建HTTP服務(wù)器
let server = HTTPServer()
//創(chuàng)建路由組,用來(lái)存放路由
var routes = Routes(baseUri: iNoteAIP.base.rawValue)
static let share = NetworkServerManager()
private init() {
//注冊(cè)您自己的路由和請(qǐng)求/響應(yīng)句柄 (請(qǐng)求方法,地址,請(qǐng)求處理)
configure()
}
func serverStart(_ port: UInt16 = 8181) {
// 監(jiān)聽(tīng)8181端口
server.serverPort = port
// 將路由注冊(cè)到服務(wù)器上
server.addRoutes(routes)
// 啟動(dòng)服務(wù)器
do {
try server.start()
} catch PerfectError.networkError(let code, let msg) {
print("network error:\(code) \(msg)")
} catch {
print("unknow network error: \(error)")
}
}
//uri使用iNoteAIP中的枚舉值字符串
func addRouteWith(method: HTTPMethod, uri: iNoteAIP, handler: @escaping RequestHandler) {
routes.add(method: method, uri: uri.rawValue, handler: handler)
}
}
我們?cè)诔跏蓟瘑卫龝r(shí),通過(guò)調(diào)用configure方法將各接口的的路由添加在路由組中,handler參數(shù)傳的是各接口的句柄處理,返回RequestHandler
類型。
extension NetworkServerManager {
//添加各模塊的路由
func configure() {
//登錄注冊(cè)接口的路由
addRouteWith(method: .post, uri: .register, handler: userRegisterHandle())
addRouteWith(method: .post, uri: .login, handler: userLoginHandle())
//筆記的CURD接口的路由
addRouteWith(method: .get, uri: .contentList, handler: getNoteContentListHandle())
addRouteWith(method: .post, uri: .addNote, handler: addNoteHandel())
addRouteWith(method: .delete, uri: .deleteNote, handler: deleteNoteHandle())
addRouteWith(method: .post, uri: .modifyNote, handler: modifyNoteHandle())
}
}
讓我們來(lái)看看RequestHandler是什么類型
public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> ()
原來(lái)是一個(gè)閉包嘛,如果我們?cè)赾onfigure中用閉包形式寫handler,那會(huì)變得臃腫。因此我們可以通過(guò)函數(shù)來(lái)返回該閉包。
// MARK:- 注冊(cè)和登錄
extension NetworkServerManager {
func userRegisterHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理注冊(cè)請(qǐng)求
}
}
func userLoginHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理登錄請(qǐng)求
}
}
}
// MARK:- 筆記CURD
extension NetworkServerManager {
func getNoteContentListHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理獲取筆記列表請(qǐng)求
}
}
func addNoteHandel() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理添加筆記請(qǐng)求
}
}
func deleteNoteHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理刪除筆記請(qǐng)求
}
}
func modifyNoteHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 處理修改筆記請(qǐng)求
}
}
}
這樣,我們就可以在main文件中通過(guò)簡(jiǎn)單的調(diào)用啟動(dòng)服務(wù)器了,并在個(gè)方法中處理相應(yīng)的客戶端請(qǐng)求,可喜可賀,可喜可賀。
第五部分:定制要返回的json格式
此刻,我們暫且先停下來(lái)思考一些問(wèn)題,例如處理一個(gè)客戶端的請(qǐng)求我們應(yīng)該做些什么事情呢?客戶端傳過(guò)來(lái)的參數(shù)缺少必要的字段時(shí)我們?cè)摲祷厥裁葱畔??正確時(shí)我們又該返回什么信息?錯(cuò)誤信息和成功的信息格式是如何的?又是否大致相同?
于是我便采用一種最簡(jiǎn)單的格式來(lái)進(jìn)行演示(反正是演示嘛,將就一下),json格式看起來(lái)像是這樣的:
{
"status": "SUCCESS",
"data": [],
"message": "注冊(cè)成功",
"result": true
}
{
"status": "FAILURE",
"data": [],
"message": "缺少對(duì)應(yīng)參數(shù)",
"result": false
}
data中的數(shù)據(jù)根據(jù)相應(yīng)的接口來(lái)構(gòu)建。所以我們寫一個(gè)枚舉值來(lái)返回status狀態(tài),寫一個(gè)函數(shù)用來(lái)處理生成該json格式的字典。
// MARK:- status狀態(tài)
enum ResponseStatus: String {
case success = "SUCCESS"
case failure = "FAILURE"
}
extension NetworkServerManager {
// 處理要返回的響應(yīng)體,構(gòu)建json格式
func requestHandle(request: HTTPRequest, response: HTTPResponse, status: ResponseStatus, result: Bool, resultMessage: String, data:[[String:Any]]?) {
let jsonDic: [String:Any]
jsonDic = [
"status":status.rawValue,
"result": result,
"message":resultMessage,
"data":data ?? []
]
do {
//jsonEncodedString: 對(duì)字典的擴(kuò)展方法,返回對(duì)應(yīng)json格式的字符串
let json = try jsonDic.jsonEncodedString()
response.setBody(string: json)
} catch {
print(error)
}
response.completed()
}
}
json格式已經(jīng)有了,緊接著我們要對(duì)客戶端請(qǐng)求的參數(shù)表格中取出必要的參數(shù)進(jìn)行合法判斷。以注冊(cè)為例:
func userLoginHandle() -> RequestHandler {
return {[weak self] request, response in
guard let phoneNum = request.param(name: "phoneNum"),
let password = request.param(name: "password"),
phoneNum.characters.count > 0,
password.characters.count > 0
else {
self?.requestHandle(request: request, response: response, status: .failure, result: false, resultMessage: "缺少對(duì)應(yīng)參數(shù)", data: nil)
return
}
//TODO: 參數(shù)合法則進(jìn)行數(shù)據(jù)庫(kù)對(duì)應(yīng)操作
}
}
其他的接口獲取參數(shù)后的處理也與注冊(cè)相似,至此,我們的NetworkServerManager
類在邏輯上基本完成了,接下來(lái)要做的事是跟數(shù)據(jù)庫(kù)打交道了,我們?cè)贒ataBase文件夾中創(chuàng)建數(shù)據(jù)庫(kù)管理類來(lái)為我們進(jìn)行處理數(shù)據(jù),畢竟我們不可能在服務(wù)器類寫數(shù)據(jù)庫(kù)對(duì)吧...
第六部分:數(shù)據(jù)庫(kù)
現(xiàn)在,我們?cè)贒ataBase文件夾中創(chuàng)建DatabaseManager
類來(lái)管理數(shù)據(jù)庫(kù),這里我們使用得是ORM數(shù)據(jù)庫(kù),同樣我們使用單例來(lái)進(jìn)行調(diào)用。在初始化配置時(shí)我們對(duì)MySQLConnector進(jìn)行配置(密碼記得填你們自己的),這里我們并找不到類似start的方法來(lái)啟動(dòng)數(shù)據(jù)庫(kù)連接,因?yàn)樗鼤?huì)在適當(dāng)?shù)臅r(shí)候便自行建立連接,例如調(diào)用單例的時(shí)候,因此我們不必操心建立連接、關(guān)閉連接、打開數(shù)據(jù)庫(kù)、關(guān)閉數(shù)據(jù)庫(kù)等。
數(shù)據(jù)庫(kù)的配置根據(jù)自己的信息進(jìn)行對(duì)應(yīng)的配置。
// MARK:- 數(shù)據(jù)庫(kù)管理類
class DatabaseManager {
static let share = DatabaseManager()
private init() {
MySQLConnector.host = "127.0.0.1"
MySQLConnector.username = "root"
MySQLConnector.password = "此處填你自己的mysql密碼"
MySQLConnector.database = "iNote" //MySql中創(chuàng)建的iNote數(shù)據(jù)庫(kù)
MySQLConnector.port = 3306
}
}
既然是ORM數(shù)據(jù)庫(kù),我們便不需要寫讓人眼花繚亂的sql語(yǔ)句,而是簡(jiǎn)單的通過(guò)調(diào)用對(duì)象的方法進(jìn)行數(shù)據(jù)庫(kù)的操作,以登錄為例:
// MARK:- User
extension DatabaseManager {
// 返回登錄操作后的結(jié)果(result, message, userInfo)
func loginWith(phoneNum: String, password: String) -> (Bool, String, [String:String]) {
return User.userLoginWith(phone: phoneNum, pwd: password) // <-- TODO:
}
}
在外部的NetworkServerManager
類中我們便可以調(diào)用DatabaseManager
了,以登錄為例:
func userLoginHandle() -> RequestHandler {
return {[weak self] request, response in
guard let phoneNum = request.param(name: "phoneNum"),
let password = request.param(name: "password"),
phoneNum.characters.count > 0,
password.characters.count > 0
else {
self?.requestHandle(request: request, response: response, status: .failure, result: false, resultMessage: "缺少對(duì)應(yīng)參數(shù)", data: nil)
return
}
// 操作是否成功, 結(jié)果信息, 用戶信息
let (result, msg, info) = DatabaseManager.share.loginWith(phoneNum: phoneNum, password: password)
let status: ResponseStatus = result ? .success : .failure
self?.requestHandle(request: request, response: response, status: status, result: result, resultMessage: msg, data: [info])
}
}
其他接口的處理與此類似,可以查看本文的服務(wù)端Demo,現(xiàn)在我們繼續(xù)以登錄為例,接下來(lái)的事情只剩下User
類對(duì)數(shù)據(jù)庫(kù)的操作了。成功近在咫尺,可喜可賀,可喜可賀。
第七部分:MySQLStORM對(duì)象
使用ORM數(shù)據(jù)庫(kù)實(shí)際上是操作ORM對(duì)象,perfect框架已經(jīng)幫我們實(shí)現(xiàn)所需要的CURD方法,我們直接調(diào)用方法的方式來(lái)操作數(shù)據(jù)庫(kù)即可。我們只需寫對(duì)應(yīng)的模型類,繼承MySQLStORM
類,實(shí)現(xiàn)要求重寫的父類方法即可。該模型對(duì)應(yīng)的屬性名、屬性類型便是數(shù)據(jù)庫(kù)中對(duì)應(yīng)的字段名以及字段類型。這里引入官方文檔的一個(gè)重要要求:
?注意? 該對(duì)象的第一個(gè)屬性將成為對(duì)應(yīng)數(shù)據(jù)表的主索引 —— 傳統(tǒng)的方式就是給主索引列起名叫做 id,雖然您可以為主索引字段設(shè)置任何有效的名字。SQL這種關(guān)系數(shù)據(jù)庫(kù)的主索引典型類型是整型、字符串或者UUID編碼。如果您的主索引不是自動(dòng)遞增的整數(shù),則一定要設(shè)置好這個(gè)id值,以保證數(shù)據(jù)的完整性和一致性。
以處理用戶注冊(cè)登錄的User模型為例(這里只是為了簡(jiǎn)單演示也沒(méi)弄UUID、token之類的字段):
import Foundation
import MySQLStORM
import StORM
class User: MySQLStORM {
// ?注意?:第一個(gè)屬性將成為主索引字段,所以應(yīng)該是ID
var id: Int = 0
var phoneNum: String = ""
var password: String = ""
var registerTime: String = ""
fileprivate override init() {
super.init()
do {
//確保該模型的表格存在
try setupTable()
} catch {
print(error)
}
}
//給對(duì)象的表名
override func table() -> String {
return "User"
}
override func to(_ this: StORMRow) {
id = numericCast(this.data["id"] as! Int32)
phoneNum = this.data["phoneNum"] as! String
password = this.data["password"] as! String
registerTime = this.data["registerTime"] as! String
}
fileprivate func rows() -> [User] {
var rows: [User] = []
for r in results.rows {
let row = User()
row.to(r)
rows.append(row)
}
return rows
}
}
在這里著重說(shuō)明一下在to
方法中為什么使用numericCast
。numericCast
是用于整型之間的轉(zhuǎn)換的,在實(shí)戰(zhàn)的過(guò)程中,起初直接用this.data["id"] as! Int
是沒(méi)問(wèn)題,可是當(dāng)從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)時(shí)就報(bào)了一個(gè)錯(cuò)誤。
Could not cast value of type 'Swift.Int32' (0x1014c1df0) to 'Swift.Int' (0x1014c2430).
2017-11-03 20:50:23.014244+0800 iNoteServer[54873:750541] Could not cast value of type 'Swift.Int32' (0x1014c1df0) to 'Swift.Int' (0x1014c2430).
從數(shù)據(jù)庫(kù)讀取出來(lái)的id類型變成了Int32
了。(當(dāng)時(shí)我臉就是這么黑的),所以用numericCast
來(lái)轉(zhuǎn)換一下類型即可。
//API相關(guān)操作
extension User {
//驗(yàn)證用戶是否存在
fileprivate func findUserWith(_ phone: String) {
// fine: 如果在數(shù)據(jù)庫(kù)中匹配到了,則將字段的內(nèi)容賦值給對(duì)象中的屬性,否則什么都不做
do {
try find([("phoneNum",phone)])
} catch {
print(error)
}
}
//登錄 -> 返回(操作結(jié)果, 結(jié)果信息, 用戶信息)
static func userLoginWith(phone: String, pwd: String) -> (Bool, String, [String:String]) {
let user = User()
user.findUserWith(phone)
if user.phoneNum == phone && user.password == pwd {
let info = [
"userId": "\(user.id)",
"phoneNum": user.phoneNum,
"registerTime": user.registerTime
]
return (true, "登錄成功", info)
} else {
let info = ["userId": "", "phoneNum": "", "registerTime": ""]
return (false, "用戶名或密碼錯(cuò)誤", info)
}
}
}
至此,用戶登錄功能已經(jīng)基本完成了。現(xiàn)在到了測(cè)試接口的時(shí)候了,成敗在此一舉。command? + R 跑起來(lái)~~~~??。在這里我們使用Paw(測(cè)接口的神器)來(lái)測(cè)試我們的接口:
成功了~~
其他接口的實(shí)現(xiàn)方式也按照同樣的套路實(shí)現(xiàn)就可以,至此,本文基于perfect框架用swift寫服務(wù)端也在此處告一段落。接下來(lái)則會(huì)寫一篇與這個(gè)iNoteServer服務(wù)端相對(duì)應(yīng)的iNoteClient。
至于本人才疏學(xué)淺,對(duì)后端只是略知一二,斗膽嘗試,如果錯(cuò)漏,懇請(qǐng)各位大佬多多包涵與明示。??