手写服务器、数据Mock、前后端联调


大纲链接 §

[toc]


前端在开发中如何mock数据

  • 涉及与后端交互,发送Ajax请求,从后台获取数据/向后端提交数据
  • 前端按照接口文档模拟接口数据,实现前后端同步开发
  • 模拟的数据和后端真实数据遵循同样的格式,内容无关紧要
  • 通过模拟数据的字段,将数据展示到页面中
  • 当后端完成接口后,通过联调,替换真实接口

准备

  • 安装node14+ 或者安装node16

1. Node.js手写服务器Mock数据

1.1 使用node.js手写server

20行代码实现server:server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 加载 node 模块
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块

const server = http.createServer((req, res) => {

}).listen(8080) // 监听端口

console.log('open http://localhost:8080'); // 运行`node server.js` ,打开地址,浏览器一直处于等待响应的状态

  • 命令行运行node server.js
  • 实现模拟后端
  • 前端发送请求至模拟后端服务器,给出对应的响应
  • 调用http.createServer(()=>{}).listen(8080)创建server
  • 监听8080端口
  • 服务器是一个运行在机器上的软件,监听一些端口
  • 当端口监听到到请求时,接收请求并发出响应
  • 服务器时刻等待请求,做出响应的一个运行中的软件

server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 加载 node 模块
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  res.end('hello server')
}).listen(8080) // 运行时一直监听8080端口

console.log('open http://localhost:8080');
  • 运行时一直监听8080端口
  • 当浏览器输入地址http://localhost:8080时,请求到服务器上
  • 请求对象req,请求相关信息
  • 响应对象res:要发送响应的对象
  • res.end('hello server') 发送响应给浏览器

1.2 打印urlObj url字符串解析对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 加载 node 模块
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块 将url字符串解析成一个对象

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  let urlObj = url.parse(req.url)
  console.log(urlObj);
  res.end('hello server')
}).listen(8080) // 监听端口

console.log('open http://localhost:8080');

  • const url = require('url'):解析url解析模块
  • url.parse(req.url)将url字符串解析成一个对象
  • 运行node server.js可以看到以下内容:
 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
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: '/'
}
// 浏览器默认自动发送请求
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: '/favicon.ico',
  path: '/favicon.ico',
  href: '/favicon.ico'
}
  • 在地址栏添加输入任意字符http://localhost:8080/aabbcc后查看可打印以下内容:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Url {
  protocol: null,     
  slashes: null,      
  auth: null,
  host: null,
  port: null,
  hostname: null,     
  hash: null,
  search: null,       
  query: null,        
  pathname: '/aabbcc',
  path: '/aabbcc',    
  href: '/aabbcc'     
}
  • 在地址栏添加输入任意字符http://localhost:8080/123/abc.html后查看可打印以下内容:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: '/123/abc.html',
  path: '/123/abc.html',
  href: '/123/abc.html'
}
  • pathname: '/123/abc.html'
  • pathObj.pathname请求路径参数:指http://localhost:8080/xxx斜杠后的内容(包括斜杠)
  • 也可以包括查询参数http://localhost:8080/123/abc.html?a=1&b=2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?a=1&b=2',
  query: 'a=1&b=2',
  pathname: '/123/abc.html',
  path: '/123/abc.html?a=1&b=2',
  href: '/123/abc.html?a=1&b=2'
}

1.3 写一个简单地HTML index.html 模拟页面发起请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Server</title>
</head>
<body>
<p>天气: <span></span></p>
<script>
  const xhr = new XMLHttpRequest()
  xhr.open(
    'GET',
    'http://localhost:8080/getWeather',
    true
  )
  xhr.onload = () => {
    document.querySelector('span').innerText = JSON.parse(xhr.responseText).data
  }
  xhr.send()
</script>
</body>
</html>

server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 加载 node 模块
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块 将url字符串解析成一个对象

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  let urlObj = url.parse(req.url)

  // 读取文件
  res.end(fs.readFileSync(__dirname + '/index.html'))

}).listen(8080) // 监听端口

console.log('open http://localhost:8080/index.html')

  • 读取文件fs.readFileSync(__dirname + '/...')
    • 其中fs.readFileSync()方法表示读取
    • __dirname表示当前路径
      • 如果在package.json中设置了"type": "module",的话,注意There is no __dirname when using ESM modules
      • 添加import * as path from 'path'const __dirname = path.resolve(path.dirname(''));
    • 读取index.htmlres.end(fs.readFileSync(__dirname + '/index.html'))
  • url.parse(req.url)node.js 15+后弃用,改为let urlObj = new URL(`${req.url}`, 'http://localhost:8080/')
  • 运行node server.js
  • 在地址栏输入http://localhost:8080/index.html查看network面板

