laitimes

Front-end unified request library design and implementation

author:Flash Gene

Preface

For a front-end engineer, the most common requirement scenario that he faces every day is to call the back-end interface, but because of well-known reasons, the front-end currently has countless ways to call the interface, for example: there were tools based on XHR, Axios, Fetch for encapsulation, and everyone tried to call the unified interface, but they seem to need to be transformed in the end. Therefore, we try to develop a set of tools that can synthesize the advantages of the above tools on Bilibili, and combine the tools needed by Bilibili to launch a unified request library with unified error handling, reducing code redundancy, smoothing out style differences, reducing document burden, optimizing code prompts, etc.

background

Why do you need a unified request library?

As a R&D, we will face a variety of business needs and technical scenarios, which leads to the need to make differentiated designs and packages for a large number of interface calls, and then mix the differences in developer styles and historical problems, which will lead to various problems.

Here are a few typical questions:

  1. Code redundancy or excessive maintenance costs: Due to historical factors and business requirements, there are multiple versions of request libraries in various teams and repositories, such as custom processing logic for SSR and CSR, Vue2 and Vue3-based encapsulation, and in-device or web-compatible request processing. The modules are similar but different between these libraries, resulting in complex maintenance and scalability;
  2. Performance issues: The company's previous request inventory was overloaded with features, resulting in too much code and possibly too large volume, which affected page performance.
  3. Front-end and back-end cannot co-evolve: At present, the company's back-end basic/general capabilities have followed a unified standard, so whenever the back-end provides a basic capability that needs front-end access, the front-end is scattered and there is no unified standard, and the consequence is that different front-end projects correspond to a common back-end capability that needs to be developed separately, and the iteration cost is high, which is also the most serious problem, which hinders the front-end and back-end co-evolution;

In order to solve these problems, we design a unified request library that can solve the above problems, and reduce the package size as much as possible through the design of some middleware patterns.

Comparison of the current situation and capabilities

We researched some of the technical solutions that the community was already familiar with to see if they met our needs, and dissected their strengths and weaknesses to conclude if there was a better solution.

Although there are mature request libraries such as Axios in the market, which provide a certain degree of extensibility through mechanisms such as interceptors, they are still insufficient in terms of multi-terminal adaptation, middleware management, dynamic configuration, etc. For example, while Axios is popular in web development, it has limitations in its compatibility with embedded H5 pages in native apps, as well as its cross-platform flexibility. In contrast, our newly developed unified request library based on the middleware pattern can provide a more flexible configuration and extension mechanism, which can not only dynamically manage all aspects of the request process, but also better adapt to the needs of different platforms and application scenarios.

Front-end unified request library design and implementation

(client-server中请求库示意图)

Let's take a look at XHR and fetch natively provided by browsers, the history of XHR needs not be repeated, but we believe that fetch can go further as a "next-generation Ajax" standard. But at the same time, fetch currently only has the core ability to initiate requests, and it is not directly responsible for these parts in terms of pre- and post-request processing, because we can use it as part of the framework, i.e. one of the standards, because this can make more teams adapt and accept the convention.

The community doesn't seem to be able to cover our scenario perfectly, so let's try to define a protocol first, and since we already know a lot about service frameworks such as KOA, and their design patterns have inspired us, we decided to use the middleware pattern as our basic generic protocol to implement our "unified request library".

target

First, let's define the Unified Request Library:

⭐️ A standard, flexible, and powerful toolset ⭐️ for service invocation

We mainly look at the problems that the unified request library can solve from the following aspects:

1. Standardization and unification: Unify the interface invocation methods in different front-end repositories, so as to reduce the behavioral differences and problems caused by the use of different request libraries;

2. Complete scene optimization: multi-platform proxy optimization (the request library of H5 scenarios is embedded in the app) completely solves historical problems such as excessive volume;

3. Implement one-click global capability registration;

4. Integration infrastructure: The unified request library is integrated with the basic technology ecology of the whole team to form an ecosystem.

Design ideas and implementation

Pattern selection ideas

In order to solve the complex business scenario of Bilibili, we first split the application into individual scenarios. In each single scenario, we want a singleton to correspond to a scenario, and we need to provide the ability to integrate plug-in logic considering the flexibility of the business.

The first thing we identified was the need for object-oriented development. Secondly, we put aside the factory integration solution, which has been verified to a certain extent for the time being because of the shortcomings of this historical solution inside station B. We have to affirm Axios's position as a leader in the front-end domain network request library, and refer to the interceptor part of Axios that we think is valuable, and the inspiration of the middleware form brought by Koa, and combine the two, which gives rise to the middleware pattern in the front-end space. According to the initial demand scenario relationship, there is such a diagram to clarify the working principle.

Front-end unified request library design and implementation

(Demand Scenario Diagram)

This is the principle that the request library is based on object-oriented design, provides flexible plug-in capabilities, and at the same time maintains a unified logical form according to the scenario.

Middleware pattern

