preface

FlutterGo probably doesn’t need much introduction.

If you’ve heard of someone for the first time, check out the FlutterGo website for a brief introduction.

FlutterGo had many updates in this iteration, and the author was responsible for the development of the back end and corresponding client side. Here is a brief introduction to the implementation of several functional modules in the FlutterGo back-end code.

Overall, the FlutterGo back end is not complex. This article introduces the implementation of the following functions (interfaces) :

  • FlutterGo login function
  • Component acquisition function
  • Collection function
  • Suggestion feedback function

Environmental information

Ali Cloud ECS cloud server

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 │ │ ├─ app_config. Ts │ │ ├─ ct.ts │ │ ├─ Collection User_collection. Ts │ │ └ ─ widget. Ts │ ├ ─ constants constant │ │ └ ─ but ts │ ├ ─ controller │ │ ├ ─ app_config. Ts │ │ ├ ─ Auth. Ts │ │ ├─ ├─ ─ bass Exercises. Auth. Ts │ │ ├─ bass Exercises Middleware middleware │ │ └ ─ auth_middleware. Ts │ ├ ─ model │ │ ├ ─ app_config. Ts │ │ ├ ─ the ts │ │ ├ ─ collections. Ts │ │ ├ ─ the ts │ ├─ ├─ ├─ ├─ all exercises, ├─ all exercises, all exercises, all exercises, all exercises, all exercises, all exercises, all exercises App_config. Ts │ │ ├ ─ the ts │ │ ├ ─ collections. Ts │ │ ├ ─ user. The ts │ │ ├ ─ user_collection. Ts │ │ ├ ─ user_setting. Ts │ │ └ ─ │ ├─ ├─ exercises, ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises │ ├ ─ ├ ─ impCopy the code

Log in function

First define a user table structure in class/user.ts, with the required fields, and declare the relevant interfaces in interface-.ts. This is the basic configuration for Midway and TS.

FlutterGo provides two login methods:

  • User name and password login
  • GitHubOAuthcertification

Because it is the mobile client GitHubOauth authentication, so there are actually some holes, I will talk about later. Let’s start with the easy one

User name/password login

Because we use a lot of username/password login way, so there needs to be listed under lot api:developer.github.com/v3/auth/,

The core part of the document: curl -u username https://api.github.com/user (you can test on a terminal), enter a password. So here we can get the user’s username and password for githu authentication.

The basic usage of Midway will not be repeated here. The whole process is very simple and clear, as shown below:

Related code implementation (related information has been desensitized: XXX) :

The service part

// Get the github config information @config()'githubConfig') GITHUB_CONFIG; // Get the request context @inject() CTX;Copy the code
//githubAuth Authentication 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
            }
        });
    }
Copy the code
Async find(options: IUserOptions): Promise<IUserResult> { const result = await this.userModel.findOne( { attributes: ['xx'.'xx'.'xx'.'xx'.'xx'."xx"],// Relevant information desensitizationwhere: { username: options.username, password: options.password }
            })
            .then(userModel => {
                if (userModel) {
                    return userModel.get({ plain: true });
                }
                return userModel;
            });
        return result;
    }
Copy the code
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;
        });
    }
Copy the code
Async create(options: IUser): Promise<any> {const result = await this.userModel.create(options);returnresult; } // 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;
        });
    }
Copy the code

controller

// inject Gets the service and the encryption string @inject('userService')
    service: IUserService

    @config('random_encrypt')
    RANDOM_STR;
Copy the code
The code implementation of the logic in the flowchartCopy the code

GitHubOAuth certification

There’s a pit! I’ll come back to you

GithubOAuth authentication is the creation of a github app

I still think the documentation class needs no introduction

Of course, I must have built it all up here, and then wrote some basic information into the server configuration

Or according to the above routine, let’s introduce the process first. And he’s talking about where the hole is.

Client side

The client part of the code is quite simple, it is a new webView to jump straight to the github.com/login/oauth/authorize bring client_id.

The server side

The overall process is 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
            }
        });
    }
Copy the code

