go+iris+jwt+mysql+xorm+viper,iris 專案實戰簡易聊天室,登入、註冊、私聊、群組聊天。
先看看本文檔下面的前端介紹,知道如何操作(因為精力有限,ui 並不是特別人性化)。
訪問演示地址,如果上方小圖示狀態是正常的則可以訪問,可能會進入休眠狀態,需要稍微等幾秒。
專案目前只寫了mysql 相關的適配,但是用了xorm,做其他資料庫支援也不難,可以做,但沒必要哈哈哈,懶得自己運行的看看demo 網址就好了,自己如果有興趣的話mysql 現在沒有誰是沒有的吧。
本來想支援sqlite 的,這樣就不需要配置資料庫參數,但是考慮windows 下編譯sqlite 要配置gcc 環境,比較麻煩,反而沒有快速啟動的效果了,乾脆就不整了,所以其他資料庫暫時沒有支援(哪天有空了再加進去)。
所以項目只需要配置以下參數
git clone https://github.com/JabinGP/demo-chatroom.git
cd demo-chatroom
// 复制config.toml.example 为 config.toml 并填写数据库信息,或者可选修改端口号
go run main.go
預設為8888 端口,啟動後訪問http://localhost:8888
用了react,但沒用ui 框架,很多的小細節上表現並不好,照著手機的尺寸做的界面,電腦打開的話可以開個f12 看著比較舒服,湊合著看吧,重點放在後端。
聊天框設定了視窗自動滾動到底端,但是api 是react 提供的,發現在許多瀏覽器上並不相容,使用chrome 瀏覽器可以解決這個問題。
註冊後手動返回選擇登陸,訊息框裡面的紅色名稱為公共發言,灰色名稱為私聊發言、可以在紅色的框裡面指定接收者的名稱,如果不指定的話,默認是公共發言,指定後只有對應的用戶能看到資訊。
藍色框內顯示自己的使用者名,點選即直接登出登入。
api 格式基於restful 設計,登入功能使用jwt 完成,許多介面需要登入狀態,請求的時候需要攜帶JWT,具體請看golang iris 的jwt 實踐,另外便於測試,JWT 簽發有效時間只設定了20 分鐘,過期需要重新登入。
api 請求格式與一般介面無異,Get 使用Params,Post、Put、Delete 等使用Body 中Json 傳參。
返回格式有較大爭議,我也研究了一段時間,有人主張使用全200 的http 狀態碼,在返回內容中添加code 來標識錯誤,就像這樣:
// 注册错误时
// http status 200
{
"code" : 40001 ,
"msg" : "注册用户名非法"
}
// 注册成功时
// http status 200
{
"code" : 200 ,
"msg" : "成功" ,
"data" : {
"username" : " JabinGP " ,
"id" : 3
}
}
又有人主張使用全http 狀態碼來表示錯誤:
// 注册错误时
// http status 400
{
"msg" : "注册用户名非法"
}
// 注册成功时
// http status 200
{
"username" : " JabinGP " ,
"id" : 3
}
實際上,以上兩種做法都各有利弊:
基於以上的情況,我將兩者結合:
成功時,返回http 狀態碼200
// 注册成功时
// http status 200
{
"username" : " JabinGP " ,
"id" : 3
}
失敗時,選擇常用的幾個狀態碼進行表達錯誤,400(請求錯誤),500(伺服器內部錯誤),404(找不到),401(認證失敗),對錯誤進行大概分類之後,再在返回的資料中自訂一個code、msg、detail 來表示詳細的錯誤原因:
// 注册失败
// http status 400
{
"code" : 6 ,
"msg" : "数据检验失败" ,
"detail" : "用户名已存在"
}
// 登录失效
// http status 401
{
"code" : 8 ,
"msg" : "未认证登录" ,
"detail" : " Token is expired "
}
這樣進行結合之後,成功回呼就是成功,不需要寫出res.data.data 這個重複的寫法,錯誤回呼就只處理錯誤,可以透過http 狀態碼判斷,並且可以透過進一步的code 和msg,detail來進行錯誤處理。
api 清單如下,把localhost 換成mike.jabingp.cn 也可以直接要求到演示後端:
功能 | 請求方式 | 地址 |
---|---|---|
取得登入token | POST | http://localhost:8888/v1/login |
尋找用戶 | GET | http://localhost:8888/v1/user |
註冊 | POST | http://localhost:8888/v1/user |
用戶自己修改訊息 | PUT | http://localhost:8888/v1/user |
用戶發送訊息 | POST | http://localhost:8888/v1/message |
用戶獲取資訊 | GET | http://localhost:8888/v1/message |
用戶獲取token 信息 | GET | http://localhost:8888/v1/token/info |
詳細請求參數可以在demo-chatroom 的postman-api 文件裡查看。
或查看原始碼,請求參數在model/reqo
裡面查看,回應參數可以在model/reso
裡查看
聊天功能AJAX 不是最好的選擇,WebSocket 比較好,但是被要求使用了AJAX 所以沒有選擇後者。
專案的前端比較簡陋,因為只是作為demo 使用。
英文不是很好,程式碼註解用英文只是因為懶得切換輸入法。
第一次用go 開發web 項目,也是第一次用react 寫前端,由於前端沒怎麼注重項目結構(xjbx),就不放源碼了,把項目編譯後放在了assets 資料夾下,可讀性很差,但是可以和後端一起啟動,不需要單獨啟動前端,比較方便查看效果。如果還有時間會考慮用原生寫一個極簡版的供大家參考原理。
第一次用ORM 操作資料庫,感覺好難用,我還是寧願手寫sql,好多想要的效果翻半天文件都找不到解決方案,後期有機會考慮用sqlx 重構。
最近對Go 比較有興趣,又接到任務編寫一個簡易聊天室,發現目前iris 的專案實踐比較少,只有一些HelloWorld 等級的範例,於是決定用Go 來做,然後開源出來供大互相參考借鑒,當然專案架構如何設計完全基於我有限的開發經驗,對於不合理的地方,請給你寶貴的意見。
這個項目有以下要求
登陸功能這次選用JWT
來實現, JWT
和Session
各自的優劣就不再贅述。
基於AJAX 是所有前後端分離項目的必備,因此這個功能不過多討論,這裡重點在於無刷新,難點在哪裡?
使用者的操作邏輯是,在聊天室裡面發送數據,然後數據就被發出去,聊天介面要顯示出自己發送的數據,以及要即時更新別人發出來的數據。
前端和後端之間是透過AJAX 來交流的,前端發送資料和後端發送資料可以表現為
這裡有什麼問題?問就在前端永遠只能主動發起請求,而後端永遠只能接受請求。這意味著最新的訊息永遠無法即時從後端主動發送給前端,最新的訊息只能先存放在後端,然後等待前端發起請求,後端才能傳回資料。
由於後端是沒有能力主動推送訊息給前端的,因此用戶獲取最新數據的解決方法是前端設定一個定時器每隔一段比较短的时间就请求一次后台接口(轮询)
,這樣就能不斷更新數據。
前端已經確定使用AJAX 定時輪詢後台介面來取得最新數據,為了數據即時性,輪詢間隔會小于1s
,這樣又會帶來另一個問題,後端在如此頻繁的請求下,一定不能每次都將所有數據都傳輸出去,一是數據大小導致的網路傳輸效率、流量成本問題,二是數據大小導致的前端判斷新數據的效率問題,那麼後端每次必須都回傳前端還沒有接收過的數據,而問題在於--後端怎麼知道前端已經接收了哪些訊息?
這個就要利用到訊息的自增主键
,只需要前端每次請求的時候都攜帶上前端已經接收的最后的消息的主键
,由於主鍵是不重複且自增的,我們可以很輕鬆的找出比該主鍵大的數據,也就是前端還沒接收到的數據。
語言
框架
資料儲存
科技
由於使用了Xorm 資料庫ORM 框架,以下表格都是自動產生的,自帶了
xxxxxx_at
字段
基於如上的需求,設計了users
和messages
兩個表
關鍵字段
資料庫表結構
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | bigint(20) | NO | PRI | NULL | auto_increment |
username | varchar(255) | YES | NULL | ||
passwd | varchar(255) | YES | NULL | ||
gender | bigint(20) | YES | NULL | ||
age | bigint(20) | YES | NULL | ||
interest | varchar(255) | YES | NULL | ||
created_at | datetime | YES | NULL | ||
updated_at | datetime | YES | NULL | ||
deleted_at | datetime | YES | NULL |
關鍵字段
資料庫表結構
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | bigint(20) | NO | PRI | NULL | auto_increment |
sender_id | bigint(20) | YES | NULL | ||
receiver_id | bigint(20) | YES | NULL | ||
content | varchar(255) | YES | NULL | ||
send_time | bigint(20) | YES | NULL | ||
created_at | datetime | YES | NULL | ||
updated_at | datetime | YES | NULL | ||
deleted_at | datetime | YES | NULL |
以下結構為個人經驗,有不當之處請給予寶貴意見
pojo
很好理解,就是資料庫對應的實體,但不要求與資料庫欄位一一對應
reqo(request object)、reso(response object)
不同介面請求的時候,可以攜帶的參數以及回應的資料也不同,所以為每一個介面設計一個對應的請求實體和回應實體
以下為個人理解
Controller
主要職責是,接受請求的請求參數,轉換為reqo,進行簡單的請求參數驗證(我個人的定義與資料庫無關的驗證,如非空、非零),呼叫Service 層的函數取得pojo 結果,並將pojo 結果轉換封裝為reso 回傳。
Service
主要職責是,對Dao 層的介面進一步封裝,提供通用的介面給Controller 調用,返回資料可以是pojo,在Service 內需要進行資料的驗證,如(新增用戶,校驗用戶名是否重複)。
Dao
這裡基本上一個方法直接對應一條sql 語句,不做任何的驗證,認為接收到的資料是可靠的(已經經過了Controller 和Service 兩層的參數驗證了),回傳資料可以是pojo。