不要等待!模拟 API

Avatar of Marko Ilic
Marko Ilic

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 200 美元的免费信用额度!

如今,Web 应用程序的前端和后端之间存在着松散的耦合。它们通常由不同的团队开发,而保持这些团队和技术同步并非易事。为了解决此问题的一部分,我们可以“伪造”后端技术通常会创建的 API 服务器,并像 API 或端点已经存在一样进行开发。

创建模拟或“伪造”组件的最常用术语是 *模拟*。模拟允许您模拟 API,而(理想情况下)无需更改前端。模拟的实现方式有很多,至少在我看来,这就是让大多数人感到害怕的原因。

让我们来了解一下一个好的 API 模拟应该是什么样的,以及如何将模拟的 API 实现到新的或现有的应用程序中。

请注意,我即将展示的实现是与框架无关的——因此它可以与任何框架或原生 JavaScript 应用程序一起使用。

Mirage:模拟框架

我们将使用的模拟方法称为 Mirage,它是一个相对较新的方法。我已经测试过很多模拟框架,最近才发现了这个框架,它彻底改变了我的开发方式。

Mirage 被宣传为一个面向前端的框架,它配备了现代界面。它在您的浏览器中(客户端)运行,通过拦截 XMLHttpRequest 和 Fetch 请求来实现。

我们将逐步创建使用模拟 API 的简单应用程序,并在此过程中介绍一些常见问题。

Mirage 设置

让我们创建一个标准的待办事项应用程序来演示模拟。我将使用 Vue 作为我的首选框架,但当然,您也可以使用其他框架,因为我们使用的是与框架无关的方法。

因此,请在您的项目中安装 Mirage

# Using npm
npm i miragejs -D


# Using Yarn
yarn add miragejs -D

要开始使用 Mirage,我们需要设置一个“服务器”(带引号,因为它是一个伪造的服务器)。在我们进入设置之前,我将介绍我发现最有效的文件夹结构。

/
├── public
├── src
│   ├── api
│   │   └── mock
│   │       ├── fixtures
│   │       │   └── get-tasks.js
│   │       └── index.js
│   └── main.js
├── package.json
└── package-lock.json

mock 目录中,打开一个新的 index.js 文件并定义您的模拟服务器

// api/mock/index.js
import { Server } from 'miragejs';


export default function ({ environment = 'development' } = {}) {
  return new Server({
    environment,


    routes() {
      // We will add our routes here
    },
  });
}

我们添加到函数签名中的环境参数只是一个约定。我们可以根据需要传递不同的环境。

现在,打开您的应用程序引导文件。在本例中,它是 src/main.js 文件,因为我们使用的是 Vue。导入您的 createServer 函数,并在 *开发环境* 中调用它。

// main.js
import createServer from './mock'


if (process.env.NODE_ENV === 'development') {
    createServer();
}

我们在这里使用的是 process.env.NODE_ENV 环境变量,它是一个常见的全局变量。条件语句允许 Mirage 在生产环境中进行树摇,因此它不会影响您的生产包。

这就是我们设置 Mirage 所需的所有操作!正是这种易用性使得 Mirage 的 DX 非常出色。

为了使本文简单易懂,我们的 createServer 函数默认设置为 development 环境。在大多数情况下,它将默认设置为 test,因为在大多数应用程序中,您将在开发模式下调用 createServer 一次,但在测试文件中调用多次。

工作原理

在我们发出第一个请求之前,让我们快速了解一下 Mirage 的工作原理。

Mirage 是一个**客户端**模拟框架,这意味着所有模拟都将在浏览器中进行,Mirage 使用 Pretender 库来实现。Pretender 将暂时替换本地的 XMLHttpRequest 和 Fetch 配置,拦截所有请求,并将它们定向到 Mirage 挂钩到的一个小型模拟服务。

如果您打开开发者工具并进入网络选项卡,您将看不到任何 Mirage 请求。这是因为请求被拦截并由 Mirage(通过后端的 Pretender)处理。Mirage 会记录所有请求,我们稍后会讨论。

让我们发出请求!

