前言
關於 FlutterGo 或許不用太多介紹了。前端
若是有第一次據說的小夥伴,能夠移步FlutterGo官網查看下簡單介紹.node
FlutterGo 在此次迭代中有了很多的更新,筆者在這次的更新中,負責開發後端以及對應的客戶端部分。這裏簡單介紹下關於 FlutterGo 後端代碼中幾個功能模塊的實現。mysql
整體來講,FlutterGo 後端並不複雜。此文中大概介紹如下幾點功能(接口)的實現:git
- FlutterGo 登錄功能
- 組件獲取功能
- 收藏功能
- 建議反饋功能
環境信息
阿里雲 ECS 雲服務器github
Linux iz2ze3gw3ipdpbha0mstybz 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
web
mysql :mysql Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)
spring
node:v12.5.0
sql
開發語言:midway
+ typescript
+ mysql
typescript
代碼結構:json
src ├─ app │ ├─ class 定義表結構 │ │ ├─ app_config.ts │ │ ├─ cat.ts │ │ ├─ collection.ts │ │ ├─ user.ts │ │ ├─ user_collection.ts │ │ └─ widget.ts │ ├─ constants 常量 │ │ └─ index.ts │ ├─ controller │ │ ├─ app_config.ts │ │ ├─ auth.ts │ │ ├─ auth_collection.ts │ │ ├─ cat_widget.ts │ │ ├─ home.ts │ │ ├─ user.ts │ │ └─ user_setting.ts │ ├─ middleware 中間件 │ │ └─ auth_middleware.ts │ ├─ model │ │ ├─ app_config.ts │ │ ├─ cat.ts │ │ ├─ collection.ts │ │ ├─ db.ts │ │ ├─ user.ts │ │ ├─ user_collection.ts │ │ └─ widget.ts │ ├─ public │ │ └─ README.md │ ├─ service │ │ ├─ app_config.ts │ │ ├─ cat.ts │ │ ├─ collection.ts │ │ ├─ user.ts │ │ ├─ user_collection.ts │ │ ├─ user_setting.ts │ │ └─ widget.ts │ └─ util 工具集 │ └─ index.ts ├─ config 應用的配置信息 │ ├─ config.default.ts │ ├─ config.local.ts │ ├─ config.prod.ts │ └─ plugin.ts └─ interface.ts
登錄功能
首先在class/user.ts
中定義一個 user
表結構,大概須要的字段以及在 interface.ts
中聲明相關接口。這裏是 midway
和 ts
的基礎配置,就不展開介紹了。
FlutterGo 提供了兩種登錄方式:
- 用戶名、密碼登錄
GitHubOAuth
認證
由於是手機客戶端的 GitHubOauth
認證,因此這裏實際上是有一些坑的,後面再說。這裏咱們先從簡單的開始提及
用戶名/密碼登錄
由於咱們使用 github 的用戶名/密碼登錄方式,因此這裏須要羅列下 github 的 api:developer.github.com/v3/auth/,
文檔中的核心部分:curl -u username https://api.github.com/user
(你們能夠自行在 terminal 上測試),回車輸入密碼便可。因此這裏咱們徹底能夠在拿到用戶輸入的用戶名和密碼後進行 githu 的認證。
關於 midway 的基本用法,這裏也再也不贅述了。整個過程仍是很是簡單清晰的,以下圖:
相關代碼實現(相關信息已脫敏:xxx):
service
部分
//獲取 userModel @inject() userModel // 獲取 github 配置信息 @config('githubConfig') GITHUB_CONFIG; //獲取請求上下文 @inject() ctx;
//githubAuth 認證 async githubAuth(username: string, password: string, ctx): Promise<any> { return await ctx.curl(GITHUB_OAUTH_API, { type: 'GET', dataType: 'json', url: GITHUB_OAUTH_API, headers: { 'Authorization': ctx.session.xxx } }); }
// 查找用戶 async find(options: IUserOptions): Promise<IUserResult> { const result = await this.userModel.findOne( { attributes: ['xx', 'xx', 'xx', 'xx', 'xx', "xx"],//相關信息脫敏 where: { username: options.username, password: options.password } }) .then(userModel => { if (userModel) { return userModel.get({ plain: true }); } return userModel; }); return result; }
// 經過 URLName 查找用戶 async findByUrlName(urlName: string): Promise<IUserResult> { return await this.userModel.findOne( { attributes: ['xxx', 'xxx', 'xxx', 'xxx', 'xxx', "xxx"], where: { url_name: urlName } } ).then(userModel => { if (userModel) { return userModel.get({ plain: true }); } return userModel; }); }
// 建立用戶 async create(options: IUser): Promise<any> { const result = await this.userModel.create(options); return result; } // 更新用戶信息 async update(id: number, options: IUserOptions): Promise<any> { return await this.userModel.update( { username: options.username, password: options.password }, { where: { id }, plain: true } ).then(([result]) => { return result; }); }
controller
// inject 獲取 service 和加密字符串 @inject('userService') service: IUserService @config('random_encrypt') RANDOM_STR;
流程圖中邏輯的代碼實現
GitHubOAuth 認證
這裏有坑!我回頭介紹
githubOAuth 認證就是咱們常說的 github app 了,這裏我直接了當的丟文檔:creating-a-github-app
筆者仍是以爲文檔類的無需介紹
固然,我這裏確定都建好了,而後把一些基本信息都寫到 server 端的配置中
仍是按照上面的套路,我們先介紹流程。而後在說坑在哪。
客戶端部分
客戶端部分的代碼就至關簡單了,新開 webView ,直接跳轉到 github.com/login/oauth/authorize
帶上 client_id
便可。
server 端
總體流程如上,部分代碼展現:
service
//獲取 github access_token async getOAuthToken(code: string): Promise<any> { return await this.ctx.curl(GITHUB_TOKEN_URL, { type: "POST", dataType: "json", data: { code, client_id: this.GITHUB_CONFIG.client_id, client_secret: this.GITHUB_CONFIG.client_secret } }); }
controller
代碼邏輯就是調用 service 中的數據來走上面流程圖中的信息。
OAuth 中的坑
其實,github app 的認證方式很是適用於瀏覽器環境下,可是在 flutter 中,因爲咱們是新開啓的 webView 來請求的 github 登錄地址。當咱們後端成功返回的時候,沒法通知到 Flutter 層。就致使我本身的 Flutter 中 dart 寫的代碼,沒法拿到接口的返回。
中間腦暴了不少解決辦法,最終在查閱 flutter_webview_plugin 的 API 裏面找了個好的方法:onUrlChanged
簡而言之就是,Flutter 客戶端部分新開一個 webView去請求 github.com/login
,github.com/login
檢查 client_id
後會帶着code 等亂七八糟的東西來到後端,後端校驗成功後,redirect Flutter 新開的 webView,而後flutter_webview_plugin
去監聽頁面 url 的變化。發送相關 event ,讓Flutter 去 destroy 當前 webVIew,處理剩餘邏輯。
Flutter 部分代碼
//定義相關 OAuth event class UserGithubOAuthEvent{ final String loginName; final String token; final bool isSuccess; UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess); }
webView page
:
//在 initState 中監聽 url 變化,並emit event flutterWebviewPlugin.onUrlChanged.listen((String url) { if (url.indexOf('loginSuccess') > -1) { String urlQuery = url.substring(url.indexOf('?') + 1); String loginName, token; List<String> queryList = urlQuery.split('&'); for (int i = 0; i < queryList.length; i++) { String queryNote = queryList[i]; int eqIndex = queryNote.indexOf('='); if (queryNote.substring(0, eqIndex) == 'loginName') { loginName = queryNote.substring(eqIndex + 1); } if (queryNote.substring(0, eqIndex) == 'accessToken') { token = queryNote.substring(eqIndex + 1); } } if (ApplicationEvent.event != null) { ApplicationEvent.event .fire(UserGithubOAuthEvent(loginName, token, true)); } print('ready close'); flutterWebviewPlugin.close(); // 驗證成功 } else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) { // 驗證失敗 if (ApplicationEvent.event != null) { ApplicationEvent.event.fire(UserGithubOAuthEvent('', '', true)); } flutterWebviewPlugin.close(); } });
login page
:
//event 的監聽、頁面跳轉以及提醒信息的處理 ApplicationEvent.event.on<UserGithubOAuthEvent>().listen((event) { if (event.isSuccess == true) { // oAuth 認證成功 if (this.mounted) { setState(() { isLoading = true; }); } DataUtils.getUserInfo( {'loginName': event.loginName, 'token': event.token}) .then((result) { setState(() { isLoading = false; }); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => AppPage(result)), (route) => route == null); }).catchError((onError) { print('獲取身份信息 error:::$onError'); setState(() { isLoading = false; }); }); } else { Fluttertoast.showToast( msg: '驗證失敗', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1, backgroundColor: Theme.of(context).primaryColor, textColor: Colors.white, fontSize: 16.0); } });
組件樹獲取
表結構
在聊接口實現的以前,咱們先了解下,關於組件,咱們的表機構設計大概是什麼樣子的。
FlutterGO 下面 widget tab不少分類,分類點進去仍是分類,再點擊去是組件,組件點進去是詳情頁。
上圖模塊點進去就是組件 widget
上圖是 widget,點進去是詳情頁
因此這裏咱們須要兩張表來記錄他們的關係:cat(category)和 widget 表。
cat 表中咱們每行數據會有一個 parent_id
字段,因此表內存在父子關係,而 widget
表中的每一行數據的 parent_id
字段的值必然是 cat
表中的最後一層。好比 Checkbox
widget
的 parent_id
的值就是 cat
表中 Button
的 id。
需求實現
在登錄的時候,咱們但願能獲取全部的組件樹,需求方要求結構以下:
[ { "name": "Element", "type": "root", "child": [ { "name": "Form", "type": "group", "child": [ { "name": "input", "type": "page", "display": "old", "extends": {}, "router": "/components/Tab/Tab" }, { "name": "input", "type": "page", "display": "standard", "extends": {}, "pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4" } ] } ], } ]
由於如今存在三方共建組件,並且咱們詳情頁也較FlutterGo 1.0 版本有了很大改動,現在組件的詳情頁只有一個,內容所有靠 md 渲染,在 md 中寫組件的 demo 實現。因此爲了兼容舊版本的 widget,咱們有 display
來區分,新舊 widget
分別經過 pageId
和 router
來跳轉頁面。
新建 widget 的 pageId 是經過FlutterGo 腳手架 goCli生成的
目前實現實際返回爲:
{ "success": true, "data": [ { "id": "3", "name": "Element", "parentId": 0, "type": "root", "children": [ { "id": "6", "name": "Form", "parentId": 3, "type": "category", "children": [ { "id": "9", "name": "Input", "parentId": 6, "type": "category", "children": [ { "id": "2", "name": "TextField", "parentId": "9", "type": "widget", "display": "old", "path": "/Element/Form/Input/TextField" } ] }, { "id": "12", "name": "Text", "parentId": 6, "type": "category", "children": [ { "id": "3", "name": "Text", "parentId": "12", "type": "widget", "display": "old", "path": "/Element/Form/Text/Text" }, { "id": "4", "name": "RichText", "parentId": "12", "type": "widget", "display": "old", "path": "/Element/Form/Text/RichText" } ] }, { "id": "13", "name": "Radio", "parentId": 6, "type": "category", "children": [ { "id": "5", "name": "TestNealya", "parentId": "13", "type": "widget", "display": "standard", "pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4" } ] } ] } ] } { "id": "5", "name": "Themes", "parentId": 0, "type": "root", "children": [] } ] }
簡單示例,省去 99%數據
代碼實現
其實這個接口也是很是簡單的,就是個雙循環遍歷嘛,準確的說,有點相似深度優先遍歷。直接看代碼吧
獲取全部 parentId 相同的 category (後面簡稱爲 cat)
async getAllNodeByParentIds(parentId?: number) { if (!!!parentId) { parentId = 0; } return await this.catService.getCategoryByPId(parentId); }
首字母轉小寫
firstLowerCase(str){ return str[0].toLowerCase()+str.slice(1); }
咱們只要本身外部維護一個組件樹,而後cat
表中的讀取到的每個parent_id
都是一個節點。當前 id
沒有別的 cat
對應的 parent_id
就說明它的下一級是「葉子」 widget
了,因此就從 widget
中查詢便可。easy~
//刪除部分不用代碼 @get('/xxx') async getCateList(ctx) { const resultList: IReturnCateNode[] = []; let buidList = async (parentId: number, containerList: Partial<IReturnCateNode>[] | Partial<IReturnWidgetNode>[], path: string) => { let list: IReturnCateNode[] = await this.getAllNodeByParentIds(parentId); if (list.length > 0) { for (let i = 0; i < list.length; i++) { let catNode: IReturnCateNode; catNode = { xxx:xxx } containerList.push(catNode); await buidList(list[i].id, containerList[i].children, `${path}/${this.firstLowerCase(containerList[i].name)}`); } } else { // 沒有 cat 表下 children,判斷是否存在 widget const widgetResult = await this.widgetService.getWidgetByPId(parentId); if (widgetResult.length > 0) { widgetResult.map((instance) => { let tempWidgetNode: Partial<IReturnWidgetNode> = {}; tempWidgetNode.xxx = instance.xxx; if (instance.display === 'old') { tempWidgetNode.path = `${path}/${this.firstLowerCase(instance.name)}`; } else { tempWidgetNode.pageId = instance.pageId; } containerList.push(tempWidgetNode); }); } else { return null; } } } await buidList(0, resultList, ''); ctx.body = { success: true, data: resultList, status: 200 }; }
彩蛋
FlutterGo 中有一個組件搜索功能,由於咱們存儲 widget
的時候,並無強制帶上該 widget
的路由,這樣也不合理(針對於舊組件),因此在widget
表中搜索出來,還要像上述過程那樣逆向搜索獲取「舊」widget
的router
字段
個人我的代碼實現大體以下:
@get('/xxx') async searchWidget(ctx){ let {name} = ctx.query; name = name.trim(); if(name){ let resultWidgetList = await this.widgetService.searchWidgetByStr(name); if(xxx){ for(xxx){ if(xxx){ let flag = true; xxx while(xxx){ let catResult = xxx; if(xxx){ xxx if(xxx){ flag = false; } }else{ flag = false; } } resultWidgetList[i].path = path; } } ctx.body={success:true,data:resultWidgetList,message:'查詢成功'}; }else{ ctx.body={success:true,data:[],message:'查詢成功'}; } }else{ ctx.body={success:false,data:[],message:'查詢字段不能爲空'}; } }
求大神指教最簡實現~🤓
收藏功能
收藏功能,必然是跟用戶掛鉤的。而後收藏的組件該如何跟用戶掛鉤呢?組件跟用戶是多對多
的關係。
這裏我新建一個collection
表來用做全部收藏過的組件。爲何不直接使用widget
表呢,由於我我的不但願表太過於複雜,無用的字段太多,且功能不單一。
因爲是收藏的組件和用戶是多對多的關係,因此這裏咱們須要一箇中間表user_collection
來維護他兩的關係,三者關係以下:
功能實現思路
-
校驗收藏
- 從
collection
表中檢查用戶傳入的組件信息,沒有則爲收藏、有則取出其在collection
表中的 id - 從
session
中獲取用戶的 id - 用
collection_id
和user_id
來檢索user_collection
表中是否有這個字段
- 從
-
添加收藏
- 獲取用戶傳來的組件信息
findOrCrate
的檢索collection
表,而且返回一個collection_id
- 而後將
user_id
和collection_id
存入到user_collection
表中(互不信任原則,校驗下存在性)
-
移除收藏
- 步驟如上,拿到
collection
表中的collection_id
- 刪除
user_collection
對應字段便可
- 步驟如上,拿到
-
獲取所有收藏
- 檢索
collection
表中全部user_id
爲當前用戶的全部collection_id
- 經過拿到的
collection_id
s 來獲取收藏的組件列表
- 檢索
部分代碼實現
總體來講,思路仍是很是清晰的。因此這裏咱們僅僅拿收藏和校驗來展現下部分代碼:
service
層代碼實現
@inject() userCollectionModel; async add(params: IuserCollection): Promise<IuserCollection> { return await this.userCollectionModel.findOrCreate({ where: { user_id: params.user_id, collection_id: params.collection_id } }).then(([model, created]) => { return model.get({ plain: true }) }) } async checkCollected(params: IuserCollection): Promise<boolean> { return await this.userCollectionModel.findAll({ where: { user_id: params.user_id, collection_id: params.collection_id } }).then(instanceList => instanceList.length > 0); }
controller
層代碼實現
@inject('collectionService') collectionService: ICollectionService; @inject() userCollectionService: IuserCollectionService @inject() ctx; // 校驗組件是否收藏 @post('/xxx') async checkCollected(ctx) { if (ctx.session.userInfo) { // 已登陸 const collectionId = await this.getCollectionId(ctx.request.body); const userCollection: IuserCollection = { user_id: this.ctx.session.userInfo.id, collection_id: collectionId } const hasCollected = await this.userCollectionService.checkCollected(userCollection); ctx.body={status:200,success:true,hasCollected}; } else { ctx.body={status:200,success:true,hasCollected:false}; } } async addCollection(requestBody): Promise<IuserCollection> { const collectionId = await this.getCollectionId(requestBody); const userCollection: IuserCollection = { user_id: this.ctx.session.userInfo.id, collection_id: collectionId } return await this.userCollectionService.add(userCollection); }
由於常要獲取 collection
表中的 collection_id
字段,因此這裏抽離出來做爲公共方法
async getCollectionId(requestBody): Promise<number> { const { url, type, name } = requestBody; const collectionOptions: ICollectionOptions = { url, type, name }; const collectionResult: ICollection = await this.collectionService.findOrCreate(collectionOptions); return collectionResult.id; }
feedback 功能
feedback 功能就是直接能夠在 FlutterGo 的我的設置中,發送 issue 到 Alibaba/flutter-go 下。這裏主要也是調用 github 的提 issue 接口 api issues API。
後端的代碼實現很是簡單,就是拿到數據,調用 github 的 api 便可
service
層
@inject() ctx; async feedback(title: string, body: string): Promise<any> { return await this.ctx.curl(GIHTUB_ADD_ISSUE, { type: "POST", dataType: "json", headers: { 'Authorization': this.ctx.session.headerAuth, }, data: JSON.stringify({ title, body, }) }); }
controller
層
@inject('userSettingService') settingService: IUserSettingService; @inject() ctx; async feedback(title: string, body: string): Promise<any> { return await this.settingService.feedback(title, body); }
彩蛋
猜想可能會有人 FlutterGo 裏面這個 feedback 是用的哪個組件~這裏介紹下
pubspec.yaml
zefyr: path: ./zefyr
由於在開發的時候,flutter 更新了,致使zefyr 運行報錯。當時也是提了 issue:chould not Launch FIle (寫這篇文章的時候纔看到回覆)
可是當時因爲功能開發要發佈,等了很久沒有zefyr
做者的回覆。就在本地修復了這個 bug,而後包就直接引入本地的包了。
共建計劃
咳咳,敲黑板啦~~
Flutter 依舊在不斷地更新,但僅憑咱們幾個 Flutter 愛好者在工做之餘維護 FlutterGo 仍是很是吃力的。因此這裏,誠邀業界全部 Flutter 愛好者一塊兒參與共建 FlutterGo!
此處再次感謝全部已經提交 pr 的小夥伴
共建說明
因爲 Flutter 版本迭代速度較快,產生的內容較多, 而咱們人力有限沒法更加全面快速的支持Flutter Go的平常維護迭代, 若是您對flutter go的共建感興趣, 歡迎您來參與本項目的共建.
凡是參與共建的成員. 咱們會將您的頭像與github我的地址收納進咱們的官方網站中.
共建方式
- 共建組件
-
本次更新, 開放了 Widget 內容收錄 的功能, 您須要經過 goCli 工具, 建立標準化組件,編寫markdown代碼。
-
爲了更好記錄您的改動目的, 內容信息, 交流過程, 每一條PR都須要對應一條 Issue, 提交你發現的
BUG
或者想增長的新功能
, 或者想要增長新的共建組件, -
首先選擇你的
issue
在類型,而後經過 Pull Request 的形式將文章內容, api描述, 組件使用方法等加入進咱們的Widget界面。
- 提交文章和修改bug
- 您也能夠將例如平常bug. 將來feature等的功能性PR, 申請提交到咱們的的主倉庫。
參與共建
關於如何提PR請先閱讀如下文檔
貢獻指南
此項目遵循貢獻者行爲準則。參與此項目即表示您贊成遵照其條款.
FlutterGo 期待你我共建~
具體 pr 細節和流程可參看 FlutterGo README 或 直接釘釘掃碼入羣
學習交流
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。
公衆號內回覆 【1】,加入全棧前端學習羣,一塊兒交流。
學習交流
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流呀~~