Three stages of Vuex calling interface

3.stages of Vuex calling interface

Editor's note: Li Songfeng, the author of this article, is a senior technical book translator. He has translated and published more than 40 technical and interactive design monographs. He is currently a senior expert in Web front-end development of 360 Qi Dance Group, a member of 360 Front-end Technical Committee and W3C AC representative.

This article originated from a "pan-front-end sharing" the author made within the company on March 11th. It is a summary of the author's experience in the development of IoT smart device linkage scenarios. The code in the article can be regarded as pseudo-code and does not contain any content involving real projects.

Vuex is an indispensable tool for developing complex Vue applications, and provides a solution suitable for Vue itself for sharing data across components. For a detailed introduction to Vuex, it is recommended to read the official website document: https://vuex.vuejs.org/.

The three stages of the Vuex calling interface generally reflect the process of optimizing the calling logic, reorganizing the code, and abstracting implementation details during the iteration of the project.

  • Separation of Concerns and Maintainable Code: Separation of Concerns (SoC, Separation of Concerns) is an important principle of software architecture design, which is embodied in the division of modules with a single responsibility as the goal, and the creation of mutual independence through logical classification and grouping But organically unified code entity. The code with separation of concerns has clear module responsibilities and clear relationships, which is convenient for troubleshooting and maintenance, and is the basis of the overall maintainability of the code.
  • Olive-shaped interface and isomorphic mapper: The olive-shaped interface is a metaphor for calling a service. The entrance and exit are small, but the connotation logic is rich. This kind of interface converges to the outside, simple and direct, but encapsulates the main logic inside, facilitating centralized processing of requests and responses. Isomorphism mapping (isomorphism mapping) is a mathematical concept, borrowed here to describe custom service mapping consistent with Vuex auxiliary methods.
  • Response normalization and three-level error handling: The purpose of response normalization (normaliztion) is to unify the format of data returned by different server interfaces and the response format of request errors. The three levels of error handling are network errors, service errors, and interface errors. These errors can be returned as responses in a normalized form, or they can be synchronously set to the Vuex state object, and become responsive data that is reflected in the interface component in real time.

Create a presentation environment

qvk is a general-purpose web development environment that integrates best practices of modern front-end engineering. It can be used to develop traditional C/S architecture web applications, SPA (single page application), H5 (app embedded page), etc.

The initial version of qvk integrates the following web frameworks and packaging tools.

  • ThinkJS: A simple, easy-to-use and powerful Node.js development framework based on the MVC model.
  • Vue.js: Progressive JavaScript framework, the mainstream choice for front-end component development.
  • Webpack: Currently the most widely used front-end resource module packaging tool.

usage

1. Copy the code

git clone git@github.com:qqvk/qvk.git

2. Install dependencies, initialize and start services

cd qvk//enter the project directory npm install//install dependencies npm run init//initialize npm start//start the project

The first stage: separation of concerns and maintainable code

The left side of the above figure shows the Vuex architecture, and the right side shows the dependencies of the demo environment code modules. The following is the code of the corresponding files, mainly lib/service1.js and store/store1.js, representing the first stage:

lib/endpoints.js

