nodejs模拟接口

最近在做一个本地mock ajax请求数据的功能,下面简单说下其依赖的方法。

我的做法是通过express + mock模拟动态接口。

先下载express包,然后分别创建get和post请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//server.js
let express = require('express');
let app = express();
let router = express.Router();

app.get('/',(req, res)=>{//这是一个非常简单的get方式的请求
    res.send('hello world');
});

router.use('/test', require('./test'));

app.use('/api', router);

app.listen(8092);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//test.js
const mockjs = require('mockjs');
let express = require('express');
let router = express.Router();

router.use('/profile', (req,res)=>{//这里给请求写入了一个列表数据
    console.log(req.body);
    let data = mockjs.mock({
        // 属性 list 的值是一个数组,其中含有 1 到 10 个元素
        'list|1-10': [{
            // 属性 id 是一个自增数,起始值为 1,每次增 1
            'id|+1': 1
        }]
    });
    res.header('Access-Control-Allow-Origin', '*');//允许跨域
    return res.json(data);
});

module.exports = router;

这样执行node server.js,在浏览器中输入localhost:8092,就能看到对应的请求了。

以上代码是get请求。

下面介绍post请求,修改server.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let express = require('express');
let app = express();
let router = express.Router();
let bodyParser = require('body-parser');

app.use(bodyParser.json());  //body-parser 解析json格式数据
app.use(bodyParser.urlencoded({//此项必须在 bodyParser.json 下面,为参数编码
    extended: true
}));

app.get('/',(req, res)=>{//这是一个非常简单的get方式的请求
    res.send('hello world');
});

router.use('/test', require('./test'));

app.use('/api', router);

app.listen(8092);

相应的fetch请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const params = {
    id: "id",
}
fetch("/api/test/profile", {
    method: "POST",
    credentials: 'include',
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify(params)
}).then((response) => {
    console.log(response);
    return response.json()
}).then((response) => {
    console.log(response)
}).catch((error) => {
    console.log(error)
});

注意要用express要用4.x版本。

那么实现了以上功能,我们就可以实现在项目中配置一些请求资源,然后在本地mock请求的功能了。

————————————————–正文开始————————————————–

我们先在项目中配置mock资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mockjs = require('mockjs');

module.exports = {
  // 支持值为 Object 和 Array
  'GET /api/users 3000': { users: [1, 2] },

  // GET POST 可省略

  'GET /api/users/1': { id: 1, name: 2, cdd: 3 },
  // 支持自定义函数,API 参考 express@4
  'POST /api/users/create': (req: any, res: any, next: any) => {
    console.log(res);
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.mockJson('OK'); // 必须要调用此方法 否则不处理结果
    next();
  },

  // 支持列表
  'GET /api/cityList': mockjs.mock({
    'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
  }),
};

这里mockjs是一个常用的mock数据的库,可以用它来模拟一些复杂数据类型的结果,它还包括捕获请求的功能等。

然后我们的开发思路是:

  1. 获取mock下的ts或js文件,遍历,先将文件通过ts的api转成浏览器支持的语法(es5),然后获取到其中module.exports的内容,组合成一个map;
  2. 整理map中的数据,格式化成由请求方式, 请求路径, 请求延时, re, keys, 请求的数据处理 组成的对象;
  3. 通过app.use 发起请求。

以下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import posix from 'path';
import glob from 'glob';
import bodyParser from 'body-parser';
import pathToRegexp from 'path-to-regexp';
import chokidar from 'chokidar';
import PluginAPI from '../../../core/PluginAPI';
import { invariant, error } from '../../../shared';
import { babelRegister } from '../../../shared/registerBabel';
import { RequestHandler, Request } from 'express';

const VALID_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
const BODY_PARSED_METHODS = ['post', 'put', 'patch'];