In the Unified Request Library, middleware provides us with plug-in granular integration capabilities. The middleware pattern works by breaking down the request processing process into a series of independent functional units, each of which is responsible for handling a specific aspect of the request, such as logging, error handling, data transformation, and so on. These middleware form a processing chain in a predetermined order, and the request and response data pass through each middleware in turn, and each middleware can process, modify, or directly terminate the request. This pattern is inspired by the onion model of the Koa framework, where requests and responses are processed by each middleware as if they were passing through each layer of an onion. This not only enhances the flexibility and scalability of request processing, but also enables each middleware to be developed and tested independently, greatly improving the maintainability of the code. Under a certain order, these middleware are combined into a request logic, and we also choose the onion pattern for this order. Before we start with our request library example, let's talk about the "onion model".

What is the Onion Model?

Imagine the structure of an onion – made up of many layers, each peeling off one reveals the next. In the onion pattern, each middleware is like a layer of an onion, and requests are passed from the outermost layer, through each layer of middleware until the core processing logic is reached, and then back layer by layer, with each layer having the opportunity to process the request and response.

Front-end unified request library design and implementation

Koa's onion model

Our unified request library also has its own call chain based on the onion model:

Front-end unified request library design and implementation

Request an onion model for the library

The Onion Pattern is also a Chain of Responsibility Pattern, which has obvious advantages over the traditional large factory pattern, which minimizes coupling, enhances the flexibility of each node, and has extremely clear responsibilities.

Front-end unified request library design and implementation

Of course, this model also has certain disadvantages, such as the difficulty of debugging and the management of the chain, which requires a good team style specification to constrain.

Therefore, we need to agree on the handling principles of the request library middleware to avoid these problems as much as possible, for example, we stipulate that we are allowed to edit the request object in the context instead of the config object.

Extensibility of middleware

In terms of segregation of responsibilities, we have a clear independent processor. Therefore, before the responsibilities are clarified, it is also essential to do a good job in the expansion of the basic agreement. In order to achieve a unified request library, we need to make its parts add, subtract, replace, and expand.

Base model

Providing a middleware base abstraction class as a prototype for all plugins is the most important step in object-oriented development, as well as providing type checking to prompt developers to implement the necessary interfaces.

Overrides preset behaviors

Starting from the built-in middleware, we design to replace the target built-in middleware by indexing with the same name, and users can not only create middleware with the same name in a custom way, but also quickly expand the built-in capabilities by inheriting or directly calling the static methods of these middleware.

Also, we distinguish between Fetch middleware and all other middleware, because there is a fundamental difference between the two categories. All the other middleware is arranged in order, first global and then temporary, and the last, which is also the center of the onion, is the Fetch middleware, so we also make different interface distinctions when it comes to overriding these preset behaviors.

For example, when instantiating, we designed the interface to pass in a middleware list by default, because modifying Fetch is a low-frequency behavior, and when passing in object configurations, the user can clearly separate the two.

...
    /**
     * 初始化
     * @param initConfig 初始配置,参考interface
     */
    (initConfig?: IHttpServiceInit | IHttpSvcMiddleware[]);   
    ...
}
    
// interface
export interface IHttpServiceInit {
  baseURL?: FetchBaseURL
  fetch?: IHttpSvcMiddleware
  middlewares?: IHttpSvcMiddleware[]
}           

Scoping development

We have come up with the concept of "global" and "temporary" scope. The scope of the global middleware is determined at the time of instantiation, and whether the middleware logic really acts on every request initiated by the instance is actually in the hands of the user. By deriving middleware based on the underlying abstract class, this pattern allows the user to define more of the behavior of the subclasses, which is also part of extensibility in a sense.

For example, users can actually implement Provide/Inject, i.e., inject first, then use on demand, depending on whether you enable the feature by default when composing the internal logic of the middleware, so that you can allow the logic switch to be turned on by an activated behavior when subsequent calls are requested.

const globalMeta = (ctx, next, config) => {
    if (config?.payload?.active) {
        ctx.request.params["meta"] = {
            platform: "web",
            device: ""
        }
    }
    await next()
}
class GlobalMeta extends Middleware {
    name = "GLOBA_META"
    constructor() {
        super(globalMeta)
    }
}


const httpSvc = new HttpService([new GlobalMeta()])
// 默认该能力注册,但是没有激活标识不会工作。


httpSvc.with("GLOBAL_META", { active: true })
// 通过指定激活全局注册过的中间件名来使其工作(使活跃)           

From these goals, we can find a one-to-one correspondence:

  1. Index key – the name of the middleware
  2. 全局注册方式——Register API
  3. 临时携带处理者及状态——With API
  4. By indexing the name of the middleware, the Disable API is temporarily disabled

Package

We consider middleware as a stand-alone package, rather than as a sub-export member of the request library framework, because it can reduce the understanding cost of making middleware for the user, and an important condition for allowing this is that the basic form of the middleware has been confirmed.

controller

We have designed three control modules for the request library:

1. ConfigCtrl,配置控制器

2. AssembleCtrl,装配控制器

3. RequestCtrl,请求控制器

If you use one sentence to connect these three modules: when you initiate a request with the request library, call the assembly controller to collect all the temporary middleware and Payload (payload), and at the same time store the Payload in the configuration controller, and after the collection is completed, the request method is generated by combining middleware logic, and the configuration controller will generate the middleware configuration context, and these materials are collected to the request controller together, creating the request context and initiating the request.

