挑战21天手写前端框架 day16 纯前端的模拟数据

820 阅读5分钟

Mock 数据是现在前后端开发分离很重要的一个环节,可以保证前端的开发进度和后端的开发进度同步开展,而不是前端开发需要在后端服务完成之后才能进行,比如我们可以预先和服务端约定好接口的请求方式和出入参。

比直接在页面中写死数据最大的好处是前端可以提前完成包括请求逻辑这一份的调通工作,到能与服务端连调时,只需要修改请求前缀就可以完成前后端连调工作,当然在实际的开发交付中,很少能够这么顺利的完成的,但是编写本地的 Mock 服务,确实是可以大大的加快整个项目的交付效率。

原理

原理其实和代理是相通的,只不过代理是匹配到特定的前缀,然后转发到目标服务器上,而 Mock 服务是匹配到完整的路径之后,返回一个本地的数据。

当然为了更加方便的进行 Mock 数据的开发,我们约定在项目根目录下面的所有文件,将会被识别成 Mock 文件,即一个文件就是一个 Mock 对象。

export default {
  'GET /mock/hello': {
    text: 'Malita',
  },
}

写法是沿用了 umi 中的 Mock 服务的写法,key 值由请求方式+空格+请求路径组合而成。umi 中做了一些友好化处理,为了更聚焦功能的实现,我们这里没有实现这一部分,所以要求 Mock 数据的编写严格按照规定。

为了和前几篇文章中的知识点产出关联,实现上我们也是沿用了day13-用户配置day15-proxy的部分实现方式,如果前几天还不是太懂的朋友,看过今天的内容能够加深理解。

实现

增加配置

你可以和用户配置对比着理解,将 Mock 服务视为是一部分的用户配置文件,只不过,用户配置是我们约定好的某个文件,而 Mock 配置则是我们约定好的整个目录下的所有文件。

新建 examples/app/mock/app.ts 文件名可以任意,其实我们都用不到。你可以将所有的服务写在同一个文件中,但为了便于管理和区分前缀,我建议你在真实的项目中按不同的模块来划分文件内容。

简单的编写两个 Mock 服务。一个 Get 请求,返回一个 Object;一个 POST 请求,返回 Function。

import { Request, Response } from 'express';

export default {
  'GET /mock/hello': {
    text: 'Malita',
  },
  'POST /mock/list': (req: Request, res: Response) => {
    const dataSource = [
      {
        id: 1,
        title: 'Title 1',
      },
      {
        id: 2,
        title: 'Title 2',
      },
      {
        id: 3,
        title: 'Title 3',
      },
      {
        id: 4,
        title: 'Title 4',
      },
      {
        id: 5,
        title: 'Title 5',
      },
    ];
    const { body } = req;

    const { pageSize, offset } = body;
    return res.json({
      total: dataSource.length,
      data: dataSource.slice(offset, offset + pageSize),
    });
  },
};

编写 Mock 中间件

新建 packages/malita/src/mock.ts,和我们之前的写法一样,返回一个 Promise。

export const getMockConfig = ({ appData, malitaServe }: { appData: AppData; malitaServe: Server; }) => {
    return new Promise(async (resolve: (value: any) => void, rejects) => {
        const config = {};
        resolve(config);
    })
}

因为我们需要需要先获取 mock 目录下的所有的文件,因此这里我们需要安装 glob 模块。

安装 glob 模块

cd packages/malita
pnpm i glob
pnpm i @types/glob -D

glob 模块的用法非常简单,你只要指定查找的路径,然后匹配你需要的文件正则即可。比如我们这里找出 mock 文件夹下的所有 ts 文件。

const mockDir = path.resolve(appData.paths.cwd, 'mock');
const mockFiles = glob.sync('**/*.ts', {
  cwd: mockDir,
});

// mockFiles [app.ts]

获取所有的 mock 文件

因为它找到的文件是相对于我们提供的 cwd 的,所以我们需要进一步的取到文件的绝对路径。