function getMockMiddleware(mockDirPath: string): RequestHandler {
    babelRegister({//这一步将mock文件夹下的文件内容编译成浏览器识别的语言
        only: [mockDirPath],
        babelPreset: [
          require.resolve('@souche-f2e/babel-preset-muji'),
          { modules: 'commonjs' },
        ],
      });
    let mockData = getMockData();//先根据项目中已有的mock文件获取mockData
    createWatcher();

  function createWatcher() {
    const watcher = chokidar.watch([mockDirPath], {//这里监听mock文件夹下文件内容的变化 有改动就执行getMockData方法 更新mockData
      ignored: /(^|[/\])\../, // ignore .dotfiles
      ignoreInitial: true,
    });
    watcher.on('all', () => {
      mockData = getMockData();
    });
  }

  function getMockData() {
    cleanRequireCache();
    let result = {};
    const mockFiles = glob.sync('**/*.{ts,js}', { cwd: mockDirPath });
    try {
      result = mockFiles.reduce((memo, file) => {
        const mockFile = posix.join(mockDirPath, file);
        const mod = require(mockFile);
        Object.assign(memo, mod && mod.__esModule ? mod.default : mod);
        return memo;
      }, {});
    } catch (err) {
      error('Mock file parse failed');
    }
    return normalizeConfig(result);
  }

  function normalizeConfig(config: { [k: string]: object | Function }) {
      //在此方法中对获取到的mock文件重的内容进行处理,比如将{'GET /api/users 3000': { users: [1, 2] },}
      //格式化为{method: 'GET', path: '/api/users', timeout: 3000, re: { /^\/api\/users\/1(?:\/(?=$))?$/i keys: [] }, keys: [], handler: Function}这样的数据
    return Object.keys(config).reduce<
      {
        method: string;
        path: string;
        timeout: number;
        re: RegExp;
        keys: { name: string }[];
        handler: Function;
      }[]
    >((memo, key) => {
      const handler = config[key];
      const type = typeof handler;
      invariant(
        type === 'function' || type === 'object',
        `mock value of ${key} should be function or object, but got ${type}`,
      );
      const { method, path, timeout } = parseKey(key);
      const keys: { name: string }[] = [];
      const re = pathToRegexp(path, keys);
      memo.push({
        method,
        path,
        timeout,
        re,
        keys,
        handler: createHandler(method, path, timeout, handler),
      });
      return memo;
    }, []);
  }

  function createHandler(
    method: string,
    path: string,
    timeout: number,
    handler: Function | object,
  ): RequestHandler {
      //创建请求的回调
    return function(req, res, next) {
        //由于post和get请求方式有区别 post要额外处理body 在这里做一下区分
      if (BODY_PARSED_METHODS.indexOf(method) > -1) {//这里匹配调用方式是否为'post', 'put', 'patch'中的一个
        bodyParser.json({ limit: '5mb', strict: false })(req, res, () => {
          bodyParser.urlencoded({ limit: '5mb', extended: true })(
            req,
            res,
            () => {
              sendData(timeout);
            },
          );
        });
      } else {
        sendData(timeout);
      }

      async function sendData(timeout: number) {
        await new Promise(r => {
          if (timeout) {
            setTimeout(r, timeout);
          } else {
            r();
          }
        });
        const mockRes = {
          code: 200,
          msg: 'success',
          success: true,
        };
        if (typeof handler === 'function') {
          const mockJSON = (data: any): void => {
            res.send({
              ...mockRes,
              data,
            });
          };
          const response: any = Object.create(res);
          response.mockJSON = mockJSON;
          handler(req, response, next);
        } else {
          res.json({
            ...mockRes,
            data: handler,
          });
        }
      }
    };
  }

  function parseKey(key: string) {
    //解析mock数据中的key
    //处理'GET /api/users 3000' 返回{method: 'get', path: '/api/users', timeout: 3000}
    let method = 'get';
    let path = key;
    let timeout = 0;
    if (key.indexOf(' ') > -1) {
      const splited = key.split(' ');
      method = splited[0].toLowerCase();
      path = splited[1]; // eslint-disable-line
      timeout = splited[2] ? Number(splited[2]) : 0;
    }
    invariant(
      VALID_METHODS.indexOf(method) > -1,
      `Invalid method ${method} for path ${path}, please check your mock files.`,
    );
    return {
      method,
      path,
      timeout,
    };
  }

  function cleanRequireCache() {
      //用来清除不存在的文件
    Object.keys(require.cache).forEach(file => {
      if (file.indexOf(mockDirPath) > -1) {
        delete require.cache[file];
      }
    });
  }

  function matchMock(req: Request) {
      //这里用来匹配请求的路径和调用方式,没有匹配上的模拟请求均按404处理
    const { path: exceptPath } = req;
    const exceptMethod = req.method.toLowerCase();

    for (const mock of mockData) {
      const { method, re, keys } = mock;
      if (method === exceptMethod) {
        const match = re.exec(req.path);
        if (match) {
          const params: any = {};

          for (let i = 1; i < match.length; i = i + 1) {
            const key = keys[i - 1];
            const prop = key.name;
            const val = decodeParam(match[i]);

            if (
              val !== undefined ||
              !Object.prototype.hasOwnProperty.call(params, prop)
            ) {
              params[prop] = val;
            }
          }
          req.params = params;
          return mock;
        }
      }
    }

    function decodeParam(val: any) {
      if (typeof val !== 'string' || val.length === 0) {
        return val;
      }

      try {
        return decodeURIComponent(val);
      } catch (err) {
        if (err instanceof URIError) {
          err.message = `Failed to decode param ' ${val} '`;
          (err as any).status = (err as any).statusCode = 400;
        }

        throw err;
      }
    }

    return mockData.filter(({ method, re }) => {//按条件返回mockData
      return method === exceptMethod && re.test(exceptPath);
    })[0];
  }

  return function MUJI_MOCK(req, res, next) {//这里真正的发送请求
    if (
      // 匹配其他前端请求或/
      !/\/$/.test(req.path) &&
      !/^.*\.(xls|woff2|woff|ttf|log|jpg|jpeg|gif|png|ico|html|cfm|cfc|afp|asp|lasso|pl|py|txt|fla|swf|zip|js|css|less)$/.test(
        req.path,
      )
    ) {
      const match = matchMock(req);
      if (match) {
        return match.handler(req, res, next);
      } // 服务端无此接口
      res.status(404).send({ error: '请仔细检查接口的路径及调用方式' });
    }
    return next();
  };
}

function setupMockMiddleware(api: PluginAPI, options: MujiOptions) {
  const middleware = getMockMiddleware(api.resolve('mock'));//入口 创建一个中间件
  api.configureDevServer(app => {
    app.use(middleware);//服务中调用
  });
}

export default setupMockMiddleware;