实战 - 打造实用的前端mock解决方案

Mock实现

效果

为了实现我们想要的mock的相关功能,首先是否开启mock的配置解耦可以通过上面说的方式来实现,我们一般在页面异步请求的时候都会目录定义一个io.js的文件, 里面定义了当前页面需要调用的相关后端接口:

// src/pages/login/login-io.jsimport {createIo} from ‘@src/io’

const apis = { // 登录 login: { method: ‘POST’, url: ‘/auth/login’, }, // 登出 logout: { method: ‘POST’, url: ‘/auth/logout’, },}export default createIo(apis, ‘login’) // 对应login-mock.json

上面定义了登录和登出接口,我们希望对应开启的mock请求能使用当前目录下的login-mock.json文件的内容。

// src/pages/login/login-mock.json{ “login”: { “failed”: { “success”: false, “code”: “ERROR_PASS_ERROR”, “content”: null, “message”: “账号或密码错误!” }, “success”: { “success”: true, “code”: 0, “content”: { “name”: “admin”, “nickname”: “超级管理员”, “permission”: 15 }, “message”: “” } }, “logout”: { “success”: { “success”: true, “code”: 0, “content”: null, “message”: “” } }}

在调用logout登出这个Ajax请求的时候且我们的conf.json中配置的是"login.logout": “success” 就返回login-mock.json中的login.success 的内容,配置没有匹配到就请求转发到后端服务。

// config/conf.json{ “title”: “前端后台模板”, “pathPrefix”: “/react-starter”, “apiPrefix”: “/api/react-starter”, “debug”: true, “delay”: 500, “mock”: { “login.logout”: “success” }}

这是我们最终要实现的效果,这里有一个约定:项目目录下所有以-mock.jsom文件结尾的文件为mock文件,且文件名不能重复。

思路

在webpack配置项中devServer的proxy配置接口的转发设置,接口转发使用了功能强大的 http-proxy-middleware 软件包, 我们约定proxy的配置格式是:

proxy: { “/api/react-starter/*”: { target: http://192.168.90.68:8888, changeOrigin: true, secure: true, // onError: (), // onProxyRes, // onProxyReq }, },

它有几个事件触发的配置:

  • option.onError 出现错误

  • option.onProxyRes 后端响应后

  • option.onProxyReq 请求转发前

  • option.onProxyReqWs

  • option.onOpen

  • option.onClose

所以我们需要定制这几个事情的处理,主要是请求转发前和请求处理后

onProxyReq

想在这里来实现mock的处理, 如果匹配到了mock数据我们就直接响应,就不转发请求到后端。怎么做呢:思路是依赖请求头,dev情况下前端在调用的时候能否注入约定好的请求头 告诉我需要寻找哪个mock数据项, 我们约定Header:

  • mock-key 来匹配mock文件如login-mock.json的内容, 如login

  • mock-method 来匹配对应文件内容的方法项 如logout

然后conf.json中mock配置寻找到具体的响应项目如:“login.logout”: "success/failed"的内容

onProxyRes

如果调用了真实的后端请求,就把请求的响应数据缓存下来,缓存到api-cache目录下文件格式mock-key.mock-method.json

├── api-cache # git 不跟踪│ ├── login.login.json│ └── login.logout.json

// api-cache/global.logout.json{ “success”: { “date”: “2020-11-17 05:32:17”, “method”: “POST”, “path”: “/render-server/api/logout”, “url”: “/render-server/api/logout”, “resHeader”: { “content-type”: “application/json; charset=utf-8”, … }, “reqHeader”: { “host”: “127.0.0.1:8888”, “mock-key”: “login”, “mock-method”: “logout” … }, “query”: {}, “reqBody”: {}, “resBody”: { “success”: true, “code”: 0, “content”: null, “message”: “” } }}

这样做的目的是为了后续实现一键生成mock文件。

前端接口封装
使用

上面我们看到定义了接口的io配置:

// src/pages/login/login-io.jsimport {createIo} from ‘@src/io’

const apis = { // 登录 login: { method: ‘POST’, url: ‘/auth/login’, }, // 登出 logout: { method: ‘POST’, url: ‘/auth/logout’, },}export default createIo(apis, ‘login’) // login注册到header的mock-key

我们在store中使用

// src/pages/login/login-store.js

import {observable, action, runInAction} from 'mobx’import io from ‘./login-io’// import {config, log} from ‘./utils’

export class LoginStore { // 用户信息 @observable userInfo // 登陆操作 @action.bound async login(params) { const {success, content} = await io.login(params) if (!success) return runInAction(() => { this.userInfo = content }) }}export default LoginStore

通过 createIo(apis, ‘login’) 的封装在调用的时候就可以非常简单的来传递请求参数,简单模式下会判断参数是到body还是到query中。复杂的也可以支持比如可以header,query, body等这里不演示了。

createIo 请求封装

这个是前端接口封装的关键地方,也是mock请求头注入的地方。

// src/io/index.jsximport {message, Modal} from 'antd’import {config, log, history} from '@src/common/utils’import {ERROR_CODE} from ‘@src/common/constant’import creatRequest from ‘@src/common/request’let mockData = {}try { // eslint-disable-next-line global-require, import/no-unresolved mockData = require(’@/mock.json’)} catch (e) { log(e)}

let reloginFlag = false// 创建一个requestexport const request = creatRequest({ // 自定义的请求头 headers: {‘Content-Type’: ‘application/json’}, // 配置默认返回数据处理 action: (data) => { // 统一处理未登录的弹框 if (data.success === false && data.code === ERROR_CODE.UN_LOGIN && !reloginFlag) { reloginFlag = true // TODO 这里可能统一跳转到 也可以是弹窗点击跳转 Modal.confirm({ title: ‘重新登录’, content: ‘’, onOk: () => { // location.reload() history.push(${config.pathPrefix}/login?redirect=${window.location.pathname}${window.location.search}) reloginFlag = false }, }) } }, // 是否错误显示message showError: true, message, // 是否以抛出异常的方式 默认false {success: boolean判断} throwError: false, // mock 数据请求的等待时间 delay: config.delay, // 日志打印 log,})

// 标识是否是简单传参数, 值为true标识复杂封装export const rejectToData = Symbol(‘flag’)

/** * 创建请求IO的封装 * @param ioContent {any { url: string method?: string mock?: any apiPrefix?: string}} } * @param name mock数据的对应文件去除-mock.json后的 /export const createIo = (ioContent, name = ‘’) => { const content = {} Object.keys(ioContent).forEach((key) => { /* * @param {baseURL?: string, rejectToData?: boolean, params?: {}, query?: {}, timeout?: number, action?(data: any): any, headers?: {}, body?: any, data?: any, mock?: any} * @returns {message, content, code,success: boolean} */ content[key] = async (options = {}) => { // 这里判断简单请求封装 rejectToData=true 表示复杂封装 if (!options[rejectToData]) { options = { data: options, } } delete options[rejectToData] if ( config.debug === false && name && config.mock && config.mock[${name}.${key}] && mockData[name] && mockData[name][key] ) { // 判断是否是生产打包 mock注入到代码中 ioContent[key].mock = JSON.parse(JSON.stringify(mockData[name][key][config.mock[${name}.${key}]])) } else if (name && config.debug === true) { //注入 mock请求头 if (options.headers) { options.headers[‘mock-key’] = name options.headers[‘mock-method’] = key } else { options.headers = {‘mock-key’: name, ‘mock-method’: key} } } const option = {…ioContent[key], …options}

option.url = ((option.apiPrefix ? option.apiPrefix : config.apiPrefix) || ‘’) + option.url

return request(option) } }) return content}

