构建模拟接口生成随机数据的JavaScript工具库

在进行Web开发的时候,很多前端同学都有等待后端接口联调的经历,一开始的时候,在后端接口没有准备好的时候,很多同学无法开始工作,只能做一些前期准备,感觉有点浪费时间。

可能有些经验比较丰富的同学会使用一些工具库来构建模拟接口,最常用的如 Mock.js

当然,Mock.js比较强大,有很多强大的功能,而且能够方便地在本地启动服务。但是,它也有一些不足。比如,它配置生成的接口,不能直接生成对应的API文档,这样还需要人为检查和管理,不方便维护和沟通。另外,它有一套自己的数据生成语法,写起来方便,但是也有额外的学习成本,且不够灵活。

针对上面的缺点,我希望设计一个新的工具,有如下特点:

  1. 它能够用原生JavaScript工具函数灵活地生成各种各样的模拟数据
  2. 生成模拟API的同时,对应生成API的文档,这样我们就可以直接通过文档了解完整的API,既方便我们的研发,后端也可以根据文档实现真正的业务代码。

好,那我们来看看如何一步步实现我们的目标。

构建数据生成函数

生成模拟数据的最基本原理,就是根据一份描述(我们称为schema),来生成对应的数据。

比如最简单的:

const schema = {
    name: 'Akira',
    score: '100',
};
const data = generate(schema);
console.log(data);

上面这个schema对象里面的所有的属性都是常量,所以我们直接生成的数据就是原始输入,最终输出的结果自然是如下:

{
    "name":"akira",
    "score":100
}

如果我们要生成的数据随机一点,那么我们可以使用随机函数,例如:

function randomFloat(from = 0, to = 1) {
    return from + Math.random() * (to - from);
}

function randomInteger(from = 0, to = 1000) {
    return Math.floor(randomFloat(from, to));
}

这样,我们修改schema得到随机的成绩:

const schema = {
    name: 'Akira',
    score: randomInteger(),
}
...

这个看起来很简单是不是?但是实际上它有缺陷。我们接着往下看。

假如我们要批量生成数据,我们可以设计一个repeat方法,它根据传入的schema返回数组:

function repeat(schema, min = 3, max = min) {
  const times = min + Math.floor((max - min) * Math.random());
	return new Array(times).fill(schema);
}

这样,我们就可以用它来生成多条数据,例如:

const schema = repeat({
    name: 'Akira',
    score: randomInteger(),
}, 5); 

但是这样明显有个问题,注意到我们通过repeat复制数据,虽然我们生成了随机的score,但是在repeat复制前,score的值已经通过randomInteger()生成好了,所以我们得到的5条记录的score值是完全一样的,这个不符合我们的期望。

那应该怎么办呢?

利用函数延迟求值

我们修改生成函数:

function randomFloat(from = 0, to = 1) {
	return () => from + Math.random() * (to - from);
}

function randomInteger(from = 0, to = 1000) {
	return () => Math.floor(randomFloat(from, to)());
}

这里最大的改动是让randomInteger生产函数不直接返回值,而是返回一个函数,这样我们在repeat的时候再去求值,就可以得到不同的随机值。

要做到这一点,我们的生成器需要能够解并和执行函数。

下面是生成器的实现代码:

function generate(schema, extras = {}) {
    if(schema == null) return null;
    if(Array.isArray(schema)) {
            return schema.map((s, i) => generate(s, {...extras, index: i}));
    }
    if(typeof schema === 'function') {
            return generate(schema(extras), extras);	
    }
    if(typeof schema === 'object') {
        if(schema instanceof Date) {
          return schema.toISOString();
        }
        if(schema instanceof RegExp) {
          return schema.toString();
        }
        const ret = {};
        for(const [k, v] of Object.entries(schema)) {
            ret[k] = generate(v, extras);
        }
        return ret;
    }
    return schema;
};

生成器是构建数据最核心的部分,你会发现其实它并不复杂,关键是递归地处理不同类型的属性值,当遇到函数的时候,再调用函数执行,返回内容。

