前端 mock 数据实践

在项目中一般使用 Mock 创建假数据,Mock 有提供拦截请求响应数据的功能 Mock.mock() ,但是 Mock 拦截就导致在 Network 中无法进行数据联调,只能通过 console 打印,使用起来就比较麻烦。这个时候我们可以使用 koa 起本地服务,再通过 Mock 生成数据,前端项目将请求代理到 koa 本地服务,这样就可以在 Network 中进行数据联调了。

eMock 使用

项目地址:eMock,示例地址:use-eMock

将 eMock 项目克隆到本地,在前端项目中新建一个文件夹例如 eMock ,将 eMock 项目中除了 .gitnode_modules 文件夹的其他所有文件拷贝到前端项目的 eMock 文件夹下,在 eMock 文件夹下 yarn 安装依赖,yarn dev 启动服务,服务端口是 8888 ,之后在前端项目中直接配置代理,这里演示 vue cli 创建的项目怎么配置:

vue.config.js 文件:

module.exports = {
  ......
  ......
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8888',
        ws: true,
        changeOrigin: true
      }
    }
  }
  ......
  ......
}

eMock 简介

eMock 采用 koa + Mock 生成 Mock 数据,yarn dev 实际执行的是 nodemon --watch index --watch controller.js --watch global.js --watch ./controllers/*nodemon 可以 watch 文件或者文件夹当内容有修改的时候,重启服务。

index.js 入口文件:

const Koa = require('koa')
const chalk = require('chalk')
const bodyParser = require('koa-bodyparser')

const global = require('./global.js').global
const controller = require('./controller')

const app = new Koa()

// 全局公共变量及方法
app.context.global = global

// log request URL:
app.use(async (ctx, next) => {
  console.log(chalk.blue(`Process ${ctx.request.method} ${ctx.request.url}...`))
  await next()
})

// parse request body:
app.use(bodyParser())

// add controllers:
app.use(controller())

app.listen(8888)
console.log(chalk.green('app started at port 8888...'))

controller.js 文件自动读取 controllers 文件夹及目录下的js文件,并自动注册 router ,其中 chalk 是修改 console 打印颜色:

// https://github.com/michaelliao/learn-javascript/blob/master/samples/node/web/koa/url2-koa/controller.js

const fs = require('fs')
const path = require('path')
const chalk = require('chalk')

/**
 * add url-route in /controllers:
 * @param {Object} router require('koa-router')()
 * @param {Object} mapping require(__dirname + '/' + dir + '/' + f)
 */
function addMapping(router, mapping) {
  for (const url in mapping) {
    if (url.toLowerCase().startsWith('get ')) {
      const path = url.substring(4)
      router.get(path, mapping[url])
      console.log(chalk.green(`register URL mapping: ${chalk.yellow('get')} ${path}`))
    } else if (url.toLowerCase().startsWith('post ')) {
      const path = url.substring(5)
      router.post(path, mapping[url])
      console.log(chalk.green(`register URL mapping: ${chalk.yellow('post')} ${path}`))
    } else if (url.toLowerCase().startsWith('put ')) {
      const path = url.substring(4)
      router.put(path, mapping[url])
      console.log(chalk.green(`register URL mapping: ${chalk.yellow('put')} ${path}`))
    } else if (url.toLowerCase().startsWith('delete ')) {
      const path = url.substring(7)
      router.del(path, mapping[url])
      console.log(chalk.green(`register URL mapping: ${chalk.yellow('delete')} ${path}`))
    } else {
      console.log(chalk.red(`invalid URL: ${url}`))
    }
  }
}

/**
 * addControllers
 * @param {Object} router require('koa-router')()
 * @param {String} dir path
 */
function addControllers(router, dir) {
  const fullpath = path.join(__dirname + '/' + dir)
  listFile(router, fullpath)
}

/**
 * 递归循环深层目录便利
 * @param {Object} router require('koa-router')()
 * @param {String} dirPath path
 */
function listFile(router, dirPath) {
  const arr = fs.readdirSync(dirPath)
  arr.forEach(function (item) {
    const fullpath = path.join(dirPath, item)
    const stats = fs.statSync(fullpath)
    if (stats.isDirectory()) {
      listFile(router, fullpath);
    } else {
      console.log(chalk.blue(`process controller: ${fullpath}...`))
      const mapping = require(fullpath)
      addMapping(router, mapping)
    }
  });
}

module.exports = function (dir) {
  const controllers_dir = dir || 'controllers'
  const router = require('koa-router')()
  addControllers(router, controllers_dir)
  return router.routes()
}

再来查看 global.js 文件,如果有约定的不同状态下返回不同的 code 值,也可以放在 global.js 文件中:

/** 存放公共变量,供所有使用 */
class Global {
  global = {
    // 模拟自动登录的时候,首次登录之后,将用户添加到 users 中,后续就可以模拟 cookie 自动登录
    users: [
      {
        cookie: 'cc077e4074d58b5b3afe96921b220364',
        name: 'fxss'
      }
    ],
    // 登录用户的 cookies
    cookies: []
  }

  /**
   * 设置cookie
   * @param {string} val 新的cookie值 
   */
  setCookie (val) {
    const cookies = this.global.cookies.concat(val)
    this.global.cookies = [...new Set(cookies)]
  }

  /**
   * 删除cookie
   * @param {string} val cookie值 
   */
  deleteCookie (val) {
    this.global.cookies.splice(this.global.cookies.indexOf(val), 1)
  }

  /**
   * 检查当前cookie是否在cookies中
   * @param {string} val 当前cookie
   * @returns true: 在cookies中,false:不在cookies中
   */
  isInCookies (val) {
    return this.global.cookies.indexOf(val) !== -1
  }

  /**
   * 获取所有的公共变量
   * @returns 所有的公共变量
   */
  getGlobal () {
    return this.global
  }

  /**
   * 使用当前cookie获取用户信息
   * @param {string} val 当前cookie
   * @returns 当前cookie对应的用户信息
   */
  getUserByCookie (val) {
    let res = {}
    for (let index = 0, length = this.global.users.length; index < length; index++) {
      if (this.global.users[index].cookie === val) {
        res = this.global.users[index]
        break
      }
    }
    return res
  }
}

const global = new Global()

module.exports = {
  global
}

公共文件就足够了,下面我们看下 controllers 内的文件,首先查看登录退出功能:

login.js 登录功能:

const md5 = require('blueimp-md5')

// 正常用户名密码登录
const loginFn = async (ctx, next) => {
  const postData = ctx.request.body.data

  // 这里只是模拟生成 cookie ,然后将其添加到公共变量中
  ctx.global.setCookie(md5(postData.password))
  ctx.response.type = 'json'
  ctx.response.body = ({
    user: {
      name: postData.name,
      cookie: md5(postData.password)
    }
  })
  next()
}

// 根据 cookie 登录
const loginByCookieFn = async (ctx, next) => {
  const user = ctx.global.getUserByCookie(ctx.cookies.get('mockCookie'))
  
  ctx.response.type = 'json'
  ctx.response.body = ({
    user: user
  })
  next()
}

module.exports = {
  'post /api/login': loginFn,
  'post /api/loginByCookie': loginByCookieFn
}

logout.js 退出功能:

// 退出登录
const logoutFn = async (ctx, next) => {
  // 删除公共变量中的 cookie
  ctx.global.deleteCookie(ctx.cookies.get('mockCookie'))
  ctx.response.type = 'json'
  ctx.response.body = ({
    msg: '退出成功'
  })
  next()
}

module.exports = {
  'post /api/logout': logoutFn
}

然后再查看文章的增删改查:

const Mock = require('mockjs')
const Random = Mock.Random

// 生成文章自增 id
const articles = Mock.mock({
  'list|100': [{
    'id|+1': 0
  }]
})

let articleList = articles.list

// 为文章填充标题、内容、时间、作者
articleList = articleList.map(item => {
  const title = Random.ctitle(3, 10)
  const description = `${title}${Random.cparagraph()}`
  return {
    id: item.id === 0 ? 100 : item.id, // 此处是为了模拟新增文章在最前面,新增文章 id 由第一个文章的 id + 1 得到
    title,
    description,
    time: Random.datetime('yyyy-MM-dd HH:mm:ss'),
    author: Random.cname()
  }
})

// 获取文章列表
const getArticlesFn = async (ctx, next) => {
  const currentPage = ctx.query.currentPage * 1
  const pageSize = ctx.query.pageSize * 1

  // 分页获取数据
  const list = articleList.filter((item, index) => index >= (currentPage - 1) * pageSize && index < currentPage * pageSize)
  ctx.response.type = 'json'
  ctx.response.body = ({
    list,
    total: articleList.length
  })
  next()
}

/**
 * 由文章 id 获取当前文章所在的 index
 * @param {string} id 文章 id
 * @returns 当前文章所在的 index
 */
function getArticlesIndexById(id) {
  let res = -1
  for (let i = 0, len = articleList.length; i < len; i++) {
    if (articleList[i].id === id) {
      res = i
      break
    }
  }
  return res
}

// 根据文章 id 获取文章获取文章详情
const getArticlesByIdFn = async (ctx, next) => {
  const articleIndex = getArticlesIndexById(ctx.params.id * 1)

  // 可以在此处进行文章是否存在的判断 articleIndex != -1
  ctx.response.type = 'json'
  ctx.response.body = ({
    article: articleList[articleIndex]
  })
  next()
}

// 新增文章
const postArticlesFn = async (ctx, next) => {
  const postData = ctx.request.body.data
  const id = articleList[0].id + 1
  articleList.unshift({
    id,
    ...postData
  })
  ctx.response.type = 'json'
  ctx.response.body = ({
    msg: '添加成功'
  })
  next()
}

// 更新文章
const putArticlesByIdFn = async (ctx, next) => {
  const articleIndex = getArticlesIndexById(ctx.params.id * 1)
  const postData = ctx.request.body.data
  articleList.splice(articleIndex, 1, postData)
  ctx.response.type = 'json'
  ctx.response.body = ({
    msg: '编辑成功'
  })
  next()
}

// 删除文章
const deleteArticlesByIdFn = async (ctx, next) => {
  const articleIndex = getArticlesIndexById(ctx.params.id * 1)
  articleList.splice(articleIndex, 1)
  ctx.response.type = 'json'
  ctx.response.body = ({
    msg: '删除成功'
  })
  next()
}

module.exports = {
  'get /api/articles': getArticlesFn,
  'get /api/articles/:id': getArticlesByIdFn,
  'post /api/articles': postArticlesFn,
  'put /api/articles/:id': putArticlesByIdFn,
  'delete /api/articles/:id': deleteArticlesByIdFn
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值