Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FlutterGo 后台代码知识点提炼:midway+Typescript+mysql(sequelize) #54

Open
Nealyang opened this issue Sep 9, 2019 · 0 comments
Labels

Comments

@Nealyang
Copy link
Owner

Nealyang commented Sep 9, 2019

前言

关于 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 中声明相关接口。这里是 midwayts 的基础配置,就不展开介绍了。

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 widgetparent_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 分别通过 pageIdrouter 来跳转页面。

新建 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表中搜索出来,还要像上述过程那样逆向搜索获取“旧”widgetrouter字段

我的个人代码实现大致如下:

    @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_iduser_id 来检索user_collection表中是否有这个字段
  • 添加收藏

    • 获取用户传来的组件信息
    • findOrCrate的检索 collection表,并且返回一个 collection_id
    • 然后将 user_idcollection_id存入到 user_collection 表中(互不信任原则,校验下存在性)
  • 移除收藏

    • 步骤如上,拿到 collection 表中的 collection_id
    • 删除 user_collection 对应字段即可
  • 获取全部收藏

    • 检索 collection 表中所有 user_id 为当前用户的所有 collection_id
    • 通过拿到的collection_ids 来获取收藏的组件列表

部分代码实现

整体来说,思路还是非常清晰的。所以这里我们仅仅拿收藏和校验来展示下部分代码:

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个人地址收纳进我们的官方网站中.

共建方式

  1. 共建组件
  • 本次更新, 开放了 Widget 内容收录 的功能, 您需要通过 goCli 工具, 创建标准化组件,编写markdown代码。

  • 为了更好记录您的改动目的, 内容信息, 交流过程, 每一条PR都需要对应一条 Issue, 提交你发现的BUG或者想增加的新功能, 或者想要增加新的共建组件,

  • 首先选择你的issue在类型,然后通过 Pull Request 的形式将文章内容, api描述, 组件使用方法等加入进我们的Widget界面。

  1. 提交文章和修改bug
  • 您也可以将例如日常bug. 未来feature等的功能性PR, 申请提交到我们的的主仓库。

参与共建

关于如何提PR请先阅读以下文档

贡献指南

此项目遵循贡献者行为准则。参与此项目即表示您同意遵守其条款.

FlutterGo 期待你我共建~

具体 pr 细节和流程可参看 FlutterGo README 或 直接钉钉扫码入群

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还可以入群,一起学习交流呀~~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant