Cree una biblioteca de herramientas de JavaScript que simule interfaces para generar datos aleatorios

Al hacer desarrollo web, muchos estudiantes de front-end tienen la experiencia de esperar la depuración conjunta de la interfaz de back-end, una especie de pérdida de tiempo.

Tal vez algunos estudiantes experimentados usen algunas bibliotecas de herramientas para crear interfaces de simulación, la más utilizada es Mock.js.

Por supuesto, Mock.js es más poderoso, tiene muchas funciones poderosas y puede iniciar fácilmente el servicio localmente. Sin embargo, también tiene algunas deficiencias. Por ejemplo, la interfaz generada por su configuración no puede generar directamente el documento API correspondiente, lo que requiere una inspección y gestión manual, lo que es un inconveniente para el mantenimiento y la comunicación. Además, tiene su propia gramática de generación de datos, lo cual es conveniente de escribir, pero también tiene costos adicionales de aprendizaje y no es lo suficientemente flexible.

En vista de las deficiencias anteriores, espero diseñar una nueva herramienta con las siguientes características:

  1. Puede generar de manera flexible una variedad de datos simulados con funciones de herramientas nativas de JavaScript
  2. Mientras genera la API simulada, genere correspondientemente documentos API, de modo que podamos entender directamente la API completa a través de los documentos, lo cual es conveniente para nuestra investigación y desarrollo, y el backend también puede implementar códigos comerciales reales basados ​​en los documentos.

Bien, veamos cómo lograr nuestro objetivo paso a paso.

Construir funciones de generación de datos

El principio más básico de generar datos simulados es generar los datos correspondientes de acuerdo con una descripción (lo llamamos esquema).

Por ejemplo la más sencilla:

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

Todas las propiedades del objeto anterior schemason constantes, por lo que los datos que generamos directamente son la entrada original y, naturalmente, la salida final es la siguiente:

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

Si queremos generar datos aleatorios, entonces podemos usar una función aleatoria, por ejemplo:

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 esta forma, modificamos schemapara obtener calificaciones aleatorias:

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

Esto parece simple, ¿no? Pero en realidad tiene fallas. Miremos hacia abajo.

Si queremos generar datos en lotes, podemos diseñar un repeatmétodo que schemadevuelva una matriz basada en la entrada:

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

De esta forma, podemos usarlo para generar múltiples datos, por ejemplo:

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将代码通过模板渲染成最后的接口页面。

生成页面类似效果如下:

imagen.png

El código completo se puede encontrar en el repositorio de código y los estudiantes interesados ​​pueden probarlo por sí mismos.

Cualquier pregunta es bienvenida para discutir, y también puede contribuir con las relaciones públicas del proyecto.

Supongo que te gusta

Origin juejin.im/post/7254794609648828477
Recomendado
Clasificación