FlutterGo Backend Knowledge Point Extraction: midway+Typescript+mysql(sequelize)

Posted by PhantomCube on Wed, 11 Sep 2019 06:25:03 +0200

Preface

about FlutterGo Maybe not too much introduction.

If you have a buddy you first heard of, you can move around. FlutterGo official website Check out a brief introduction.

FlutterGo has many updates in this iteration. In this update, the author is responsible for developing the back end and the corresponding client part. Here is a brief introduction about the implementation of several functional modules in FlutterGo back-end code.

Overall, the FlutterGo back end is not complex. In this paper, the following functions (interfaces) are briefly introduced:

  • FlutterGo login function
  • Component acquisition function
  • Collection function
  • Suggestion Feedback Function

environmental information

Aliyun ECS Cloud Server

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

Development language: midway + typescript + mysql

Code structure:

src
├─ app
│    ├─ class Define table structure
│    │    ├─ app_config.ts 
│    │    ├─ cat.ts
│    │    ├─ collection.ts
│    │    ├─ user.ts
│    │    ├─ user_collection.ts
│    │    └─ widget.ts
│    ├─ constants constant
│    │    └─ index.ts
│    ├─ controller 
│    │    ├─ app_config.ts
│    │    ├─ auth.ts
│    │    ├─ auth_collection.ts
│    │    ├─ cat_widget.ts
│    │    ├─ home.ts
│    │    ├─ user.ts
│    │    └─ user_setting.ts
│    ├─ middleware 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 Toolset
│           └─ index.ts
├─ config Application configuration information
│    ├─ config.default.ts
│    ├─ config.local.ts
│    ├─ config.prod.ts
│    └─ plugin.ts
└─ interface.ts

Landing function

First, define a user table structure in class/user.ts, approximate fields needed, and declare relevant interfaces in interface.ts. This is the basic configuration of midway and ts, so we will not introduce them.

FlutterGo offers two ways of landing:

  • User name, password login
  • GitHubOAuth authentication

Because it's GitHubOauth authentication for mobile clients, there are actually some pits here, let's talk about later. Let's start with a simple start.

User name/password login

Because we use github's username/password login mode, we need to list the api of GitHub here: developer.github.com/v3/auth/,

The core part of the document: curl-u username https://api.github.com/user (you can test it on terminal) Enter your password on return. So here we can get the user's input username and password for githu authentication.

The basic usage of midway is not repeated here. The whole process is very simple and clear, as follows:

Relevant Code Implementation (Relevant Information Desensitized: xxx):

servicePart

    //Get userModel
    @inject()
    userModel
    
    // Get github configuration information
    @config('githubConfig')
    GITHUB_CONFIG;

    //Get the request context
    @inject()
    ctx;
    //githubAuth Certification
    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
            }
        });
    }
    // Find user 
    async find(options: IUserOptions): Promise<IUserResult> {
        const result = await this.userModel.findOne(
            {
                attributes: ['xx', 'xx', 'xx', 'xx', 'xx', "xx"],//Relevant information desensitization
                where: { username: options.username, password: options.password }
            })
            .then(userModel => {
                if (userModel) {
                    return userModel.get({ plain: true });
                }
                return userModel;
            });
        return result;
    }
    // Finding Users through 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;
        });
    }
    // Create user
    async create(options: IUser): Promise<any> {
        const result = await this.userModel.create(options);
        return result;
    }
    
    // Update user information
    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 gets service and encrypted string
    @inject('userService')
    service: IUserService

    @config('random_encrypt')
    RANDOM_STR;
Code Implementation of Logic in Flow Chart

GitHubOAuth authentication

There are pits here! Let me turn around.

Github OAuth authentication is what we often call github app. Here I lose documents directly: creating-a-github-app


I still feel that there is no need to introduce document classes.

Of course, I must have built all of them here, and then I'll write some basic information to the server side configuration.

Or follow the above routine, let's introduce the process first. Then say where the pit is.

Client Section

The client part of the code is fairly simple. Open a new webView and jump directly to github.com/login/oauth/authorize with client_id.

server terminal

The whole process is as follows, and some of the codes are shown as follows:

service

    //Get 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
            }
        });
    }

The controller code logic is to call the data in service to get the information in the flow chart above.

Pits in OAuth

