On June 9, 2018, Deng Ruoqi, front-end development expert of user experience department of a large domestic e-commerce company, delivered a speech “GraphQL Engineering Practice based on SPA Architecture” in “Hangzhou first GraphQLParty — Collaborative value brought by GraphQL and Domain Drive”. As the exclusive video partner, IT mogul Said (wechat ID: Itdakashuo) is authorized to release the video through the review and approval of the host and the speaker.

Read the word count: 3838 | 10 minutes to read

Access to guest speech videos and PPT:
suo.im/5ebSDK


Abstract

The main speech mainly introduces GraphQL engineering practice based on SPA architecture, and analyzes the coordination efficiency of GraphQL in the whole link from the front-end perspective.

The philosophy of the GraphQL

GraphQL defines domain models through a set of schemas, officially known as SDL. It introduces a type system to constrain the model, such as the three types shown above.

In the actual application, the client sends the field to the server in the form of Schema text, and the server returns the data in JSON format after receiving and processing.

GraphQL provides a unified set of model definitions with flexible on-demand query capabilities. Another feature that is often overlooked is that the type system provides a description of the relationship between the models, showing that while the data is returned in JSON format, the actual application data is presented in a mesh architecture. This makes GraphQL a great choice for describing application data, hence its name.

Architecture design and technology selection

The front and back ends are separated from the front view

In my personal experience, anterior and posterior end separation can be divided into four stages.

In the first stage, the front-end asynchronous request data interface refreshes the local UI.

In the second stage, the front end takes over the View layer, which is the pattern used by many MVC-based frameworks.

In the third and fourth stages, with the rise of nodeJS technology, the collaborative efficiency of the front and back ends began to attract attention. Subsequently, BFF layer was introduced to enable the front end to iterate quickly, while the back end sank into service or micro-service.

The figure above is my technical selection scheme. React and Relay are the front-end. Relay is a data integration scheme based on GraphQL and React. The BFF layer introduces egg.js, which is ali open source Web framework for enterprise development.

How to design BFF

Restful layered design

Let’s take a look at the traditional MVC based Web Server processing of REST requests. The first request is to go to middleware, where some general logic, such as user logon status judgment or API authentication, is handled. Then enter the Router and distribute the requests to different controllers. The Controller layer calls the Model for business processing, and then the Model calls the Service layer to fetch data. Finally, the data is packaged and returned at the Controller layer.

Layered design based on GraphQL

The Router and controller are no longer needed after GraphQL is introduced, because GraphQL is not endpoint-based and its own resolver can encapsulate data. In this architecture, we introduced two modules connector and Schema Loader. On the one hand, the connector module makes a special cache design for some characteristics of GraphQL, and on the other hand, formulates the specification of front-end and back-end collaboration.

Build the schema

This is the GraphQL code I originally wrote, borrowed from the official repo with GraphQL-JS. Now it seems that there are two problems with this code. First, schema should be irrelevant to language but only a description of the model. Second, the principle of design first should be followed during development, and the model should be determined before the code is written.


Ideally, the model descriptions and relationships would be determined, then the resolver would be written to decide what to do, and then the Schema Loader would be used to bind them together when the application is loaded.

Authentication and authorization

The difference between authentication and authorization is that authentication is coarse-grained for general logic, while authorization is fine-grained for custom logic.


In GraphQL, authorization may target a certain field. As shown in the figure, query queries Xiaoming’s salary. Since the salary can only be viewed by oneself, a section of authorization logic should be added to resolver to ensure that the interrogator is himself. The idea here is to encapsulate authorization logic in the Model layer so that it can be reused across different Resolvers.

Cache design

The figure above shows two user records in the database. They are friends of each other, and two pieces of code are used to query the user and their friend.

This is the sequence diagram of the code request above. You can see that four requests were made, but only two data were retrieved.

After caching is introduced, requests from the second round can be found in the first round’s query cache.