The controller code logic is to call the data in the service to follow the information in the above flowchart.

Request of the pit

In fact, the Github app authentication mode is very suitable for the browser environment, but in Flutter, we are the newly opened webView requesting the Github login address. Failed to notify the Flutter layer when our backend returned successfully. Dart code in my own Flutter cannot get the return of the interface.

After searching for a number of solutions, I finally found a good one in the FLutter_webview_plugin API :onUrlChanged

Is, in short, because the client part of the new open a webView to request github.com/login github.com/login check after client_id with code clutter to the backend, such as the back-end check after the success, Redirect redirect a newly opened webView, and then flutter_webview_plugin listens for page URL changes. Send a corresponding event to destroy the current webVIew and handle the rest of the logic.

Part of Flutter code

OAuth event class UserGithubOAuthEvent{final String loginName; final String token; final bool isSuccess; UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess); }Copy the code

webView page:

/ / in initState url change monitoring, 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 successful}else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) {// Validation failedif(ApplicationEvent.event ! = null) { ApplicationEvent.event.fire(UserGithubOAuthEvent(' '.' '.true)); } flutterWebviewPlugin.close(); }});Copy the code

login page:

/ / event listener, page jump and reminding information processing ApplicationEvent. Event. On < UserGithubOAuthEvent > (), listen ((event) {if (event.isSuccess == true) {// oAuth authentication succeededif (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: 'Verification failed', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1, backgroundColor: Theme. Of (context). PrimaryColor, textColor: Colors. White, fontSize: 16.0); }});Copy the code

Component Tree Acquisition

Table structure

Before we talk about interface implementation, let’s take a look at what our table mechanism design looks like in terms of components.

The widget TAB below FlutterGO has many categories. Click on the widget TAB to enter the categories, click on the widget TAB to enter the categories, and click on the widget TAB to enter the details page.

Click on the module above to see the component widget

The widget is shown above. Click on the details page

So here we need two tables to keep track of their relationship: cat(category) and widget tables.

Each row in the CAT table has a parent_ID field, so there is a parent-child relationship in the table. The value of the parent_id field in each row in the Widget table must be the last layer in the CAT table. For example, the parent_id value of the Checkbox widget is the ID of the Button in the CAT table.

Need to implement

When logging in, we want to be able to obtain all the component trees. The demander requires the following structure:

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

Since there are components co-built by three parties now, and our detail page has been greatly changed compared with FlutterGo 1.0 version, there is only one component detail page now, all contents are rendered by MD, and the demo implementation of components is written in MD. Therefore, in order to be compatible with older versions of widgets, we have display to distinguish between the old widgets and the new widgets by using pageId and Router respectively to jump to the page.

The pageId of the new widget is generated by the FlutterGo scaffolding goCli

The current implementation actually returns:

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

Simple example, save 99% of the data

Code implementation

In fact, this interface is very simple, is a double loop traversal, to be precise, similar to depth first traversal. Let’s just look at the code

Get all categories with the same parentId (cat)

async getAllNodeByParentIds(parentId? : number) {if(!!!!!! parentId) { parentId = 0; }return await this.catService.getCategoryByPId(parentId);
}
Copy the code

Change the first letter to lower case

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

We simply maintain a component tree externally, and each parent_ID read from the CAT table is a node. If the current ID does not have the parent_id of another CAT, the next level of the widget is the “leaf” widget. easy~

// delete part of the 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{/ / not the cat under the table children, judge 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 };
    }
Copy the code

eggs

There is a component search function in FlutterGo, because when we store the widget, we do not force the route with the widget, which is not reasonable (for old components), so we search it in the widget table. Also reverse search to retrieve the Router field of the “old” widget as described 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 successful'};
            }else{
                ctx.body={success:true,data:[],message:'Query successful'}; }}else{
            ctx.body={success:false,data:[],message:'Query field cannot be empty'}; }}Copy the code

For the most simple realization of god’s advice ~🤓

Collection function

Collect a function, be to hook up with the user necessarily. Then how do you hook a favorite component to the user? Components have a many-to-many relationship with users.

