开发记事本实战,体验perfect框架用swift写服务端

Perfect

说明

本文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方法中为什么使用numericCastnumericCast是用于整型之间的转换的,在实战的过程中,起初直接用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。
至于本人才疏学浅,对后端只是略知一二,斗胆尝试,如果错漏,恳请各位大佬多多包涵与明示。🤣

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 官方网站文档详情: 官方中文文档 实践Demo Perfect实践Demo 简介 Perfect是一组完整、强大的...
    LuisX阅读 19,299评论 58 101
  • 在做自己的项目的时候遇到这样一个需求,遂即使来纪录一下 2015年12月31号 周四 我需要得到的是 2015年...
    Easy_VO阅读 14,092评论 1 8
  • 今天上午去了依然是理论课,我就服了,一上午理论课我真的觉得就没时间约客户,每天大好时光都被理论课占了,主管也啥...
    王紫彦的老公阅读 211评论 0 0