前言
关于 FlutterGo 或许不用太多介绍了。
如果有第一次听说的小伙伴,可以移步FlutterGo官网查看下简单介绍.
FlutterGo 在这次迭代中有了不少的更新,笔者在此次的更新中,负责开发后端以及对应的客户端部分。这里简单介绍下关于 FlutterGo 后端代码中几个功能模块的实现。
总体来说,FlutterGo 后端并不复杂。此文中大概介绍以下几点功能(接口)的实现:
- FlutterGo 登陆功能
- 组件获取功能
- 收藏功能
- 建议反馈功能
环境信息
阿里云 ECS 云服务器
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
mysql :mysql Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)
node:v12.5.0
开发语言:midway
+ typescript
+ mysql
代码结构:
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:'查询字段不能为空'};
}
}
求大神指教最简实现~
请发表评论