const ret = mockFiles.map((memo) => path.join(mockDir, memo));

将 mock 文件编译成 js 文件

然后参照用户配置那边的写法,先将目标文件编译成 js 文件,写到我们的临时文件中。

const mockOutDir = path.resolve(appData.paths.absTmpPath, 'mock');
await build({
  format: 'cjs',
  logLevel: 'error',
  outdir: mockOutDir,
  bundle: true,
  watch: {
      onRebuild: (err, res) => {
          if (err) {
              console.error(JSON.stringify(err));
              return;
          }
          malitaServe.emit('REBUILD', { appData });
      }
  },
  define: {
      'process.env.NODE_ENV': JSON.stringify('development'),
  },
  external: ['esbuild'],
  entryPoints: ret,
});

读取 mock 配置

然后读取编译后的文件,获得我们需要的数据。

try {
    const outMockFiles = glob.sync('**/*.js', {
        cwd: mockOutDir,
    });
    cleanRequireCache(mockOutDir);
    config = outMockFiles.reduce((memo, mockFile) => {
        memo = {
            ...memo,
            ...require(path.resolve(mockOutDir, mockFile)).default,
        };
        return memo;
    }, {});
} catch (error) {
    console.error('getMockConfig error', error);
    rejects(error);
}
resolve(config);

系列化 mock 数据

因为我们约定了 key 值由请求方式+空格+请求路径组合而成,因此我们需要先处理数据,将请求方式和请求路径提取出来。

- resolve(config);
+ resolve(normalizeConfig(config));

function normalizeConfig(config: any) {
    return Object.keys(config).reduce((memo: any, key) => {
        const handler = config[key];
        const type = typeof handler;
        // 如果不符合规范,我们舍弃它
        if (type !== 'function' && type !== 'object') {
            return memo;
        }
        const req = key.split(' ');
        const method = req[0];
        const url = req[1];
        if (!memo[method]) memo[method] = {};
        memo[method][url] = handler;
        return memo;
    }, {});
}

操作之后我们会得到一个对象 config

{
  GET: { '/mock/hello': { text: 'Malita' } },
  POST: { '/mock/list': [Function: POST /mock/list] }
}

使用 mock 中间件

接下里的使用就很简单了,参照 proxy 中中间件的添加方式。

packages/malita/src/dev.ts 中:

import { getMockConfig } from './mock';

const buildMain = async ({ appData }: { appData: AppData }) => {
    // getUserConfig
    // 获取 mock 数据
    const mockConfig = await getMockConfig({
        appData, malitaServe
    });

    app.use((req, res, next) => {
        const result = mockConfig?.[req.method]?.[req.url];
        if (Object.prototype.toString.call(result) === "[object String]" || Object.prototype.toString.call(result) === "[object Array]" || Object.prototype.toString.call(result) === "[object Object]") {
            res.json(result)
        } else if (Object.prototype.toString.call(result) === "[object Function]") {
            result(req, res);
        } else {
            next();
        }
    });
    // createProxyMiddleware
}

运行验证

cd examples/app

pnpm dev

> malita dev

App listening at http://127.0.0.1:8888

浏览器中访问 http://127.0.0.1:8888/mock/hello

{ "text": "Malita" }

其他

为了编写 Mock 数据编写更加的高效和便捷,可以在项目中引入一些第三方的 Mock 数据生成库,比如 mock.js

import mockjs from 'mockjs';

export default {
  // 使用 mockjs 等三方库
  'GET /mock/tags': mockjs.mock({
    'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
  }),
};

这样就会随机生成 100 条数据,比我们上面写的 Title 1-5 高明的多了。

感谢阅读,今天的内容就到这里了。距离这个系列结束,仅剩 5 天了,如果你喜欢我的叙述方式,还有其他想看的的系列或者文章,可以通过评论或者微信联系我。我期望将每一个知识点都讲的明白一些,能够帮助到你,哪怕成果仅仅是“入门级别”,我也很满足了。

源码归档