/** * Default export API configuration*/export default {//1. Get user device list getUserDeviceList: {method:'GET', endpoint:'/getUserDeviceList'},//2. Get user scene list getUserSceneList: {method :'GET', endpoint:'/getUserSeceneList' },//3. Query scene getUserScene: {method:'GET', endpoint:'/getUserScene' }}/** * Name export global environment*/export const ENV = 'https://the-service-address'

lib/factory.js

import API, {ENV} from'./endpoints'
/** * Default export factory method: return the requested url and options according to the service name and service parameters */export default ({ serviceName ='', serviceArguments = {} }) => {const {method, endpoint} = API[ serviceName] const urlBase = `${ENV}${endpoint}` const {queryString, headers} = getQueryStringAndHeaders(serviceArguments)
    let url, body, options if (method =='POST') {url = urlBase; body = queryString options = {headers, method, body}} if (method =='GET') {url = `${urlBase} ?${queryString}` options = {headers, method}} return {url, options }}/** * getQueryStringAndHeaders() and other Helpers (omitted) */

lib/service1.js

import factory from'./factory'
export default {//Get user scene list async getUserSceneList() {const {url, options} = factory({ serviceName:'getUserSceneList'}) const res = await fetch(url, {...options }).then(res => res.json()) return res },//Get the user device list async getUserDeviceList() {const {url, options} = factory({ serviceName:'getUserSceneList'}) const res = await fetch(url, {. ..options }).then(res => res.json()) return res },//Get user scene details async getUserScene({ scene_id }) {const {url, options} = factory({ serviceName:'getUserScene' , serviceArguments: {scene_id} }) const res = await fetch(url, {...options }).then(res => res.json()) return res }}

store/store1.js

import SERVICE from'../lib/service1'
const store = {state: {}, actions: {getUserSceneList() {return SERVICE['getUserSceneList']() }, getUserScene(store, {scene_id}) {return SERVICE['getUserScene']({scene_id})}} }
export default new Vuex.Store(store)

In the first phase, service1.js and store1.js achieved separation of concerns. The former is responsible for requesting back-end APIs, and the latter is responsible for mapping interfaces between Vue components and services. The problem at this stage is the duplication of code logic: the internal logic of the three interface calls exported by service1.js is almost the same (except that getUserScene() needs to receive a parameter), and the logic mapped in actions in store1.js is also repeated.

The second stage: olive-shaped interface and isomorphic mapper

The second stage is to solve the problems of the first stage. 1. extract the repetitive logic to construct an "olive-shaped" interface.

The first step to refine the repetitive logic is to create a new serve() function, and then call serve() in each interface. The result is of course repeated: each interface calls serve() repeatedly. The second step is to integrate all interface calls, and achieve the purpose of "converging" the interface by dynamically generating each interface.

There are two ways to implement the convergence interface: the first is to dynamically generate export objects, and the second is to use a proxy to dynamically intercept requests. See the code for details:

lib/service2.js

import factory from'./factory'//import API from'../lib/endpoints'
function serve({ serviceName ='', serviceArguments = {} }) {const {url, options} = factory({ serviceName, serviceArguments}) return fetch(url, {...options }).then(res => res .json())}//The second implementation method export default new Proxy({}, {get(target, serviceName) {return serviceArguments => serve({ serviceName, serviceArguments}) }})
//The first way to implement//export default Object.keys(API).reduce((pre, serviceName) => {//pre[serviceName] = serviceArguments => serve({ serviceName, serviceArguments})//return pre//}, {})

As shown above, service2.js solves the problem of service1.js, eliminates duplicate code, and converges all interfaces to only 4-5 lines of code. These few lines of code are the total entry and exit of all requests. These are the "olive" pointed ends.

Next, transform store1.js by customizing the isomorphic mapper (see the previous section). The so-called isomorphic mapper is the same mapping function constructed with the built-in mapActions and mapMutations auxiliary methods of Vuex. By customizing these mapping functions, you can extract the original repetitive code and realize the registration of custom services in Vuex in the form of function declarations, which is the same as using Vuex in Vue components:

store/store2.js

import {mapActions, mapMutations} from'./store2.mapper'
const store = {state: {}, mutations: {...mapMutations({ getUserSceneList:'scenes', getUserScene:'scene', getUserDeviceList:'devices' }) }, actions: {...mapActions(['getUserSceneList ','getUserScene','getUserDeviceList' ]) }}
export default new Vuex.Store(store)

In order to implement the above store2.js call, a module store2.mapper.js needs to be added:

store/store2.mapper.js

import SERVICE from'../lib/service2'
//Name the export isomorphic action mapper export const mapActions = API => API.reduce((pre, serviceName) => {pre[serviceName] = ({ commit }, serviceArguments) => {return SERVICE[serviceName](serviceArguments ).then(res => {//Submit the asynchronous response data to the mutation commit(serviceName, res.data) return res })} return pre}, ())//Name the export isomorphic mutation mapper export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {pre[serviceName] = (state, data) => {//mutation adds data to state state according to configuration[MAP[serviceName]] = data} return pre}, {})

In the second stage, the two endpoints of the "olive" interface are implemented in service2.js with a few lines of code through refactoring, and the internal logic of the "olive" will be enriched in the third stage. In addition, the second stage simplifies the Vuex core code through a custom isomorphic mapper, and the newly added store2.mapper.js provides a gateway for the third stage to achieve response normalization.

The third stage: Respond to one and three-level error handling

As mentioned earlier, the newly added store2.mapper.js in the second stage provides a gateway for the realization of response normalization in the third stage, which is the place to write code. There is no difference between store3.js and store2.js in the third stage, but the new store3.mapper.js is referenced:

store/store3.js

//Except for the reference to store/store3.mapper.js, the rest is the same as store/store2.js

There are normal responses as well as abnormal responses and errors to the response returned by the backend request. In addition, if different server interfaces are to be called in the project, the data format returned by these interfaces may be more or less different. In order to achieve more consistent response and error handling on the front end, it is necessary to normalize these "responses", that is, customize a standard response format. As shown below, store3.mapper.js borrows the response format of the fetch request and uses {ok: true/false, payload: data/error} as the normalized format:

store/store3.mapper.js

import SERVICE from'../lib/service3'
export const mapActions = API => API.reduce((pre, serviceName) => {pre[serviceName] = ({ commit }, serviceArguments) => {return SERVICE[serviceName](serviceArguments).then(res => {commit (serviceName, res.data)//Response normalization: normal response if(res.code === 0) {return {ok: true, payload: res}}//Response normalization: abnormal response and each Kind of error return {ok: false, payload: res} })} return pre}, {})
export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {pre[serviceName] = (state, data) => {state[MAP[serviceName]] = data} return pre} , {})

The normalization of the response is handled at the store3.mapper.js layer because the normalization not only covers normal and abnormal responses, but also covers errors. The errors we are talking about can be roughly divided into three categories or three levels:

  • Network errors, including disconnection, weak network, etc., disconnection will cause the request to fail immediately, and the weak network will cause the request to time out;
  • System errors, usually caused by back-end services failing to provide responses normally, such as service offline;
  • Interface error refers to the interface returning an error response due to the request itself.

The following is the code of service3.js to implement three-level error handling, including two ways to achieve timeout: use AbortController to timeout to interrupt the request and use the packaging contract (promise) to take over the fetch response, and then timeout to reject the promise (reject promise).

lib/service3.js

import factory from'./factory'
function serve({ serviceName ='', serviceArguments = {} }) {let {url, options} = factory({ serviceName, serviceArguments})
    const controller = new AbortController() const signal = controller.signal
   //Timeout implementation method 1: AbortController//setTimeout(() => {//controller.abort()//}, 10)//return fetch(url, {...options, signal }).then(/*...*/).catch(/*...*/)
   //Timeout implementation method 2: wrap Promise (and AbortController interrupt request) return new Promise((resolve, reject) => {TIMEOUT_GUARD({ reject, controller })
       //url ='/index/s500' simulates server internal error response fetch(url, {...options, signal }).then(resolve, reject) }) .then(res => {const {ok, status, statusText} = res if(ok) {return res.json().then(res => {const {code, msg, reqid} = res if (code === 0) {return res} else {//third Level error: the interface returns an error return {code: 9001, error: res}} })} else {//Level 2 error: remote service error return {code: 8001, error: {status, statusText}}} }). catch(error => {//First level error: network error (timeout or disconnection)//If you use the first timeout implementation method, there will be this error if(error.name === 'AbortError') {return {code: 7001, error}}//If the second timeout implementation method is used, the error will be returned directly without wrapping if(error.code === 7002) return error//other request errors, For example, network interruption is unified into one encoding return {code: 7000, error} })//The second timeout implementation method defines the auxiliary function of the timeout rejection agreement (promise) function TIMEOUT_GUARD({ reject, controller }) {setTimeout( () => {reject({ code: 7002, error: new Error('Request timeout') }) controller.abort() }, 10) }}error} })//The second way to implement timeout is to define the auxiliary function of timeout rejection (promise) function TIMEOUT_GUARD({ reject, controller }) {setTimeout(() => {reject({ code: 7002, error: new Error('Request timeout') }) controller.abort() }, 10) }}error} })//The second way to implement timeout is to define the auxiliary function of timeout rejection agreement (promise) function TIMEOUT_GUARD({ reject, controller }) {setTimeout(() => {reject({ code: 7002, error: new Error('Request timeout') }) controller.abort() }, 10) }}
export default new Proxy({}, {get(target, serviceName) {return serviceArguments => serve({ serviceName, serviceArguments}) }})

As shown in the code above, service3.js implements hierarchical error handling within the serve() function, adapts the data format required for response normalization (all errors return error objects with code greater than 0), and also enriches " The internal logic of the "olive" shaped interface makes the "olive" truly take shape.

Concluding remarks

This article uses the Vuex call interface as an example to gradually demonstrate the process of optimizing, organizing, abstracting, and refining the logic of obtaining back-end data. These processes are essentially to write the most "cost-effective" code, that is, to achieve as complex functions as possible with as little code as possible: less code, easy to maintain; well organized, easy to debug; abstract and accurate, easy to understand. Just as the "4.Strategies of Simple Design" mentioned in the author's best-selling interactive design monograph "Simple Design": organize, hide, delete, and transfer. These four strategies also apply to the abstraction and simplification of code and logic. To sum up the "three stages" described in this article, it can also be roughly classified into one of the strategies.

Finally, although this article uses Vuex as an example to demonstrate, the principles and principles behind it are the same. Therefore, this article should also be helpful for using Redux in React development.

end

Reference: https://cloud.tencent.com/developer/article/1630746 The three stages of Vuex calling interface-Cloud + Community-Tencent Cloud