In fact, the authentication method of github app is very suitable for browser environment, but in flutter, because we are the github landing address requested by the newly opened webView. When our back end returns successfully, we cannot notify the Flutter layer. This led to the code written by dart in my own Flutter, which could not get the interface back.

There are a lot of solutions to midbrain storms, which are finally being consulted. flutter_webview_plugin There's a good way to do this in API: onUrlChanged

In short, the Flutter client part opens a new webView to request github.com/login,github.com/login checks client_id and then comes to the back end with messy things like code. After the back-end verification is successful, redirect Flutter opens a new webView, and then flutter_webview_plugin listens to the changes in the url of the page. . Send related event s and let Flutter go to the current webVIew of destroy to process the remaining logic.

Flutter partial code

//Define relevant OAuth event s
class UserGithubOAuthEvent{
  final String loginName;
  final String token;
  final bool isSuccess;
  UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess);
}

webView page:

    //Monitor url changes in initState and 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();
        // Verify success
      } else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) {
        // Validation failed
        if (ApplicationEvent.event != null) {
          ApplicationEvent.event.fire(UserGithubOAuthEvent('', '', true));
        }
        flutterWebviewPlugin.close();
      }
    });

login page:

    //event Monitoring, Page Jump and Reminder Information Processing
    ApplicationEvent.event.on<UserGithubOAuthEvent>().listen((event) {
      if (event.isSuccess == true) {
        //  oAuth Certification Successful
        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('Access to identity information error:::$onError');
          setState(() {
            isLoading = false;
          });
        });
      } else {
        Fluttertoast.showToast(
            msg: 'Validation failed',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
            backgroundColor: Theme.of(context).primaryColor,
            textColor: Colors.white,
            fontSize: 16.0);
      }
    });

Component tree acquisition

Table structure

Before we talk about the implementation of the interface, let's first understand what our table mechanism design looks like about the components.

The widget tab under FlutterGO has many classifications, whether the classifications are entered or classified, then click on the components, and the component points are entered into the details page.


The component widget is shown in the module point above.


The image above is a widget, and the details page is clicked in.

So here we need two tables to record their relationships: cat(category) and widget tables.

In cat tables, we have a parent_id field for each row of data, so there is a parent-child relationship in the table, and the value of the parent_id field for each row of data in the widget table must be the last layer in cat tables. For example, the parent_id value of the Checkbox widget is the ID of the Button in the cat table.

Requirement realization

At the time of landing, we hope to get all the component trees. The structure of the requirement side is as follows:

[
   {
    "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"
               }
            ]
         }
      ],
   }
]

Because now there are three-party components, and our details page has changed a lot compared with FlutterGo version 1.0. Today there is only one component details page, the content is all rendered by md, write component demo in MD. So in order to be compatible with the old version of widgets, we have display ing to distinguish between the old and the new widgets by pageId and router respectively.

The pageId of the new widget is via FlutterGo scaffolding goCli Generated

At present, the actual return is as follows:

{
    "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": []
        }
    ]
}

Simple example, save 99% of data

code implementation

In fact, this interface is also very simple, is a double-loop traversal, to be exact, a bit similar to depth-first traversal. Look directly at the code.

Get all parentId identical categories (hereinafter referred to as cat)

async getAllNodeByParentIds(parentId?: number) {
    if (!!!parentId) {
        parentId = 0;
    }

    return await this.catService.getCategoryByPId(parentId);
}

Capital to lowercase

firstLowerCase(str){
    return str[0].toLowerCase()+str.slice(1);
}

We only need to maintain a component tree externally, and then each parent_id read from the cat table is a node. The absence of parent_id corresponding to other cats indicates that its next level is the "leaf" widget, so it can be queried from the widget. easy~

    //Delete parts without code
   @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 {
                // Without children under the cat table, determine whether there is a 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 };
    }

Egg

FlutterGo has a component search function, because when we store widgets, we do not force the widget to be routed, which is unreasonable (for old components), so we search in the widget table and retrieve the router field of the "old" widget as mentioned above.

My personal code implementation is roughly as follows:

    @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:'query was successful'};
            }else{
                ctx.body={success:true,data:[],message:'query was successful'};
            }
        }else{
            ctx.body={success:false,data:[],message:'Query field cannot be empty'};
        }
        
    }

Ask the Great God to teach us the simplest way to realize

