说明
本文Demo是使用 Swift3.0 基于perfect框架用swift写服务端的文章。对于perfect框架入门和配置方面不做过多的讲解,想要了解该方面的大佬们请参考以下学习资料,而客户端方面则使用RxSwift框架来写,关于客户端的内容在有机会再进行介绍,本文着重讲解服务端Demo。
首先献上本文Demo
GitHub:服务端Demo(Perfect)
GitHub:客户端Demo(RxSwift+Moya)
接着再献上参考的学习资料
GitHub地址,过万的start
官方中文文档,教练我要学这个
perfect框架入门比较不错文章,配置方面讲得很详细
本文实战Demo主要的参考来源,对官方文档有一些重要的补充说明,入门讲解很详细
Perfect是什么东西呢?
Perfect是一组完整、强大的工具箱、软件框架体系和Web应用服务器,可以在Linux、iOS和macOS (OS X)上使用。该软件体系为Swift工程师量身定制了一整套用于开发轻量、易维护、规模可扩展的Web应用及其它REST服务的解决方案,这样Swift工程师就可以实现同时在服务器和客户端上采用同一种语言开发软件项目。
性能对比
一篇性能对比的文章:不服跑个分
至于是什么原因让我想学习perfect呢?
作为一名刚接触ios开发没多久的小白,回忆起当初加入学校一个软件开发团队的时候,为了能与团队其他方面的人相互协作,了解其他方面的一些基本知识是有必要的。就好比前后端交互,作为移动端方面也要了解后端知识,这样在前后端交互的时候就会少很多麻烦事。于是在加入团队初期,师兄便要求我自己写一个demo,服务端你用什么写都可以。对于当时的我来说,真的是件麻烦事了,因为学习ios并不像学习Android,Android使用java语言,Android与java服务器相互协作,所以在学习Android的同时或多或少也会学到一些后端的知识。🤔虽然最后我用python写了一个很烂的后端,但是那时候便在想为啥不能写Android那样,能用同一种语言也写后端。直到前些日子发现了perfect,倍感欢喜,于是便琢磨了一番。接下来实战Demo! GO! GO! GO!
第一部分:Demo演示
由于简书限制了图片的大小,所以只能分开进行演示。(内心的忧伤你们应该懂吧)
注册:
登录:
添加笔记:
修改笔记:
删除笔记:
数据库中直接操作:
第二部分:初始化项目结构
首先让我们按部就班的完成初始化工作,我们的工程名就叫iNoteServer好了,所以我们创建一个iNoteServer,在iNoteServer里我们使用终端创建Package.swfit文件和一个Sources文件夹,在Sources文件夹里创建一个main.swift文件。你的项目结构在iNoteServer文件里看起来是这样的:
紧接着在Package.swfit文件中,写入需要使用的仓库。
import PackageDescription
let versions = Version(0,0,0)..<Version(10,0,0)
let urls = [
"https://github.com/PerfectlySoft/Perfect-HTTPServer.git", //服务端核心框架
"https://github.com/SwiftORM/MySQL-StORM.git", //对象关系型数据库
]
let package = Package(
name: "iNoteServer",
targets: [],
dependencies: urls.map { .Package(url: $0, versions: versions) }
)
然后我们在终端中输入swift build。(该过程等待的时间挺久的,毕竟网速慢,文件也不小...)fetch完成之后,输入swift package generate-xcodeproj命令创建iNoteServer.xcodeproj文件。打开iNoteServer.xcodeproj文件,在Build Settings中Library Search Paths检索项目软件库中增加(不单单是编译目标)
$(PROJECT_DIR) - Recursive
最后,我们划分一下目录:
DataBase目录里存放含管理数据库的类(DatabaseManager
),一些ORM对象的数据模型(User
对象,NoteContent
对象)
NetworkServer目录里存放接口API(iNoteAIP
)以及HTTPServer类(NetworkServerManager
)
因此在main文件中,我们只要简单的通过HTTPServer类来调用start方法就可以直接启动服务器了
NetworkServerManager.share.serverStart()
至此,我们项目的初始化已经完成了,可喜可贺,可喜可贺。
第三部分:创建各功能模块的接口
在这里,我创建一个iNoteAPI文件用来管理各模块的接口
enum iNoteAIP: String {
case base = "/iNote"
case register = "/register" //注册页面
case login = "/login" //登录页面
case contentList = "/contentList" //获取笔记列表
case addNote = "/addNote" //添加笔记
case deleteNote = "/deleteNote" //删除笔记
case modifyNote = "/modifyNote" //修改笔记
}
第四部:创建HTTP服务器管理类
在此之前我还是先提一下使用perfect框架构建服务器的基本流程,详细的还是请看学习参考资料。
在main文件中直接写入以下代码:
import PerfectLib
import PerfectHTTP
import PerfectHTTPServer
// 创建HTTP服务器
let server = HTTPServer()
// 监听8181端口
server.serverPort = 8181
//创建路由组,用来存放各个路由
var routes = Routes()
//注册您自己的路由和请求/响应句柄 (请求方法,地址,请求处理)
routes.add(method: .get, uri: "test") { (request, response) in
response.setBody(string: "hello word!")
response.completed()
}
// 将路由注册到服务器上
server.addRoutes(routes)
// 启动服务器
do {
try server.start()
} catch PerfectError.networkError(let code, let msg) {
print("network error:\(code) \(msg)")
} catch {
print("unknow network error: \(error)")
}
command⌘ + R 跑起来~~~🏃
接着打开浏览器,输入localhost:8181/test,一按回车
成功的显示响应句柄,我们的服务器成功的跑起来了,可喜可贺,可喜可贺。
这就是最基本的构建流程。
回到本文的Demo中,我们在main文件中,只是简单的通过startServer
方法启动服务器,因此我们创建NetworkServerManager
类来封装以上的流程。
class NetworkServerManager {
// 创建HTTP服务器
let server = HTTPServer()
//创建路由组,用来存放路由
var routes = Routes(baseUri: iNoteAIP.base.rawValue)
static let share = NetworkServerManager()
private init() {
//注册您自己的路由和请求/响应句柄 (请求方法,地址,请求处理)
configure()
}
func serverStart(_ port: UInt16 = 8181) {
// 监听8181端口
server.serverPort = port
// 将路由注册到服务器上
server.addRoutes(routes)
// 启动服务器
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)
}
}
我们在初始化单例时,通过调用configure方法将各接口的的路由添加在路由组中,handler参数传的是各接口的句柄处理,返回RequestHandler
类型。
extension NetworkServerManager {
//添加各模块的路由
func configure() {
//登录注册接口的路由
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())
}
}
让我们来看看RequestHandler是什么类型
public typealias RequestHandler = (HTTPRequest, HTTPResponse) -> ()
原来是一个闭包嘛,如果我们在configure中用闭包形式写handler,那会变得臃肿。因此我们可以通过函数来返回该闭包。
// MARK:- 注册和登录
extension NetworkServerManager {
func userRegisterHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理注册请求
}
}
func userLoginHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理登录请求
}
}
}
// MARK:- 笔记CURD
extension NetworkServerManager {
func getNoteContentListHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理获取笔记列表请求
}
}
func addNoteHandel() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理添加笔记请求
}
}
func deleteNoteHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理删除笔记请求
}
}
func modifyNoteHandle() -> RequestHandler {
return {[weak self] request, response in
//TODO: 处理修改笔记请求
}
}
}
这样,我们就可以在main文件中通过简单的调用启动服务器了,并在个方法中处理相应的客户端请求,可喜可贺,可喜可贺。
第五部分:定制要返回的json格式
此刻,我们暂且先停下来思考一些问题,例如处理一个客户端的请求我们应该做些什么事情呢?客户端传过来的参数缺少必要的字段时我们该返回什么信息?正确时我们又该返回什么信息?错误信息和成功的信息格式是如何的?又是否大致相同?
于是我便采用一种最简单的格式来进行演示(反正是演示嘛,将就一下),json格式看起来像是这样的:
{
"status": "SUCCESS",
"data": [],
"message": "注册成功",
"result": true
}
{
"status": "FAILURE",
"data": [],
"message": "缺少对应参数",
"result": false
}
data中的数据根据相应的接口来构建。所以我们写一个枚举值来返回status状态,写一个函数用来处理生成该json格式的字典。
// MARK:- status状态
enum ResponseStatus: String {
case success = "SUCCESS"
case failure = "FAILURE"
}
extension NetworkServerManager {
// 处理要返回的响应体,构建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: 对字典的扩展方法,返回对应json格式的字符串
let json = try jsonDic.jsonEncodedString()
response.setBody(string: json)
} catch {
print(error)
}
response.completed()
}
}
json格式已经有了,紧接着我们要对客户端请求的参数表格中取出必要的参数进行合法判断。以注册为例:
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: "缺少对应参数", data: nil)
return
}
//TODO: 参数合法则进行数据库对应操作
}
}
其他的接口获取参数后的处理也与注册相似,至此,我们的NetworkServerManager
类在逻辑上基本完成了,接下来要做的事是跟数据库打交道了,我们在DataBase文件夹中创建数据库管理类来为我们进行处理数据,毕竟我们不可能在服务器类写数据库对吧...
第六部分:数据库
现在,我们在DataBase文件夹中创建DatabaseManager
类来管理数据库,这里我们使用得是ORM数据库,同样我们使用单例来进行调用。在初始化配置时我们对MySQLConnector进行配置(密码记得填你们自己的),这里我们并找不到类似start的方法来启动数据库连接,因为它会在适当的时候便自行建立连接,例如调用单例的时候,因此我们不必操心建立连接、关闭连接、打开数据库、关闭数据库等。
数据库的配置根据自己的信息进行对应的配置。
// MARK:- 数据库管理类
class DatabaseManager {
static let share = DatabaseManager()
private init() {
MySQLConnector.host = "127.0.0.1"
MySQLConnector.username = "root"
MySQLConnector.password = "此处填你自己的mysql密码"
MySQLConnector.database = "iNote" //MySql中创建的iNote数据库
MySQLConnector.port = 3306
}
}
既然是ORM数据库,我们便不需要写让人眼花缭乱的sql语句,而是简单的通过调用对象的方法进行数据库的操作,以登录为例:
// MARK:- User
extension DatabaseManager {
// 返回登录操作后的结果(result, message, userInfo)
func loginWith(phoneNum: String, password: String) -> (Bool, String, [String:String]) {
return User.userLoginWith(phone: phoneNum, pwd: password) // <-- TODO:
}
}
在外部的NetworkServerManager
类中我们便可以调用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: "缺少对应参数", data: nil)
return
}
// 操作是否成功, 结果信息, 用户信息
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])
}
}
其他接口的处理与此类似,可以查看本文的服务端Demo,现在我们继续以登录为例,接下来的事情只剩下User
类对数据库的操作了。成功近在咫尺,可喜可贺,可喜可贺。
第七部分:MySQLStORM对象
使用ORM数据库实际上是操作ORM对象,perfect框架已经帮我们实现所需要的CURD方法,我们直接调用方法的方式来操作数据库即可。我们只需写对应的模型类,继承MySQLStORM
类,实现要求重写的父类方法即可。该模型对应的属性名、属性类型便是数据库中对应的字段名以及字段类型。这里引入官方文档的一个重要要求:
️注意️ 该对象的第一个属性将成为对应数据表的主索引 —— 传统的方式就是给主索引列起名叫做 id,虽然您可以为主索引字段设置任何有效的名字。SQL这种关系数据库的主索引典型类型是整型、字符串或者UUID编码。如果您的主索引不是自动递增的整数,则一定要设置好这个id值,以保证数据的完整性和一致性。
以处理用户注册登录的User模型为例(这里只是为了简单演示也没弄UUID、token之类的字段):
import Foundation
import MySQLStORM
import StORM
class User: MySQLStORM {
// ️注意️:第一个属性将成为主索引字段,所以应该是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)
}
}
//给对象的表名
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
}
}
在这里着重说明一下在to
方法中为什么使用numericCast
。numericCast
是用于整型之间的转换的,在实战的过程中,起初直接用this.data["id"] as! Int
是没问题,可是当从数据库中读取数据时就报了一个错误。
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).
从数据库读取出来的id类型变成了Int32
了。(当时我脸就是这么黑的),所以用numericCast
来转换一下类型即可。
//API相关操作
extension User {
//验证用户是否存在
fileprivate func findUserWith(_ phone: String) {
// fine: 如果在数据库中匹配到了,则将字段的内容赋值给对象中的属性,否则什么都不做
do {
try find([("phoneNum",phone)])
} catch {
print(error)
}
}
//登录 -> 返回(操作结果, 结果信息, 用户信息)
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, "用户名或密码错误", info)
}
}
}
至此,用户登录功能已经基本完成了。现在到了测试接口的时候了,成败在此一举。command⌘ + R 跑起来~~~~🏃。在这里我们使用Paw(测接口的神器)来测试我们的接口:
成功了~~
其他接口的实现方式也按照同样的套路实现就可以,至此,本文基于perfect框架用swift写服务端也在此处告一段落。接下来则会写一篇与这个iNoteServer服务端相对应的iNoteClient。
至于本人才疏学浅,对后端只是略知一二,斗胆尝试,如果错漏,恳请各位大佬多多包涵与明示。🤣