preface

In the process of project development, the front-end development needs to rely on the background environment data, and the background staff is usually unable to immediately provide data to the front-end. In order to make the front and back end development do not affect each other, the background usually defines the interface document first and provides it to the front end. Then the front end can simulate the background environment data through the local data of the interface document to realize the parallel development of the front and back end. The front end can also develop self-test according to the fake data to achieve the defect advance.

This paper provides a reductive local data simulation scheme implementation, the scheme may not be the best scheme now, but the implementation process of the scheme is worth carrying out a talk, learn about the implementation ideas.

The implementation effect is as follows:

Create a new API file in the API directory, write the mock data together with the configuration of the API request. When the project starts, scan all files in the API directory, read the API request and its mock configuration, and generate mock data.

// @file src/api/vm.js

import defineRequest from 'xxx';

export default {
    fetchVmData: defineRequest({
        url: '/api/vm'.type: 'GET'.// Get request data
        params (params) {
            return params;
        },
        mock: {
            'data|1-5': [{
                name: '@cname'.id: '@uuid'.status: '@boolean',}],},}),};Copy the code

Concrete implementation process

1. Recursively scan the API directory

The first step is to recursively scan all files in the API directory and return an array of files.

const scanApi = (baseDir) = > {
    const result = [];
    const files = fs.readFileSync(baseDir);
    for (const file of files) {
        const current = path.join(basePath, file);
        const stat = fs.statSync(current);
        const isFile = stat.isFile();
        const isDir = stat.isDirectory();
        if (isFile) {
            result.push(current);
        } else if(isDir) { result = result.concat(scanApi(current)); }}return result;
};
Copy the code

Second, read the contents of each file in the file array

const readFileContent = (files) = > {
    return files.map(currentFile= > fs.readFileSync(currentFile).toString());
};
Copy the code

2. Extract mock configurations

The third step is to extract the mock configuration we need from the contents of the file. How do we extract the mock configuration? Write a regular extraction? This is also possible, but requires you to maintain multiple complex regular expressions. In addition to regular extraction, you can also extract from the jscodeshift artifact. Jscodeshift is to parse JS, TS code into an abstract syntax tree, and provide a series of operational interfaces for accessing and modifying the abstract syntax tree (abstract syntax tree is a tree representation of the syntax structure of a programming language, each node in the tree represents a structure in the source code, an AST visualization tool is recommended).

From the AST analysis above, we find the call to defineRequest and get arguments[0] from the call to read the mock related configuration.

const jAst = require('jscodeshift');

// Get all definerequests in the project
const getDefineRequests = (contents) = > {
    return contents.reduce((total, content) = > {
        const ast = jAst(content); // String to abstract syntax tree
        const arr = ast.find(jAst.CallExpression, {
            callee: {
                // Find a node whose type is function call and name is defineRequest
                name: 'defineRequest',
            },
        }).map(item= > item.parent); // A single file can have multiple definerequests
        total = total.concat(arr);
        returntotal; } []); };Copy the code

const getConfigMap = (defineRequests) = > {
    const result = {};
    defineRequests.forEach(({ value: item }) = > {
        const name = item.key.name;
        const properties = item.value.arguments[0].properties;

        // Store 'URL ', 'type', 'mock' configurations for each defineRequest
        const cfgMap = ['url'.'type'.'mock'].reduce((map, keyName) = > {
            const target = properties.find(prop= > prop.key.name === keyName);
            let value;
            if(target? .value) {// Assign a value to value
                eval(`value = ${jAst(target.value).toSource()}`);
            }
            map[keyName] = value;
            return map;
        }, {});
        
        result[name] = cfgMap;
    });
    
    return result;
};
Copy the code
// map to array
const getConfigList = (map) = > {
    return Object.entries(map).map(([key, value]) = > {
        return {
            name: key,
            url,
            type: value.type || 'get'.response: value.mock,
        };
    }).filter(obj= > obj.response); // Filter out the unconfigured mock
};
Copy the code

Mock fields can be passed to MockJS for mock data generation. If the project is built using WebPack, we can use the setupMiddlewares hook method in webpack-dev-server (v4.7.x). Get devServer.app and match the mock data related to the route response. (This article focuses on implementing local data simulation based on ast, so the code implementation of matching the route response data will be skipped.)

Problems encountered and countermeasures

The solution has been realized, but there are still some problems. Next, we will talk about the problems encountered and how to solve them.

1. Ts parsing is not supported

If you write code using TS in an API file, an error will be reported. Jscodeshift does not parse typescript by default. We need to pass a configuration to jscodeshift

const jAst = require('jscodeshift');
const parser = require('jscodeshift/parser/ts') ();const ast = jAst(content); // old
const ast = jAst(content, { parser }); // new
Copy the code

Export let xx = defineRequest(…)

Export default {x: defineRequest(…) }, if export let x = defineRequest(…) In this case, its abstract syntax tree becomes different, and we need to write extra code to handle this.

// old
const name = item.key.name;
const properties = item.value.arguments[0].properties;

// new
const { name, properties } = ((item) = > {
    if (item.type === 'VariableDeclarator') {
        // let x = defineRequest(...)
        return {
            name: item.id.name,
            properties: item.init.arguments[0].properties,
        };
    }
    // export default {x: defineRequest(...) }
    return {
        name: item.key.name,
        properties: item.value.arguments[0].properties,
    };
})(item);
Copy the code

3. Fancy writing in API files is not supported

If your API file is written like this, it will parse an error

const mock = {
    'data|1-5': [{
        name: '@cname'.id: '@uuid'.status: '@boolean',}]};export default {
    fetchVmData: defineRequest({
        url: '/api/vm'.type: 'GET'.// Get request data
        params (params) {
            return params;
        },
        mock,
    }),
    fetchHostData: defineRequest({
        url: '/api/host'.type: 'GET'.// Get request data
        params (params) {
            return params;
        },
        mock,
    }),
};
Copy the code

As a result, the abstract syntax tree changes again, and the mock configuration is no longer available. In other words, the scheme in this article is a reduced-form local data simulation scheme, which does not allow developers to write various kinds of syntax in API files. The best way to write code is to follow the syntax supported in the scheme. It is worth mentioning that we can refactor old code based on AST, and the difficulty of implementing AST based code is that there are so many different styles of code that it is easy to miss some styles, or it is difficult to access and modify the AST for a particular style.

4. Mock code is packaged into production

We passed the mock code to defineRequest, which is coupled to the API request code and packaged into production when the project is packaged. We also need to write a plug-in that removes the mock code and then gets rid of it when packaging.

jAst(content).find(jAst.Property, {
    key: {
        type: 'Identifier'.name: 'mock',
    },
}).filter(p= > {
    const node = p.parent.parent.node;
    if (node.type === 'CallExpression') {
       const callee = node.callee;
       return callee.type === 'Identifier' && callee.name === 'defineRequest';
    }
    return false;
}).remove().toSource();
Copy the code

While the mock code is coupled to the API request code, it’s not entirely a bad thing. When your API request code is deprecated, removing the API request code will also remove the mock code. If the mock code is removed, many developers will forget to remove the mock code when removing deprecated API request code.

As written throughout, the AST based approach to local data simulation has many limitations and is clearly not the best implementation of local data simulation. A long, long time ago, I implemented another local data emulation scheme with umiJS: SXf-dev-mock. You can compare the implementation of these two schemes, and I think umiJS is much better.

Finally, I would like to thank my mentor Rui Ge and my colleague Wei Ge for their technical solutions and overcoming technical difficulties.