Collection function

Collection function must be linked to users. Then how can the collection be linked to the user? Components have a many-to-many relationship with users.

Here I create a collection table to be used for all the collected components. Why not use widget tables directly, because I personally do not want the tables to be too complex, too many useless fields, and not a single function.

Since the component of the collection and the user are many-to-many relationships, we need an intermediate table user_collection to maintain the relationship between them. The three relationships are as follows:

Thoughts on Functional Realization

  • Check collection

    • Check the user's incoming component information from the collection table. If not, take out the id in the collection table for collection or for collection.
    • Get the user's id from session
    • Use collection_id and user_id to retrieve if there is this field in the user_collection table
  • Add collection

    • Getting Component Information from Users
    • findOrCrate retrieves the collection table and returns a collection_id
    • Then user_id and collection_id are stored in the user_collection table (mutual distrust principle, checking existence)
  • Remove collection

    • As above, get collection_id in the collection table
    • Delete the user_collection field
  • Get all the collections

    • Retrieve all collection_ids in the collection table that are all collection_ids for the current user
    • Get a list of collections by collecting_ids

Partial code implementation

Overall, the idea is still very clear. So here we just show part of the code by collecting and checking:

serviceLayer Code Implementation

    @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);
    }

Control Layer Code Implementation

    @inject('collectionService')
    collectionService: ICollectionService;

    @inject()
    userCollectionService: IuserCollectionService

    @inject()
    ctx;
    
    // Check whether the components are in collection
    @post('/xxx')
    async checkCollected(ctx) {
        if (ctx.session.userInfo) {
            // Already logged in
            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);
    }

Because the collection_id field in the collection table is often fetched, it is extracted here as a common method.

    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 function

The feedback function is to send issue directly to FlutterGo's personal settings. Alibaba/flutter-go Next. Here is also the Ti issue interface api that calls github issues API.

The back-end code implementation is very simple, that is to get the data and call the github api.

service level

    @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 layer

    @inject('userSettingService')
    settingService: IUserSettingService;

    @inject()
    ctx;

    async feedback(title: string, body: string): Promise<any> {
        return await this.settingService.feedback(title, body);
    }

Egg

Guess which component FlutterGo uses for this feed back. ~Here's an introduction

pubspec.yaml

  zefyr:
    path: ./zefyr

Because at the time of development, flutter was updated, resulting in zefyr Operational error reporting. At that time, issue was also mentioned: chould not Launch FIle (I didn't see a reply until I wrote this article.)

But at that time, due to the release of functional development, there was no response from the author of zefyr for a long time. The bug was fixed locally, and the package was introduced directly into the local package.

Co construction plan

Cough, knock on the blackboard.~~

Flutter is still updating, but it's still very hard for several of our Flutter enthusiasts to maintain FlutterGo after work. So here, all the Flutter enthusiasts in the industry are invited to participate in the co-construction of Flutter Go!

Thank you again. Small partners who have submitted pr

Co construction instructions

Because the Flutter version iterates faster and produces more content, and our limited human resources can not support the daily maintenance iteration of Flutter Go more comprehensively and quickly. If you are interested in the co-construction of flutter go, you are welcome to participate in the co-construction of this project.

We will include your avatar and github's personal address in our official website.

Co construction mode

  1. Co constructing components

    • This update opens up the Widget content collection function, you need to pass through goCli Tools, create standardized components, write markdown code.
    • In order to better record your changes, content information, communication process, each PR needs to correspond to an Issue, submit the BUG you found, or want to add new features, or want to add new ones. Co constructing components,
    • First select your issue in the type, and then add the article content, api description, component usage method and so on into our Widget interface in the form of Pull Request.

  1. Submit articles and fix bug s

    • You can also submit functional PR applications such as daily bug s, future feature s, etc. to our main warehouse.

Participation in co construction

Read the following documents about how to mention PR

Contribution Guide

This project follows Code of Conduct for Contributors . Participation in this project means that you agree to abide by its terms.

FlutterGo expects you and me to build it together~

Details and procedures of pr can be consulted FlutterGo README or Direct Nail Sweeping into Groups

Exchange of learning

Focus on Public Number: [Full stack front-end selection] Get good text recommendations every day. You can also join the group and learn to communicate with each other.~~

Topics: node.js github Session MySQL curl