function generate(schema, extras = {}) {
    if(schema == null) return null;
    if(Array.isArray(schema)) {
            return schema.map((s, i) => generate(s, {...extras, index: i}));
    }
    if(typeof schema === 'function') {
            return generate(schema(extras), extras);	
    }
    if(typeof schema === 'object') {
        if(schema instanceof Date) {
          return schema.toISOString();
        }
        if(schema instanceof RegExp) {
          return schema.toString();
        }
        const ret = {};
        for(const [k, v] of Object.entries(schema)) {
            ret[k] = generate(v, extras);
        }
        return ret;
    }
    return schema;
};

function randomFloat(from = 0, to = 1) {
	return () => from + Math.random() * (to - from);
}

function randomInteger(from = 0, to = 1000) {
	return () => Math.floor(randomFloat(from, to)());
}

function genName() {
  let i = 0;
  return () => `student${i++}`;
}

function repeat(schema, min = 3, max = min) {
  const times = min + Math.floor((max - min) * Math.random());
	return new Array(times).fill(schema);
}

const res = generate(repeat({
  name: genName(),
  score: randomInteger(0, 100),
}, 5));

console.log(JSON.stringify(res, null, 2));

输出结果如下:

[
  {
    "name": "student0",
    "score": 47
  },
  {
    "name": "student1",
    "score": 71
  },
  {
    "name": "student2",
    "score": 68
  },
  {
    "name": "student3",
    "score": 96
  },
  {
    "name": "student4",
    "score": 91
  }
]

所以,这里最关键的问题就是利用函数表达式延迟取值,这样能及时取到随机数值,以符合自己的要求。

比如:

生成 API 文档

第二个比较核心的功能是根据schema生成API文档,这个其实本质上是生成一段 HTML 片段,难度应该不大,细节比较复杂,可选方案也很多。

这里我选择的是根据schema构建markdown文本,然后通过marked最终解析成HTML的办法。

Marked初始化代码片段如下:

const renderer = new marked.Renderer();

renderer.heading = function(text, level, raw) {
  if(level <= 3) {
    const anchor = 'mockingjay-' + raw.toLowerCase().replace(/[^\w\\u4e00-\\u9fa5]]+/g, '-');
    return `<h${level} id="${anchor}"><a class="anchor" aria-hidden="true" href="#${anchor}"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a>${text}</h${level}>\n`;
  } else {
    return `<h${level}>${text}</h${level}>\n`;
  }
};

const options = {
  renderer,
  pedantic: false,
  gfm: true,
  breaks: false,
  sanitize: false,
  smartLists: true,
  smartypants: false,
  xhtml: false,
  headerIds: false,
  mangle: false,
};

marked.setOptions(options);
marked.use(markedHighlight({
  langPrefix: 'hljs language-',
  highlight(code, lang) {
    const language = hljs.getLanguage(lang) ? lang : 'plaintext';
    return hljs.highlight(code, { language }).value;
  }
}));

再准备一个 HTML 模板

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>AirCode Doc</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <meta name="description" content="A graphics system born for visualization.">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.css">
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/styles/github.css">
  <style>
    .markdown-body {
      padding: 2rem;
    }
  </style>
</head>
<body>
  <div class="markdown-body">
  ${markdownBody}
  </div>
</body>
</html>

最终我们实现一个compile方法:

  compile() {
    return async (params, context) => {
      // console.log(process.env, params, context);
      const contentType = context.headers['content-type'];
      if(contentType !== 'application/json') {
        context.set('content-type', 'text/html');
        const markdownBody = marked.parse(this.info());
        return await display(path.join(__dirname, 'index.html'), {markdownBody});
      }
      const method = context.method;
      const headers = this.#responseHeaders[method];
      if(headers) {
        for(const [k, v] of Object.entries(headers)) {
          context.set(k, v);
        }
      }
      const schema = this.#schemas[method];
      if(schema) {
        return generate(schema, {params, context, mock: this});
      }
      if(typeof context.status === 'function') context.status(403);
      else if(context.response) context.response.status = 403;
      return {error: 'method not allowed'};
    };
  }

这个方法返回一个服务端云函数,根据http请求的content-type返回内容,如果是application/json,返回接口生成的JSON数据,否则返回HTML页面,其中this.info()是得到Markdown代码,display将代码通过模板渲染成最后的接口页面。

生成页面类似效果如下:

image.png

完整的代码见代码仓库,有兴趣的同学可以自行尝试。

有任何问题欢迎讨论,也欢迎给项目贡献PR。

猜你喜欢

转载自juejin.im/post/7254794609648828477