關於 FlutterGo 或許不用太多介紹了。前端
若是有第一次據說的小夥伴,能夠移步FlutterGo官網查看下簡單介紹.node
FlutterGo 在此次迭代中有了很多的更新,筆者在這次的更新中,負責開發後端以及對應的客戶端部分。這裏簡單介紹下關於 FlutterGo 後端代碼中幾個功能模塊的實現。mysql
整體來講,FlutterGo 後端並不複雜。此文中大概介紹如下幾點功能(接口)的實現:git
阿里雲 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 認證就是咱們常說的 github app 了,這裏我直接了當的丟文檔:creating-a-github-app
筆者仍是以爲文檔類的無需介紹
固然,我這裏確定都建好了,而後把一些基本信息都寫到 server 端的配置中
仍是按照上面的套路,我們先介紹流程。而後在說坑在哪。
客戶端部分的代碼就至關簡單了,新開 webView ,直接跳轉到 github.com/login/oauth/authorize
帶上 client_id
便可。
總體流程如上,部分代碼展現:
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 中的數據來走上面流程圖中的信息。
其實,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,處理剩餘邏輯。
//定義相關 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
表中的 idsession
中獲取用戶的 idcollection_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 功能就是直接能夠在 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界面。
關於如何提PR請先閱讀如下文檔
此項目遵循貢獻者行爲準則。參與此項目即表示您贊成遵照其條款.
具體 pr 細節和流程可參看 FlutterGo README 或 直接釘釘掃碼入羣
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流呀~~