Here I create a new collection table to use for all the favorites. Why not just use the widget table, because I personally don’t want the table to be too complex, too many useless fields, and too versatile?

Since there is a many-to-many relationship between the collected component and the user, we need an intermediate table user_Collection to maintain the relationship between the two. The three relationships are as follows:

Idea of Function implementation

  • Check the collection

    • fromcollectionCheck the component information passed in by the user in the table. If there is no component information, it is collected. If there is, it is taken outcollectionThe id in the table
    • fromsessionTo obtain the user ID
    • withcollection_iduser_idTo retrieve theuser_collectionWhether this field exists in the table
  • Add a collection

    • Get component information from the user
    • findOrCrateThe retrieval of thecollectionTable, and return onecollection_id
    • thenuser_idcollection_idDeposit touser_collectionTable (mutual distrust principle, check existence)
  • Remove the collection

    • Step above, getcollectionIn the tablecollection_id
    • deleteuser_collectionThe corresponding field is ok
  • Get the entire collection

    • retrievecollectionAll in the tableuser_idIs owned by the current usercollection_id
    • By means ofcollection_idS to get a list of favorite components

Partial code implementation

Overall, the thinking is very clear. So here we just take favorites and checksums to show part of the code:

Service layer 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);
    }
Copy the code

Controller layer code implementation

    @inject('collectionService') collectionService: ICollectionService; @inject() userCollectionService: IuserCollectionService @inject() ctx; // Check whether the component is favorites @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);
    }
Copy the code

Because the collection_ID field in the collection table is often retrieved, it is pulled out as a public 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;
    }
Copy the code

Feedback function

The feedback function is to directly send an issue to Alibaba/flutter-go in FlutterGo’s personal Settings. This is also called github issue API issues API.

Back end code implementation is very simple, is to get the data, call github API

The service layer

    @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,
            })
        });
    }
Copy the code

The controller layer

    @inject('userSettingService')
    settingService: IUserSettingService;

    @inject()
    ctx;

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

eggs

Some people may guess which component is used in the feedback in FlutterGo ~ here is an introduction

pubspec.yaml

  zefyr:
    path: ./zefyr
Copy the code

Zefyr failed because the Flutter was updated during development. “Chould not Launch FIle” was also mentioned.

However, it took a long time for the author of Zefyr to reply due to feature development. The bug was fixed locally, and the package was imported directly into the local package.

Build plan

Cough, knock on the blackboard ~~

Flutter is still constantly updated, but maintaining FlutterGo outside of work is a struggle for just a few of us Flutter enthusiasts. Therefore, all Flutter enthusiasts in the industry are cordially invited to join us to build FlutterGo!

Thanks again to all of you who have submitted your PR

To build that

Because the iteration speed of Flutter version is fast and there are many contents generated, our manpower is limited and we cannot support the daily maintenance iteration of Flutter Go more comprehensively and quickly. If you are interested in the construction of Flutter Go, you are welcome to participate in the construction of this project.

All members who participate in the joint construction. We will include your profile picture and Github address on our official website.

Build the way

  1. To build the component
  • This update opens up the ability to catalog Widget content by using goCli tools to create standardized components and write Markdown code.

  • In order to better record your change purpose, content information and communication process, each PR needs to correspond to an Issue, submit bugs you find or new functions you want to add, or add new co-built components.

  • First select your issue type and then use a Pull Request to add the article content, API description, and component usage to our Widget interface.

  1. Submit articles and fix bugs
  • You can also submit functional PR requests such as routine bugs, future features, etc. to our main repository.

Participate in the building

Please read the following documents about how to raise PR first

  • How do I submit a Pull Request to the repository
  • Dart code specification
  • How to create a Widget Page using go-CLI

Contribution to guide

This project follows the Contributor Code of conduct. By participating in the program, you agree to abide by its terms.

FlutterGo looks forward to our cooperation

For pr details and procedures, refer to the FlutterGo README or pin scan code directly into the group

Study and communication

Pay attention to the public number: [full stack front selection] daily access to good articles recommended. You can also join groups to learn and communicate with them