龙空技术网

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

架构思考 185

前言:

眼前小伙伴们对“前端模拟数据的方式”都比较关心,兄弟们都想要分析一些“前端模拟数据的方式”的相关资讯。那么小编同时在网络上汇集了一些关于“前端模拟数据的方式””的相关内容,希望你们能喜欢,同学们一起来了解一下吧!

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

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

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

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

它能够用原生JavaScript工具函数灵活地生成各种各样的模拟数据生成模拟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=";>  <link rel="stylesheet" href=";>  <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将代码通过模板渲染成最后的接口页面。

生成页面类似效果如下:

文章来源:

标签: #前端模拟数据的方式