关于 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 或 直接钉钉扫码入群
关注公众号: 【全栈前端精选】 每日获取好文推荐。
公众号内回复 【1】,加入全栈前端学习群,一块儿交流。