1.4 改为使用ES6模块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 加载 node 模块
/*
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块 将url字符串解析成一个对象
 */
// 转换为ES6模块
import http from 'http' // 创建服务器
import fs from 'fs' // 文件读取模块
import * as path from 'path'

const __dirname = path.resolve(path.dirname(''));

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  let urlObj = new URL(`${req.url}`, 'http://localhost:8080/')
  // There is no __dirname when using ESM modules
  res.end(fs.readFileSync(__dirname + '/index.html'))

}).listen(8080, () => {
  console.log('点击打开: http://localhost:8080/index.html')
}) // 监听端口

  • 读取了index.html并展示到浏览器中,现在的server.js启动服务展示了index.html页面
  • 模拟Ajax请求
  • res.end(JSON.stringify())发出响应,传输字符串
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 加载 node 模块
import http from 'http' // 创建服务器
import fs from 'fs' // 文件读取模块
import * as path from 'path'

const __dirname = path.resolve(path.dirname(''))

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  let urlObj = new URL(`${req.url}`, 'http://localhost:8080/')

  if (urlObj.pathname === '/getWeather') {
    res.end(JSON.stringify({data: '晴天'}))
  } else {
    res.end(fs.readFileSync(__dirname + '/index.html'))
  }

}).listen(8080, () => {
  console.log('点击打开: http://localhost:8080/index.html')
}) // 监听端口

  • 浏览器打开http://localhost:8080/index.html,向服务器发送请求,请求的urlObj.pathname/,代表当前主页
  • 此时urlObj.pathname/,返回读取当前路径下的index.html文件
    • 获取文件的绝对路径__dirname + '/index.html'
    • 读取文件内容fs.readFileSync(__dirname + '/index.html')
    • 发送响应res.end(fs.readFileSync(__dirname + '/index.html')),即发送首页内容
  • 浏览器解析首页,执行script标签中的JS代码
    • JS发送Ajax请求xhr.open('GET', 'http://localhost:8080/getWeather', true)到服务器server.js
  • 此时urlObj.pathname/getWeather,返回数据{data: '晴天'}

小结

  • 此时的服务器server.js既能够支持当前index.html的页面展示,也能支持当前需要用到的接口

1.5 功能完善

index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Server</title>
</head>
<body>
<p>天气: <span></span></p>
<script>
  const xhr = new XMLHttpRequest()
  xhr.open(
    'GET',
    'http://localhost:8080/getWeather?city=beijing',
    true
  )
  xhr.onload = () => {
    document.querySelector('span').innerText = JSON.parse(xhr.responseText).city + JSON.parse(xhr.responseText).weather
  }
  xhr.send()
</script>
</body>
</html>

node-server/server.js

 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
// 加载 node 模块
/*
const http = require('http') // 创建服务器
const fs = require('fs') // 文件读取模块
const url = require('url') // url解析模块 将url字符串解析成一个对象
 */
// 转换为ES6模块
import http from 'http' // 创建服务器
import fs from 'fs' // 文件读取模块
import * as path from 'path'

const __dirname = path.resolve(path.dirname(''))

const server = http.createServer((req /*请求对象*/, res /*响应对象*/) => {
  let pathObj = new URL(`${req.url}`, 'http://localhost:8080/')
  // console.log(pathObj)
  // console.log(pathObj.searchParams.get('city'))
  if (pathObj.pathname === '/getWeather') {
    pathObj.searchParams.get('city') === 'beijing'
      ? res.end(JSON.stringify({city: '北京', weather: '晴天'}))
      : res.end(JSON.stringify({city: pathObj.searchParams.get('city'), weather: '未知'}))
  } else {
    try {
      // 默认为index.html
      let pathname = pathObj.pathname === '/' ? '/index.html' : pathObj.pathname
      // 读取非index.html文件的内容
      res.end(fs.readFileSync(__dirname + pathname))
    } catch (e) {
      res.writeHead(404, 'Note Found')
      res.end('<h1>404 Page Not Found</h1>')
    }

  }

}).listen(8080, () => {
  console.log('点击打开: http://localhost:8080/index.html')
}) // 监听端口

  • 运行node node-server/server.js
  • 默认首页/,判断请求路径是否为/index.html
  • 注意关闭上一个server.js,避免已使用端口address already in use :::8080
  • 如果路径不为//index.html,就读取路径下的文件res.end(fs.readFileSync(__dirname + pathname))
  • 如果读取文件失败,就走catch逻辑
  • 服务器端告诉浏览器的状态码与提示文字:res.writeHead(404, 'Note Found')
  • pathObj.searchParams.get('city') === 'beijing':返回一个Map类型的数据,使用Map.prototype.get方法获取值

