FlutterGo 后端知识点提炼:midway+Typescript+mysql(sequelize)
比较尴尬,组件树的 json 改为了截图形式。需要复制的可点击阅读原文查看
前言
关于 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
userModel
// 获取 github 配置信息
GITHUB_CONFIG;
//获取请求上下文
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 和加密字符串
service: IUserService
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生成的
目前实现实际返回为:
简单示例,省去 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
层代码实现
'collectionService')
collectionService: ICollectionService;
()
userCollectionService: IuserCollectionService
()
ctx;
// 校验组件是否收藏
('/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
层
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
层
settingService: IUserSettingService;
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请先阅读以下文档
如何向仓库提交 Pull Request
dart 代码规范
如何使用go-cli 创建 Widget Page
贡献指南
此项目遵循贡献者行为准则。参与此项目即表示您同意遵守其条款.
FlutterGo 期待你我共建~
具体 pr 细节和流程可参看 FlutterGo README 或 直接钉钉扫码入群
完
往期精彩回顾前端工程师为什么要学习编译原理深入理解 Node.js 的进程与线程FlutterGo2.0 强势归来
戳“阅读原文”,获取原文地址(能查阅外链)“在看”的永远18岁~