Through dependency injection, three modules with distinct responsibilities are related to each other:

Front-end unified request library design and implementation

Overall design drawing

装配控制器(AssembleCtrl)

As the name suggests, the assembly controller is responsible for assembling a variety of objects, including:

1. Middleware

2. Temporary Payload

It can either assemble Payload for the registered global middleware, or mount Payload with temporary middleware; In addition to increasing, it can also decrease; We also provide a disabling feature to disable middleware that has been registered globally;

This increase and decrease can flexibly control the active state of all middleware used to initiate the request; In addition, the design of temporary Payload is more intuitive and clear than the traditional pattern of a large Config object, which effectively avoids the unmaintainable trend of infinite increase in configuration members, and compared with the built-in logic of large objects, our middleware logic is hot-swappable.

配置控制器(ConfigCtrl)

The configuration controller generates the middleware context for each request, where all configurations are recorded, participates in various configuration configurations at assembly time, and injects configurations into the middleware at runtime.

请求控制器(RequestCtrl)

In the mode with the assembly controller as the core, all conditions are prepared, the request controller is called when the request is initiated, and the request controller is used to generate the assembled request function (RequestFunction), and the request is executed after the middleware configuration context and the initial request configuration are generated.

brief summary

As the most important controller of the chain mode, the assembly controller also provides a direct API for the HTTP Service, and the core of its circular chain call capability requires the implementation of a dispatcher.

With this scheduler, the workflow of the three controllers when a request is scheduled is as follows:

Front-end unified request library design and implementation

Scheduling flowchart

Implement combinatorial permutations

Through the scheduling of the scheduler, whether it is adding Payload or setting disable, we must have a clear set of organizational rules to integrate the resources obtained after scheduling in an orderly manner. As mentioned above, we set the tone for the whole first, then the temporary, which is also intuitively regular.

The timing of global registration is relatively early, and this time is much earlier than the part that is temporarily carried, so there is a saying that the global registration capability is in front. Of course, thinking of this, we also consider that users may also have middleware sequences that want to operate globally in a fixed position, so we can continue to expand upwards, so we have the logic of global middleware privilege escalation.

Front-end unified request library design and implementation

Global privilege escalation

achievement

Direct benefits

By accessing the unified request library, each team can quickly integrate by simply installing the NPM package when accessing the company-level common capabilities, which greatly reduces the development resources required. In terms of adapting to multiple platforms, we provide a dedicated Fetch middleware package to replace the old SDK, which liberates the trapped old packaging form, stabilizes the style of middleware expansion and integration, and lays a solid foundation for building a healthy and sustainable front-end technology ecosystem.

Front-end unified request library design and implementation

Value model

Side-by-side comparison

Let's say we call an API and we want to implement:

1. Encode the input parameters

2. The request needs to be reported (success or failure)

3. You need to get a tag in the headers

4. The interface returns a non-JSON format (e.g. plain text)

5. 服务端渲染请求时需要透传 headers(user-agent,ip 等 kv)

The traditional way

// 传统 API 形式
// http 内部需要实现 encode,responseType 等逻辑
http.request({
    url: '/xxx',
    params: { id: 1 },
    encode: true,
    report: true,
    responseType: 'text',
    responseAll: true,
    headers: {
        ...(typeof window === 'undefined' ? context.headers : {})
    },
})           

Every time you need to add a config key, you have to go deep into the http kernel to add an if case, and the kernel will get bigger and bigger as it iterates until it is hard to decouple.

Unified request library mode

// 请求库
httpSvc
    .with(encodeHandler),
    .with(reportHandler),
    .with('RES_DATA', { type: 'text' }) // 对应 responseType
    .disable('RES_EXTRACT') // 负责从 response 对象中取得data数据,我们此举会禁用该中间件从而实现获取到整个响应对象,对应上面的responseAll的输入
    .with('SERVER_SIDE', { headers: context.headers || {} })
    .request({
        url: '/xxx',
        params: { id: 1 },
    })           

Compared with the traditional way, which adds a configuration key for each additional function, the request library calls the specified middleware one by one in a chained way, which is intuitive and clear, and fully decoupled and easy to maintain.

epilogue

As HttpService continues to mature and improve, it has become one of the indispensable foundational capabilities for us to handle front-end network requests. Its flexibility and scalability greatly simplify front-end development, allowing us to focus more on creating a great user experience. As technology continues to advance, we believe that the middleware-based request library will continue to lead the innovation of front-end request processing and bring more possibilities to developers.

With the above introduction, I believe you already understand the basic capabilities of how to use the request library, if you want more examples, please go to our Github() site, where you will get:

  1. Built-in middleware definitions and descriptions
  2. Public middleware that has been published by the community
  3. More complete design introduction, scheme comparison
  4. N ways to play bad middleware

Author: Big Frontend

Source-WeChat public account: Bilibili Technology

Source: https://mp.weixin.qq.com/s/NNeTDuTUbbrM4G_cqUhpFg

Read on