缺点

  • 难以和后端沟通联调,统一接口文档

1.6 拓展:高级用法,手写Express.js

从实现简易Server到实现node后端框架Express.js


2 Mock.js 和 Mock 平台介绍和用法

安装引入Mock.js,在本地模拟数据

2.1 Mock.js用法

预先定义好数据范例格式,随机生成数据

2.2 使用 Mock.js 构造随机数据示例

构造重复的字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 'name|min-max': string
Mock.mock({
  "welcome|1-3": "滚"
})
/*
{
  "welcome": "滚滚滚"
}
*/

/*
Mock.mock({
  "表示属性名称|生成规则": 数据类型
})
*/
  • 表示生成数据对象的属性名称:合法字符串即可
  • 生成规则:比如 规定范围、自增+1等
  • 数据类型:真正决定生成数据类型

生成小数

1
2
3
4
5
// 'name|min-max.dmin-dmax': number
// 'name|num.dmin-dmax': number
Mock.mock({
  "price|1-100.0-2": 1
})

Boolean

1
2
3
Mock.mock({
  "idOk|1": true
})

生成对象,包含任意可选属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 'name|min-max': object
Data Template
Mock.mock({
  "邮编|2-4": {
    "110000": "北京市",
    "120000": "天津市",
    "130000": "河北省",
    "140000": "山西省"
  }
})

生成从数组中挑选任意个数的数据组成的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 'name|1': array
// 'name|+1': array 表示遍历数组
// 'name|1-10': array 表示范围规定的个数的数据
Mock.mock({
  "array|1": [
    "AMD",
    "CMD",
    "UMD"
  ]
})

Mock.mock({
  "weather|1": [
    "毛毛雨",
    "大雨",
    "十年一遇大雨",
    "百年一遇大雨",
    "千年一遇大雨",
  ]
})

按正则生成

1
2
3
4
// 'name': regexp
Mock.mock({
  'password': /[a-z][A-Z][0-9]/
})

使用 Mock.js 数据占位符

Mock.mock('@...')

  • 常用占位符有:
    • @integer
    • @boolean
    • @string
    • @date
    • @time
    • @datetime
    • @now
    • @image(100x100)
    • @paragraph
    • @cparagraph
    • @name
    • @cname
    • @cword
    • @ctitle(3, 10)
    • @guid
    • @email
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  'statusCode|1': [1, 2, 3, 4],
  'msg|1': '@cword(4, 10)',
  'data|4': [
    {
      id: '@id',
      title: '@ctitle',
      author: '@cname',
      createdAt: '@datetime',
    }
  ]
}

3. 阿里Rap2 Mock平台的使用

淘宝 Rap2 平台

  • 使用不同模块来区分不同类型的功能,比如登录、注册、订单、地址管理模块等
  • 新建仓库->参考示例模块->新建模块->新建接口
  • 所有新建名称一般为英文,可添加中文描述

编辑接口,添加响应

  • 编写相应内容:
    • 名称
    • 类型
    • 初始值
    • 简介
  • 保存

新建接口 getUserInfo

  • 名称 getUserInfo
  • 地址 /getUserInfo
  • 类型 GET
  • 状态码 200
  • 简介 获取用户信息

编辑接口

  • 添加 请求参数
    • 如果类型是 GET,则请求参数放在HeadersQuery Params中,Query Params更常用
    • 如果类型是 POST,则请求参数放在Query Params
    • 前端向此接口发请求时,必须带上这个 请求参数,否则报错
  • 添加 响应内容
    • 名称 name
    • 必选
    • 类型 String
    • 生成规则
    • 初始值 @cname@canme
    • 简介
  • 保存

@cname响应模板的随机数据格式

  • 表示中文名字

Mock.js 语法规范

导出接口

  • 可选择导出格式为markdown的接口文档,给前后端分别按文档去实现接口和mock接口,保证联调效率

后端接口检测

rap2无法检测后端接口是否符合规定的格式


沟通确定接口文档

4. 前后端接口规范

接口文档

和后端沟通的过程中,必须确定好接口,根据业务分好模块,根据模块编写对应的接口规则,最终产出文档

  • 前端根据接口文档去mock数据,开发页面
  • 后端根据接口,开发后端功能
  • 前后端联调
  • 需求发生变动,更新接口文档版本,以文档为准