这里对request也做进一步的封装,配置项设置了一些默认的处理设置。比如通用的请求响应失败的是否有一个message, 未登录的情况是否有一个弹窗提示点击跳转登陆页。如果你想定义多个通用处理可以再创建一个request2和createIo2。

request封装axios

是基于axios的二次封装, 并不是非常通用,主要是在约定的请求失败和成功的处理有定制,如果需要可以自己修改使用。

import axios from ‘axios’

// 配置接口参数// declare interface Options {// url: string// baseURL?: string// // 默认GET// method?: Method// // 标识是否注入到data参数// rejectToData?: boolean// // 是否直接弹出message 默认是// showError?: boolean// // 指定 回调操作 默认登录处理// action?(data: any): any// headers?: {// [index: string]: string// }// timeout?: number// // 指定路由参数// params?: {// [index: string]: string// }// // 指定url参数// query?: any// // 指定body 参数// body?: any// // 混合处理 Get到url, delete post 到body, 也替换路由参数 在createIo封装// data?: any// mock?: any// }// ajax 请求的统一封装// TODO 1. 对jsonp请求的封装 2. 重复请求

/** * 返回ajax 请求的统一封装 * @param Object option 请求配置 * @param {boolean} opts.showError 是否错误调用message的error方法 * @param {object} opts.message 包含 .error方法 showError true的时候调用 * @param {boolean} opts.throwError 是否出错抛出异常 * @param {function} opts.action 包含 自定义默认处理 比如未登录的处理 * @param {object} opts.headers 请求头默认content-type: application/json * @param {number} opts.timeout 超时 默认60秒 * @param {number} opts.delay mock请求延迟 * @returns {function} {params, url, headers, query, data, mock} data混合处理 Get到url, delete post 到body, 也替换路由参数 在createIo封装 */export default function request(option = {}) { return async (optionData) => { const options = { url: ‘’, method: ‘GET’, showError: option.showError !== false, timeout: option.timeout || 60 * 1000, action: option.action, …optionData, headers: {‘X-Requested-With’: ‘XMLHttpRequest’, …option.headers, …optionData.headers}, } // 简单请求处理 if (options.data) { if (typeof options.data === ‘object’) { Object.keys(options.data).forEach((key) => { if (key[0] === ‘:’ && options.data) { options.url = options.url.replace(key, encodeURIComponent(options.data[key])) delete options.data[key] } }) } if ((options.method || ‘’).toLowerCase() === ‘get’ || (options.method || ‘’).toLowerCase() === ‘head’) { options.query = Object.assign(options.data, options.query) } else { options.body = Object.assign(options.data, options.body) } } // 路由参数处理 if (typeof options.params === ‘object’) { Object.keys(options.params).forEach((key) => { if (key[0] === ‘:’ && options.params) { options.url = options.url.replace(key, encodeURIComponent(options.params[key])) } }) } // query 参数处理 if (options.query) { const paramsArray = [] Object.keys(options.query).forEach((key) => { if (options.query[key] !== undefined) { paramsArray.push(${key}=${encodeURIComponent(options.query[key])}) } }) if (paramsArray.length > 0 && options.url.search(/?/) === -1) { options.url += ?${paramsArray.join('&')} } else if (paramsArray.length > 0) { options.url += &${paramsArray.join('&')} } } if (option.log) { option.log(‘request options’, options.method, options.url) option.log(options) } if (options.headers[‘Content-Type’] === ‘application/json’ && options.body && typeof options.body !== ‘string’) { options.body = JSON.stringify(options.body) } let retData = {success: false} // mock 处理 if (options.mock) { retData = await new Promise((resolve) => setTimeout(() => { resolve(options.mock) }, option.delay || 500), ) } else { try { const opts = { url: options.url, baseURL: options.baseURL, params: options.params, method: options.method, headers: options.headers, data: options.body, timeout: options.timeout, } const {data} = await axios(opts) retData = data } catch (err) { retData.success = false retData.message = err.message if (err.response) { retData.status = err.response.status retData.content = err.response.data retData.message = 浏览器请求非正常返回: 状态码 ${retData.status} } } }

// 自动处理错误消息 if (options.showError && retData.success === false && retData.message && option.message) { option.message.error(retData.message) } // 处理Action if (options.action) { options.action(retData) } if (option.log && options.mock) { option.log(‘request response:’, JSON.stringify(retData)) } if (option.throwError && !retData.success) { const err = new Error(retData.message) err.code = retData.code err.content = retData.content err.status = retData.status throw err } return retData }}

一键生成mock

根据api-cache下的接口缓存和定义的xxx-mock.json文件来生成。

“build-mock”: “node ./scripts/build-mock.js”# 所有:npm run build-mock mockAll # 单个mock文件:npm run build-mock login# 单个mock接口:npm run build-mock login.logout# 复杂 npm run build-mock login.logout user

具体代码参考build-mock.js

mock.json文件生成

为了在build打包的时候把mock数据注入到前端代码中去,使得mock.json文件内容尽可能的小,会根据conf.json的配置项来动态生成mock.json的内容,如果build里面没有开启mock项,内容就会是一个空json数据。当然后端接口代理处理内存中也映射了一份该mock.json的内容。这里需要做几个事情:

  • 根据配置动态生成mock.json的内容

  • 监听config文件夹 判断关于mock的配置项是否有改变重新生成mock.json

// scripts/webpack-init.js 在wenpack配置文件中初始化const path = require(‘path’)const fs = require(‘fs’)const {syncWalkDir} = require(‘./util’)let confGlobal = {}let mockJsonData = {}exports.getConf = () => confGlobalexports.getMockJson =() => mockJsonData

/** * 初始化项目的配置 动态生成mock.json和config/conf.json * @param {string} env dev|build */ exports.init = (env = process.env.BUILD_ENV ? ‘build’ : ‘dev’) => {

delete require.cache[require.resolve(‘…/config’)] const config = require(‘…/config’) const confJson = env === ‘build’ ? config.conf.build : config.conf.dev confGlobal = confJson // 1.根据环境变量来生成 fs.writeFileSync(path.join(__dirname, ‘…/config/conf.json’), JSON.stringify(confGlobal, null, ‘\t’)) buildMock(confJson) }

// 生成mock文件数据 const buildMock = (conf) => { // 2.动态生成mock数据 读取src文件夹下面所有以 -mock.json结尾的文件 存储到io/index.json文件当中 let mockJson = {} const mockFiles = syncWalkDir(path.join(__dirname, ‘…/src’), (file) => /-mock.jsonKaTeX parse error: Expected '}', got 'EOF' at end of input: …r(`有相同的mock文件名称{p.name} 存在`, filePath) } delete require.cache[require.resolve(filePath)] mockJson[mockKey] = require(filePath) }) // 如果是打包环境, 最小化mock资源数据 const mockMap = conf.mock || {} const buildMockJson = {} Object.keys(mockMap).forEach((key) => { const [name, method] = key.split(‘.’) if (mockJson[name][method] && mockJson[name][method][mockMap[key]]) { if (!buildMockJson[name]) buildMockJson[name] = {} if (!buildMockJson[name][method]) buildMockJson[name][method] = {} buildMockJson[name][method][mockMap[key]] = mockJson[name][method][mockMap[key]] } }) mockJsonData = buildMockJson fs.writeFileSync(path.join(__dirname, ‘…/mock.json’), JSON.stringify(buildMockJson, null, ‘\t’)) }

// 监听配置文件目录下的config.js和config_default.jsconst confPath = path.join(__dirname, ‘…/config’)

if ((env = process.env.BUILD_ENV ? ‘build’ : ‘dev’) === ‘dev’) { fs.watch(confPath, async (event, filename) => { if (filename === ‘config.js’ || filename === ‘config_default.js’) { delete require.cache[path.join(confPath, filename)] delete require.cache[require.resolve(‘…/config’)] const config = require(‘…/config’) // console.log(‘config’, JSON.stringify(config)) const env = process.env.BUILD_ENV ? ‘build’ : ‘dev’ const confJson = env === ‘build’ ? config.conf.build : config.conf.dev if (JSON.stringify(confJson) !== JSON.stringify(confGlobal)) { this.init() } } });}

接口代理处理
onProxyReq和onProxyRes

实现上面思路里面说的onProxyReq和onProxyRes 响应处理

util.js

// scripts/api-proxy-cache const fs = require(‘fs’)const path = require(‘path’)const moment = require(‘moment’)const {getConf, getMockJson} = require(‘./webpack-init’)const API_CACHE_DIR = path.join(__dirname, ‘…/api-cache’)const {jsonParse, getBody} = require(‘./util’)

fs.mkdirSync(API_CACHE_DIR,{recursive: true})

module.exports = { // 代理前处理 onProxyReq: async (_, req, res) => { req.reqBody = await getBody(req) const {‘mock-method’: mockMethod, ‘mock-key’: mockKey} = req.headers // eslint-disable-next-line no-console console.log(Ajax 请求: ${mockKey}.${mockMethod},req.method, req.url) // eslint-disable-next-line no-console req.reqBody && console.log(JSON.stringify(req.reqBody, null, ‘\t’)) if (mockKey && mockMethod) { req.mockKey = mockKey req.mockMethod = mockMethod const conf = getConf() const mockJson = getMockJson() if (conf.mock && conf.mock[${mockKey}.${mockMethod}] && mockJson[mockKey] && mockJson[mockKey][mockMethod]) { // eslint-disable-next-line no-console console.log(use mock data ${mockKey}.${mockMethod}:, conf.mock[${mockKey}.${mockMethod}], ‘color: green’) res.mock = true res.append(‘isMock’,‘yes’) res.send(mockJson[mockKey][mockMethod][conf.mock[${mockKey}.${mockMethod}]]) }

} }, // 响应缓存接口 onProxyRes: async (res, req) => { const {method, url, query, path: reqPath, mockKey, mockMethod} = req

if (mockKey && mockMethod && res.statusCode === 200) {

let resBody = await getBody(res) resBody = jsonParse(resBody) const filePath = path.join(API_CACHE_DIR, ${mockKey}.${mockMethod}.json) let data = {} if (fs.existsSync(filePath)) { data = jsonParse(fs.readFileSync(filePath).toString()) } const cacheObj = { date: moment().format(‘YYYY-MM-DD hh:mm:ss’), method, path: reqPath, url, resHeader: res.headers, reqHeader: req.headers, query, reqBody: await jsonParse(req.reqBody), resBody: resBody } if (resBody.success === false) { data.failed = cacheObj } else { data.success = cacheObj } // eslint-disable-next-line no-console fs.writeFile(filePath, JSON.stringify(data,‘’, ‘\t’), (err) => { err && console.log(‘writeFile’, err)}) } }, // 后端服务没启的异常处理 onError(err, req, res) { setTimeout(() => { if (!res.mock) { res.writeHead(500, { ‘Content-Type’: ‘text/plain’, }); res.end(‘Something went wrong. And we are reporting a custom error message.’); } }, 10) }}

webpack配置

在webpack配置中引入使用

const config = require(‘.’)// config/webpack.config.jsconst {init} = require(‘…/scripts/webpack-init’);init();// 接口请求本地缓存const apiProxyCache = require(‘…/scripts/api-proxy-cache’)for(let key in config.proxy) { config.proxy[key] = Object.assign(config.proxy[key], apiProxyCache);}

const webpackConf = { devServer: { contentBase: path.join(__dirname, ‘…’), // 本地服务器所加载的页面所在的目录 inline: true, port: config.port, publicPath: ‘/’, historyApiFallback: { disableDotRule: true, // 指明哪些路径映射到哪个html rewrites: config.rewrites, }, host: ‘127.0.0.1’, hot: true, proxy: config.proxy, },}

总结

mock做好其实在我们前端实际中还是很有必要的,做过的项目如果后端被铲除了想要回忆就可以使用mock让项目跑起来,可以寻找一些实现的效果来进行代码复用。

当前介绍的mock流程实现有很多定制的开发,但是真正完成后,团队中的成员只是使用还是比较简单配置即可。

关于前端项目部署我也分享了一个BFF 层,当前做的还不是很完善,也分享给大家参考

Render-Server 主要功能包含:

  • 一键部署 npm run deploy

  • 支持集群部署配置

  • 是一个文件服务

最后

面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

96道前端面试题:

常用算法面试题:

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

96道前端面试题:

  • [外链图片转存中…(img-BqMI31m3-1714651961591)]

常用算法面试题:

  • [外链图片转存中…(img-7AmTRJYi-1714651961593)]

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值