You can optimize it further by combining the first round of requests from both pieces of code, which is the optimal solution.

To achieve this, you first need to use caching. Then there is the request queue, where all loads or queries in the same cycle are cached and then released in a single request in the next cycle. Finally, there is the ability to batch process requests with batch keys attached.

Facabook provides a batch processing solution called the DataLoader, which receives a method for processing batch keys with a cache below each instance of the DataLoader. The initial requirements after the introduction of DataLoader code are shown below.

The net effect of this code is to combine three requests into a single request, executing a single SQL statement on the back end.

However, the actual use of relational databases is a little more complicated. Generally, when we query the relational database, we will base on both PK (primary key) and UK (unique key). The above code queries about users by ID or Mobiles, forcing two DataLoader instances to be instantiated. Because these are different DataLoader instances, different caches are used, resulting in low cache utilization.

Therefore, I wrote the RDB-Dataloader module, so that PK and UK queries are in the same instance, to achieve the purpose of reuse cache. Notice the code in the red box, where a record is queried by name, and then a second query is performed on the record by ID. The second query is not issued, but will be cached. The core of the scheme is to cache all the fields of the record, and the control of the amount of data should be concerned by the paging logic.

DataLoader is request-level caching. The DataLoader instance is initialized when the request comes in and destroyed when the request is finished.

How do the front and back ends collaborate

Relay

The first thing you need to think about when using GraphQL as a front-end is the impact on browser performance, which is why you need to explore Relay further.

When using the React component, the most common appeal is to fetch data asynchronously and then render the data. The common approach is to add asynchronous fetch logic to componentDidMount. Therefore, in practical applications, as the page hierarchy deepens, the loading time will become longer, and the child component must wait for the parent component to complete the data load before starting rendering.

The simplest optimization for this is to put all the data required by the components in the first request, as shown above. However, WHEN I tried to add requirements later, I got a bug because IT was no longer clear which fields corresponded to which components.

Relay has a creatFragmenContainer method. You can pass the React component to this method and return the Relay component via GraphQL scheam. This approach not only implements dependency injection but also does not break the data encapsulation of components.

After embedding the fragment above in the original Query, we know which component issued the field.

The figure above is a piece of pseudocode that represents the underlying collaboration of Relay. The first object is a blog, which has content and an author, but the author is of type User. The blog does not store all the user’s data directly, but refers to the second object by reference. The same goes for the author of the comment and the blog it belongs to. The advantage of this is that whenever an object changes, all references to the object are updated synchronously.

Note the numbers 1, 2, and 3 in the figure, which are globally unique cache keys. Since all the data is in the cache, you can no longer use the ID in the database, otherwise you won’t be able to handle blogs and users with the same ID. There are various implementations of unique ids, using the base64 (type+ “:” + ID) form.

The global ID requires a backend to work with, defining the fromGlobalId and toGobalId methods. FromGlobalId is responsible for unpacking the ID from relay into the database ID, and toGobalId is responsible for packing the database ID on return.

When the client sends the schema text to the server and the server processes it, the amount of text is quite large, and the user experience will be very poor in a poor network environment.


Can you send the query ID directly and parse the text on the server? Fortunately, relay provides this approach. When the relay script is constructed, the module is injected with a hash to identify the current schema, using which the front and back ends are mapped together.

Problems that need to be solved

Note that this is not an infinite loop, it’s just an attacker deliberately writing a very complex nesting through your Query infinite to drain your server. Obviously setting the length of the Query text and the Query whitelist is not going to solve the problem. Instead, control the depth of the query.

For rate limiting flow, since GraphQL is not REST-based, it cannot be solved by limiting the number of route calls per minute. Instead, limit reads and writes, as shown in the example above, to a maximum of 20 comments per minute, via directive.

However, the actual implementation cost of the upper limit stream is relatively high. If you want to specifically implement the flow limiting function, you need to rely on some third-party services.