接口约定

  • 当前接口的路径,例如:/auth/register
  • 当前接口提交数据的类型 即请求动词,(语义化接口,即RESTful风格)例如:
    • GET 获取数据
    • POST 提交或者创建数据
    • PATCH 修改数据(部分修改)
    • DELETE 删除数据
    • PUT 修改数据(整体替换原有数据)
  • 参数类型/格式,(后端接受和处理方式不同,影响发送请求是否能够被收到)例如:
    • from-datajson 格式或者 application/x-www-form-urlencoded
  • 参数字段(语义化接口可减少字段的数量),以及限制条件,例如:用户名、密码等
    • 前端mock数据必须覆盖所有限制条件,包括成功和失败的所有情况
  • 返回成功的数据格式
  • 返回失败的数据格式

多人共享博客项目的接口文档


5. 使用curl命令行快速测试已经运行的后端接口

当后端接口完成时,还未放入项目代码中,接口对应的UI展示和功能未完成,可以使用命令行对接口进行简单地测试

  • 按接口文档约定的格式、请求动词、对应参数发送请求的时候,返回的响应是否符合规则

发送GET请求

1
2
3
curl "http://rap2api.taobao.org/app/mock/244238/getWeather?city=beijing"
curl "http://rap2api.taobao.org/app/mock/295128/weather?city=%E7%BA%BD%E7%BA%A6"

发送POST请求

1
2
3
4
# -d 表示提交的参数,默认是POST请求
curl -d "username=aaaa&password=bbb" "http://rap2api.taobao.org/app/mock/244238/login"
curl -d "username=aaaa&password=bbb" "http://rap2api.taobao.org/app/mock/295128/login"

  • 加上 -d修饰符,后带所需要的 请求信息,默认为POST请求

发送拥有权限的POST请求

  • 需要用户登录的信息
  • 模拟用户的登录,调用接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# -i 展示响应头
curl -d "username=hunger12&password=123456" "http://blog-server.hunger-valley.com/auth/login" -i

# -H 设置请求头
curl -H "Content-Type:application/json" -X POST -d '{"user":"admin","passwd":"12345678"}'
http://127.0.0.1:8000/login

# -X 设置请求类型
curl -d "username=aaaa&password=bbb" -X POST "http://rap2api.taobao.org/app/mock/244238/login"

# -b 请求带上 cookie
curl "http://blog-server.hunger-valley.com/auth" -b "connect.sid=s%3AmeDbrn03UtTM8fqChaPQ20wmWlnKeHiu.e3uMtu7j1zQ1iNeaajCmxkYYGQ%2FyHV1ZsozMvZYWC6s"

常用curl发送POST请求修饰符

  • -i 展示响应头,可放命令行在末尾
    • 例如登录响应成功时,响应头会带着 token 字段或者 set-cookie 字段,即用户的身份标识
    • "token":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh1bmdlcjEyIiwiaWQiOjgzLCJpYXQiOjE2Mzg5OTkyNzUsImV4cCI6MTYz OTI1ODQ3NX0.NYvilBRaPcWqXc3sspuZKk_y1DAkatcOunkRKDyd1Vo"
    • 验证后续的接口时,发送请求时带上这个字段,就会让服务器识别为登录状态
    • 格式为 curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" https://{hostname}/api/myresource
    • 例如: curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh1bmdlcjEyIiwiaWQiOjgzLCJpYXQiOjE2MzkwMDI4MDksImV4cCI6MTYzOTI2MjAwOX0.yyp-DzFe1XZagIjzCiBnbTDYMldT4dyU4Xy_35-BhZ4" "http://blog-server.hunger-valley.com/auth"
    • google 搜索 curl with token JWT
  • -H 设置请求头信息 "Content-Type:application/json"
    • 在请求头里可设置 JWT Token 信息代替cookie
    • curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imh1bmdlcjEyIiwiaWQiOjgzLCJpYXQiOjE2MzkwMDI4MDksImV4cCI6MTYzOTI2MjAwOX0.yyp-DzFe1XZagIjzCiBnbTDYMldT4dyU4Xy_35-BhZ4" "http://blog-server.hunger-valley.com/auth"
  • -X 设置请求类型 请求动词
  • -b 请求带上 cookie -b "connect.sid=s%3AmeDbrn03UtTM8fqChaPQ20wmWlnKeHiu.e3uMtu7j1zQ1iNeaajCmxkYYGQ%2FyHV1ZsozMvZYWC6s"

使用 POSTMAN、Apifox 或者直接WebStorm自带 http 客户端

  • 测试接口返回不符合文档预期,可及时向后端反馈
  • 实时跟踪反馈
  • 自动生成接口文档


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名