基于AST实现本地数据模拟方案

前言

项目开发过程中,前端开发需要依赖后台的环境数据,而后台人员通常是无法立即提供数据给到前端。为了使前后端开发互不影响,后台一般会先定义好接口文档并提供给前端,前端就可以通过接口文档进行本地数据模拟后台环境数据,实现前后端并行开发,前端也可以根据假数据进行开发自测,做到缺陷前移。

本文提供一种约定式的本地数据模拟方案的实现,该方案在现在看来可能不是最佳的方案,但是方案的实现过程还是值得拎出来讲一讲,学习一下其中的实现思路的。

实现效果如下:

在api目录下新建api文件,将mock数据与api请求的配置写在一起,项目启动时扫描api目录下所有文件,读取api请求及其mock配置,生成mock数据。

// @file src/api/vm.js

import defineRequest from 'xxx';

export default {
    fetchVmData: defineRequest({
        url: '/api/vm',
        type: 'GET', // 一般都是模拟get请求数据
        params (params) {
            return params;
        },
        mock: {
            'data|1-5': [{
                name: '@cname',
                id: '@uuid',
                status: '@boolean',
            }],
        },
    }),
};
复制代码

具体实现过程

1. 递归扫描api目录

第一步,递归扫描api目录下的所有文件,返回文件数组。

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;
};
复制代码

第二步,读取文件数组中每个文件的内容

const readFileContent = (files) => {
    return files.map(currentFile => fs.readFileSync(currentFile).toString());
};
复制代码

2. 提取mock配置

第三步,从文件内容中提取我们需要的mock配置,那如何提取mock配置呢?写一个正则提取吗?也是可行的,只不过需要我们去维护多个复杂的正则表达式,除了正则提取,我们还可以借助jscodeshift神器进行提取。jscodeshift是将js、ts代码解析为抽象语法树,并提供一系列用于访问和修改抽象语法树的操作接口(抽象语法树是以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,推荐一个AST可视化工具)。

image.png

从上图的ast分析可知,我们要先找到defineRequest的调用处,然后从调用处获取arguments[0]来读取mock相关配置。

const jAst = require('jscodeshift');

// 获取项目里所有的defineRequest
const getDefineRequests = (contents) => {
    return contents.reduce((total, content) => {
        const ast = jAst(content); // 字符串转抽象语法树
        const arr = ast.find(jAst.CallExpression, {
            callee: {
                // 找到节点类型为函数调用且名字为defineRequest
                name: 'defineRequest',
            },
        }).map(item => item.parent); // 单文件可以有多个defineRequest
        total = total.concat(arr);
        return total;
    }, []);
};
复制代码

image.png

image.png

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

        // 存放每个defineRequest的'url', 'type', 'mock'配置
        const cfgMap = ['url', 'type', 'mock'].reduce((map, keyName) => {
            const target = properties.find(prop => prop.key.name === keyName);
            let value;
            if (target?.value) {
                // 给value赋值
                eval(`value = ${jAst(target.value).toSource()}`);
            }
            map[keyName] = value;
            return map;
        }, {});
        
        result[name] = cfgMap;
    });
    
    return result;
};
复制代码
// map转数组
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); // 过滤掉没有配置mock的
};
复制代码

至此,我们就拿到了所有的mock配置,mock字段的内容可以传给mockjs进行假数据生成,项目如果是使用webpack构建的话,我们可以在webpack-dev-server(v4.7.x)的setupMiddlewares钩子方法里,拿到devServer.app,通过匹配路由响应相关的mock数据了(本文重点在于讲基于ast实现本地数据模拟,匹配路由响应数据的代码实现就略过)。

遇到的问题和应对措施

方案是实现出来了,但是还是存在一些问题的,接下来讲下遇到的问题以及如何解决。

1. 不支持解析ts

如果你在api文件中使用ts写代码,是会报错的,因为jscodeshift默认是不解析typescript的,我们需要给jscodeshift传一个配置

const jAst = require('jscodeshift');
const parser = require('jscodeshift/parser/ts')();

const ast = jAst(content); // old
const ast = jAst(content, { parser }); // new
复制代码

2. 不支持解析export let xx = defineRequest(...)

上文我们是针对export default { x: defineRequest(...) }的抽象语法树进行操作,如果是export let x = defineRequest(...),这个时候它的抽象语法树就变得不一样了,我们需要额外写代码处理下这种情况。

image.png

image.png

// 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);
复制代码

3. 不支持在api文件使用花式写法

如果你的api文件是像下面这么写的,那么是会解析报错的

const mock = {
    'data|1-5': [{
        name: '@cname',
        id: '@uuid',
        status: '@boolean',
    }],
};

export default {
    fetchVmData: defineRequest({
        url: '/api/vm',
        type: 'GET', // 一般都是模拟get请求数据
        params (params) {
            return params;
        },
        mock,
    }),
    fetchHostData: defineRequest({
        url: '/api/host',
        type: 'GET', // 一般都是模拟get请求数据
        params (params) {
            return params;
        },
        mock,
    }),
};
复制代码

这种写法导致抽象语法树又发生了变化,获取不到mock的配置了,也就是说,本文的方案是一种约定式的本地数据模拟方案,不支持开发者在api文件里进行各种花式写法,最好的写法就是按照方案里支持的写法进行去写代码。值得提一下的是,我们可以基于AST去对老代码进行重构,而基于AST实现的难点就在于代码的各种花式写法太多了,我们很容易就遗漏某些骚写法,或者难以针对某种骚写法访问修改它的AST。

4. mock代码会被打包到生产环境

我们把mock代码传给了defineRequest,mock代码与api请求代码耦合在了一起,项目打包的时候,也会把mock代码打包到生产环境中。我们还需要写一个删除mock代码的插件,在打包时把mock代码删掉。

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();
复制代码

虽然mock代码与api请求代码耦合在了一起,但是也不完全是一件坏事。当你的api请求代码废弃时,将api请求代码删除的同时也会把mock代码也给删掉,如果把mock代码抽出去,很多开发者在删除废弃api请求代码时,总是会忘记把mock代码也删掉。

通篇写下来,基于AST实现本地数据模拟方案的方式存在着许多局限性,显然不是最佳的本地数据模拟方案实现。我在很久很久以前,曾经参考umijs实现过另外一套本地数据模拟方案:sxf-dev-mock。可以将这两套方案实现进行对比,我觉得还是umijs的方案可用性会好很多。

最后的最后,感谢我的导师锐哥、同事炜哥提供的技术方案、技术难点攻克。

Guess you like

Origin juejin.im/post/7077012551703199774