Construire une bibliothèque d'outils JavaScript qui simule des interfaces pour générer des données aléatoires

Lors du développement Web, de nombreux étudiants front-end ont l'expérience d'attendre le débogage conjoint de l'interface back-end. Une sorte de perte de temps.

Peut-être que certains étudiants expérimentés utiliseront certaines bibliothèques d'outils pour créer des interfaces de simulation, la plus couramment utilisée est Mock.js.

Bien sûr, Mock.js est plus puissant, possède de nombreuses fonctions puissantes et peut facilement démarrer le service localement. Cependant, il a aussi quelques lacunes. Par exemple, l'interface générée par sa configuration ne peut pas générer directement le document API correspondant, ce qui nécessite une inspection et une gestion manuelles, ce qui est peu pratique pour la maintenance et la communication. De plus, il a sa propre grammaire de génération de données, ce qui est pratique à écrire, mais il a également des coûts d'apprentissage supplémentaires et n'est pas assez flexible.

Au vu des lacunes ci-dessus, j'espère concevoir un nouvel outil avec les caractéristiques suivantes :

  1. Il peut générer de manière flexible une variété de données simulées avec des fonctions d'outil JavaScript natives
  2. Lors de la génération de l'API simulée, générez en conséquence des documents API, afin que nous puissions comprendre directement l'API complète à travers les documents, ce qui est pratique pour notre recherche et développement, et le backend peut également implémenter de vrais codes commerciaux basés sur les documents.

Ok, voyons comment atteindre notre objectif étape par étape.

Créer des fonctions de génération de données

Le principe le plus fondamental de la génération de données simulées est de générer des données correspondantes selon une description (nous l'appelons schéma).

Par exemple le plus simple :

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

Toutes les propriétés de l'objet ci-dessus schemasont des constantes, donc les données que nous générons directement sont l'entrée d'origine, et la sortie finale est naturellement la suivante :

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

Si nous voulons générer des données aléatoires, nous pouvons utiliser une fonction aléatoire, par exemple :

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

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

De cette façon, nous modifions schemapour obtenir des notes aléatoires :

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

Cela a l'air simple n'est-ce pas ? Mais en réalité, il a des défauts. Regardons en bas.

Si nous voulons générer des données par lots, nous pouvons concevoir une repeatméthode qui schemarenvoie un tableau basé sur l'entrée :

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

De cette façon, nous pouvons l'utiliser pour générer plusieurs éléments de données, par exemple :

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

Le code complet se trouve dans le référentiel de code et les étudiants intéressés peuvent l'essayer par eux-mêmes.

Toutes les questions sont les bienvenues pour discuter, et vous êtes également les bienvenus pour contribuer aux relations publiques du projet.

Je suppose que tu aimes

Origine juejin.im/post/7254794609648828477
conseillé
Classement