让我们创建一个对 /api/tasks 端点的请求,该端点将返回一个待办事项列表,我们将在待办事项应用程序中显示这些列表。请注意,我使用的是 axios 来获取数据。这只是我的个人喜好。同样,Mirage 与本地的 XMLHttpRequest、Fetch 和任何其他库都兼容。

// components/tasks.vue
export default {
  async created() {
    try {
      const { data } = await axios.get('/api/tasks'); // Fetch the data
      this.tasks = data.tasks;
    } catch(e) {
      console.error(e);
    }
  }
};

打开您的 JavaScript 控制台——您应该会在那里看到来自 Mirage 的错误

Mirage: Your app tried to GET '/api/tasks', but there was no route defined to handle this request.

这意味着 Mirage 正在运行,但路由器还没有被模拟。让我们通过添加该路由来解决这个问题。

模拟请求

在我们的 mock/index.js 文件中,有一个 routes() 钩子。路由处理程序允许我们定义哪些 URL 应该由 Mirage 服务器处理。

要定义路由处理程序,我们需要将其添加到 routes() 函数中。

// mock/index.js
export default function ({ environment = 'development' } = {}) {
    // ...
    routes() {
      this.get('/api/tasks', () => ({
        tasks: [
          { id: 1, text: "Feed the cat" },
          { id: 2, text: "Wash the dishes" },
          //...
        ],
      }))
    },
  });
}

routes() 钩子是我们定义路由处理程序的方式。使用 this.get() 方法可以模拟 GET 请求。所有请求函数的第一个参数是我们正在处理的 URL,第二个参数是一个返回一些数据的函数。

需要注意的是,Mirage 接受任何 HTTP 请求类型,每种类型都有相同的签名

this.get('/tasks', (schema, request) => { ... });
this.post('/tasks', (schema, request) => { ... });
this.patch('/tasks/:id', (schema, request) => { ... });
this.put('/tasks/:id', (schema, request) => { ... });
this.del('/tasks/:id', (schema, request) => { ... });
this.options('/tasks', (schema, request) => { ... });

我们将在稍后讨论回调函数的 schemarequest 参数。

有了这些,我们就成功地模拟了我们的路由,我们应该在控制台中看到来自 Mirage 的成功响应。

Screenshot of a Mirage response in the console showing data for two task objects with IDs 1 and 2.

使用动态数据

尝试在我们的应用程序中添加新的待办事项是不可能的,因为我们 GET 响应中的数据是硬编码的。Mirage 的解决方案是提供一个轻量级的数据层,充当数据库。让我们修复一下我们目前遇到的问题。

routes() 钩子一样,Mirage 定义了一个 seeds() 钩子。它允许我们为服务器创建初始数据。我将把 GET 数据移动到 seeds() 钩子中,在那里我将把它推送到 Mirage 数据库中。

seeds(server) {
  server.db.loadData({
    tasks: [
      { id: 1, text: "Feed the cat" },
      { id: 2, text: "Wash the dishes" },
    ],
  })
},

我把我们的静态数据从 GET 方法移动到 seeds() 钩子中,在那里该数据被加载到一个模拟数据库中。现在,我们需要重构我们的 GET 方法以返回来自该数据库的数据。这实际上非常简单——任何 route() 方法回调函数的第一个参数都是模式。

this.get('/api/tasks', (schema) => {
  return schema.db.tasks;
})

现在,我们可以通过发出 POST 请求来向我们的应用程序添加新的待办事项

async addTask() {
  const { data } = await axios.post('/api/tasks', { data: this.newTask });
  this.tasks.push(data);
  this.newTask = {};
},

我们通过创建 POST /api/tasks 路由处理程序来模拟 Mirage 中的此路由

this.post('/tasks', (schema, request) => {})

使用回调函数的第二个参数,我们可以看到已发送的请求。

Screenshot of the mocking server request. The requestBody property is highlighted in yellow and contains text data that says Hello CSS-Tricks.

requestBody 属性中包含我们发送的数据。这意味着现在我们可以使用它来创建一个新的待办事项。

this.post('/api/tasks', (schema, request) => {
  // Take the send data from axios.
  const task = JSON.parse(request.requestBody).data


  return schema.db.tasks.insert(task)
})

待办事项的 id 将由 Mirage 的数据库默认设置。因此,无需跟踪 id 并将其与您的请求一起发送——就像真实的服务器一样。

动态路由?当然!

最后要介绍的是动态路由。它们允许我们在 URL 中使用动态段,这对于删除或更新应用程序中的单个待办事项非常有用。

我们的删除请求应该发送到 /api/tasks/1/api/tasks/2 等等。Mirage 提供了一种方法,我们可以使用它来定义 URL 中的动态段,如下所示

this.delete('/api/tasks/:id', (schema, request) => {
  // Return the ID from URL.
  const id = request.params.id;


  return schema.db.tasks.remove(id);
})

在 URL 中使用冒号 (:) 是我们定义 URL 中动态段的方法。冒号后面,我们指定段的名称,在本例中,名为 id,它映射到特定待办事项的 ID。我们可以通过 request.params 对象访问段的值,其中属性名称对应于段名称 - request.params.id。然后,我们使用模式从 Mirage 数据库中删除具有相同 ID 的项。

如果你已经注意到,到目前为止我所有的路由都以 api/ 为前缀。一遍又一遍地写这个可能会很麻烦,你可能想要让它更容易。Mirage 提供了 namespace 属性,可以提供帮助。在路由钩子内部,我们可以定义 namespace 属性,这样我们就不必每次都写出来。

routes() {
 // Prefix for all routes.
 this.namespace = '/api';


 this.get('/tasks', () => { ... })
 this.delete('/tasks/:id', () => { ... })
 this.post('/tasks', () => { ... })
}

好的,让我们把它集成到一个现有的应用中。

到目前为止,我们所看到的一切都是将 Mirage 集成到一个新的应用程序中。但是,如何将 Mirage 添加到现有的应用程序中呢?Mirage 已经为你准备好了,所以你不必模拟整个 API。

首先要注意的是,如果网站发出了 Mirage 没有处理的请求,那么将 Mirage 添加到现有的应用程序中会导致错误。为了避免这种情况,我们可以告诉 Mirage 通过所有未处理的请求。

routes() {
  this.get('/tasks', () => { ... })
  
  // Pass through all unhandled requests.
  this.passthrough()
}

现在,我们可以基于现有 API 进行开发,Mirage 只处理我们 API 中缺失的部分。

Mirage 甚至可以更改它捕获请求的基 URL。这很有用,因为通常服务器不会运行在 localhost:3000 上,而是运行在自定义域名上。

routes() {
 // Set the base route.
 this.urlPrefix = 'https://devenv.ourapp.example';


 this.get('/tasks', () => { ... })
}

现在,我们所有的请求都将指向真正的 API 服务器,但 Mirage 会像我们在设置新应用程序时一样拦截它们。这意味着从 Mirage 到真正的 API 的过渡非常无缝 - 从模拟服务器中删除路由,一切都将正常进行。

总结

在过去的五年里,我使用过很多模拟框架,但我从来没有真正喜欢过任何现有的解决方案。直到最近,当我的团队面临着对模拟解决方案的需求时,我才发现了 Mirage。

其他解决方案,例如常用的 JSON-Server,是需要与前端一起运行的外部进程。此外,它们通常只是一台 Express 服务器,上面有一些实用功能。结果是,像我们这样的前端开发人员需要了解中间件、NodeJS 和服务器的工作原理……这些东西我们可能都不想处理。其他尝试,例如 Mockoon,具有复杂的界面,同时缺乏急需的功能。还有一组框架只用于测试,例如流行的 SinonJS。不幸的是,这些框架无法用于模拟常规行为。

我的团队设法创建了一个功能服务器,使我们能够像使用真正的后端一样编写前端代码。我们通过编写前端代码库来做到这一点,而无需运行任何外部进程或服务器。这就是我喜欢 Mirage 的原因。它非常易于设置,但功能强大,足以处理任何抛给它的东西。你可以用它来编写返回静态数组的基本应用程序,也可以用它来编写完整的后端应用程序 - 无论是新的应用程序还是现有应用程序。

除了我们在这里介绍的实现之外,Mirage 还有更多功能。我们所介绍内容的工作示例可以在 GitHub 上找到。(有趣的事实:Mirage 也适用于 GraphQL!)Mirage 有完善的 文档,其中包括大量分步教程,所以一定要查看。