Front end mock perfect solution


Written in the front, this article needs some knowledge about nodejs, because it will expand the related functions of webpack, and the implementation needs to comply with certain conventions and Ajax encapsulation. The scaffolding of precipitation is also put on GitHub for students’ referenceReact-StarterThe user’s manual has not been completed yet. The overall idea has nothing to do with react or Vue. If you have any harvest, remember star.
It has these functions:

  • There are different configurations for development packaging
  • Eslint verification
  • Unified code style
  • Commit specification verification
  • Interface mock
  • Hot update
  • Asynchronous component

Introduction to mock function

There are many articles on how to do front-end mock in the market. On the whole, none of them really stand in the front-end perspective, which makes me feel powerful and easy to use. Let’s talk about what functions I expect the front-end mock to have:

  1. The function of mock is decoupled from the front-end code
  2. One interface supports multiple mock situations
  3. No need to rely on other back-end services and third-party libraries
  4. You can see the requests of mock interface in the network and distinguish them
  5. Mock data, interface configuration and page are in the same directory
  6. There is no need to restart the front-end dev when the mock configuration changes
  7. Production packaging can inject mock data into the packaged JS and take the front-end mock
  8. For the existing back-end interface, it can also quickly convert the response data into mock data

I will talk about the functions of the above functions

The role of point 7 is to complete the development of subsequent projects. It can also be demonstrated without developing back-end services at all. This is very useful for some tob customized projects to precipitate project maps (cases).
For point 8, when the back-end services in the development environment are often unstable, you can also do page development without relying on the back-end. The core is to generate mock data with one click.

Configuration decoupling


What is front-end configuration decoupling? First, let’s take a look at the usual configuration coupling conditions

  • The back-end test environment of webpack dev has changed, and the code to be tracked by git needs to be changed
  • Dev and build need to change git tracking code
  • When developing this interface, mock needs to change the code mockurl, mock?

How to solve it

The idea of front-end dependent configuration decoupling is configuration file conf.json It is generated dynamically in dev or build, and then the file is referenced in the front-end project

├── config
│   ├──  conf.json                                     #Git does not track
│   ├──  config.js                                     #Git does not track
│   ├── config_default.js
│   ├── index.js
│   └── webpack.config.js
├── jsconfig.json
├──  mock.json                                             #Git does not track

Webpack configuration file introduces JS configuration and generates conf.json

// config/index.js
const _ = require("lodash");
let config = _.cloneDeep(require("./config_default"))
try {
  const envConfig = require('./config') // eslint-disable-line
  config = _.merge(config, envConfig);
} catch (e) {
module.exports = config;

Config is used by default_ default.js If any config.js Then override and copy config during development_ default.js by config.js Subsequent configuration can be modified config.js That’s it.

// config/config_default.js
const pkg = require("../package.json");
module.exports = {
  version: pkg.version,
  port: 8888,
  proxy: {
    "/render-server/api/*": {
      target: ``,
      Changeorigin: true, // supports cross domain requests
      Secure: true, // supports HTTPS
  conf: {
    dev: {
      Title: "front end template",
      Pathprefix: "/ react starter", // unify front end path prefix
      apiPrefix: "/api/react-starter", //
      debug: true,
      Delay: 500, // mock data simulation delay
      mock: {
        // "global.login": "success",
        // "global.loginInfo": "success",
    build: {
      Title: "front end template",
      pathPrefix: "/react-starter",
      apiPrefix: "/api/react-starter",
      debug: false,
      mock: {}

It is used according to environment variables during development or packaging or generate conf.json Document content

// package.json
  "name": "react-starter",
  "version": "1.0.0",
  "Description": "react front end development scaffolding,",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --config './config/webpack.config.js' --open --mode development",
    "build": "cross-env BUILD_ENV=VERSION webpack --config './config/webpack.config.js' --mode production --progress --display-modules && npm run tar",
    "build-mock": "node ./scripts/build-mock.js "

The specified webpack path is./config/webpack.config.js

And then in thewebpack.config.jsIntroduce configuration and generate conf.json file

// config/webpack.config.js
const config = require('.')
const env = process.env.BUILD_ENV ? 'build' : 'dev'
const confJson = env === 'build' ? :
fs.writeFileSync(path.join(__dirname, './conf.json'),  JSON.stringify(confGlobal, null, '\t'))

Reference configuration

staysrc/common/utils.jsxThe configuration item is exposed in the file, and the configuration can also be accessed through the window.conf To cover

// src/common/utils.jsx
import conf from '@/config/conf.json'
export const config = Object.assign(conf, window.conf)

Then it can be used in various pages

import {config} from '@src/common/utils'
class App extends Component {
  render() {
    return (
      <Router history={history}>
          <Route path={`${config.pathPrefix}`} component={Home} />
          <Redirect from="/" to={`${config.pathPrefix}`} />
     <App />,

Mock implementation


In order to realize the related functions of the mock we want, first of all, whether to turn on the configuration decoupling of the mock can be realized by the above way. We usually define a directory when the page requests asynchronously io.js It defines the relevant back-end interfaces that the current page needs to call

// src/pages/login/login-io.js
import {createIo} from '@src/io'

const apis = {
  login: {
    method: 'POST',
    url: '/auth/login',
  //Log out
  logout: {
    method: 'POST',
    url: '/auth/logout',
Export default createio (APIs, 'Login') // corresponding to login- mock.json

The login and logout interfaces are defined above. We hope that the corresponding open mock requests can use thelogin-mock.jsonContents of the document

// src/pages/login/login-mock.json
    "login": {
        "failed": {
            "success": false,
            "code": "ERROR_PASS_ERROR",
            "content": null,
            "Message": "wrong account or password!"
        "success": {
            "success": true,
            "code": 0,
            "content": {
                "name": "admin",
                "Nickname": Super administrator,
                "permission": 15
            "message": ""
    "logout": {
        "success": {
            "success": true,
            "code": 0,
            "content": null,
            "message": ""

When calling logout to log out the Ajax request, and our conf.json Is configured in"login.logout": "success"Just go backlogin-mock.jsonIn login.success If the configuration does not match, the request will be forwarded to the back-end service.

// config/conf.json
    "Title": "front end and background template,",
    "pathPrefix": "/react-starter",
    "apiPrefix": "/api/react-starter",
    "debug": true,
    "delay": 500,
    "mock": {
        "login.logout": "success"

This is the final effect we want to achieve. Here is an agreement:All items in the project directory-mock.jsomThe file at the end of the file is a mock file, and the file name cannot be repeated


In the webpack configuration item, the proxy of devserver configures the forwarding setting of the interface, and the interface forwarding uses powerfulhttp-proxy-middlewareThe configuration format of proxy is as follows:

  proxy: {
    "/api/react-starter/*": {
      target: ``,
      changeOrigin: true,
      secure: true,
      // onError: (),
      // onProxyRes,
      // onProxyReq  

It has several event triggered configurations:

  • option.onErrorAn error occurred
  • option.onProxyResAfter the back end responds
  • option.onProxyReqBefore forwarding request
  • option.onProxyReqWs
  • option.onOpen
  • option.onClose

So we need to customize the processing of these things, mainly before and after the request forwarding


If we want to implement mock processing here, if we match the mock data, we will respond directly without forwarding the request to the back end. How to do it: the idea is to rely on the request header. In the case of dev, can the front end inject the agreed request header to tell me which mock data item to look for

  • mock-keyTo match the mock file, such aslogin-mock.jsonFor examplelogin
  • mock-methodTo match the method items of the corresponding file contents, such aslogout

then conf.json Specific response items are found in the mock configuration, such as:"login.logout": "success/failed"Content of


If the real back-end request is called, the response data of the request will be cached to theapi-cacheFile format under directorymock-key.mock-method.json

╎ -- API cache # git does not track
│   ├── 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": "",
            "mock-key": "login",
            "mock-method": "logout"
        "query": {},
        "reqBody": {},
        "resBody": {
            "success": true,
            "code": 0,
            "content": null,
            "message": ""

The purpose of this is to generate a mock file with one click.

Front end interface encapsulation


The IO configuration of the interface is defined as follows:

// src/pages/login/login-io.js
import {createIo} from '@src/io'

const apis = {
  login: {
    method: 'POST',
    url: '/auth/login',
  //Log out
  logout: {
    method: 'POST',
    url: '/auth/logout',
Export default createio (APIs, 'Login') // register login to the mock key of the header

We use it in the 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 {
  //User information
  @observable userInfo
  //Login operation
  async login(params) {
    const {success, content} = await io.login(params)
    if (!success) return
    runInAction(() => {
      this.userInfo = content
export default LoginStore

adoptcreateIo(apis, 'login')Encapsulation of can be very simple to pass request parameters when calling. In simple mode, it will determine whether the parameters are to body or query. Complex can also be supported, such as header, query, body and so on, which will not be demonstrated here.

Createio request encapsulation

This is the key point of front-end interface encapsulation, and also the place of mock request header injection

// src/io/index.jsx
import {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) {

let reloginFlag = false
//Create a request
export const request = creatRequest({
  //Custom request header
  headers: {'Content-Type': 'application/json'},
  //Configure default return data processing
  action: (data) => {
    //Unified processing of unregistered balloons
    if (data.success === false && data.code === ERROR_CODE.UN_LOGIN && !reloginFlag) {
      reloginFlag = true
      //Todo here may be unified jump to, can also be pop-up click jump
        Title: 'login again',
        content: '',
        onOk: () => {
          // location.reload()
          reloginFlag = false
  //Error display message
  showError: true,
  //Whether to default to false by throwing an exception {success: Boolean judgment}
  throwError: false,
  //Wait time of mock data request
  delay: config.delay,
  //Log printing

//Indicates whether the parameter is a simple parameter, and a value of true indicates a complex encapsulation
export const rejectToData = Symbol('flag')

 *Create encapsulation of request IO
 * @param ioContent {any { url: string method?: string mock?: any apiPrefix?: string}}
 *The corresponding file of @ param name mock data is deleted- mock.json After
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 = {}) => {
      //Here, we judge that simple request encapsulation rejecttodata = true indicates complex encapsulation
      if (!options[rejectToData]) {
        options = {
          data: options,
      delete options[rejectToData]
      if (
        config.debug === false &&
        name &&
        config.mock &&
        config.mock[`${name}.${key}`] &&
        mockData[name] &&
      ){// judge whether the production package mock is injected into the code
        ioContent[key].mock = JSON.parse(JSON.stringify(mockData[name][key][config.mock[`${name}.${key}`]]))
      } else if (name &&  config.debug  ===True) {// inject mock request header
        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

Here, the request is further encapsulated. The configuration item sets some default processing settings. For example, if the general request response fails, there will be a message, and if it is not logged in, there will be a pop-up window to prompt you to click to jump to the landing page. If you want to define multiple common processes, you can create another request 2 and create io2.

Request encapsulates Axios

It’s a secondary encapsulation based on Axios. It’s not very general. It’s mainly customized when the request fails or succeeds. You can modify it if you need to.

import axios from 'axios'

//Configure interface parameters
// declare interface Options {
//   url: string
//   baseURL?: string
//// get by default
//   method?: Method
//// identify whether to inject the data parameter
//   rejectToData?: boolean
//// pop up message directly or not. The default is yes
//   showError?: boolean
//// specify the default login process for callback operation
//   action?(data: any): any
//   headers?: {
//     [index: string]: string
//   }
//   timeout?: number
//// specify routing parameters
//   params?: {
//     [index: string]: string
//   }
//// specify the URL parameter
//   query?: any
//// specify the body parameter
//   body?: any
//// mixed processing of get to URL, delete post to body, and replacement of routing parameters encapsulated in createio
//   data?: any
//   mock?: any
// }
//Uniform encapsulation of Ajax requests
//Todo 1. Encapsulating jsonp requests 2. Repeating requests

 *Return the uniform encapsulation of Ajax request
 *@ param object option request configuration
 * @param {boolean}  opts.showError  Whether to call the error method of message incorrectly
 * @param {object}  opts.message   Called when the. Error method showerror true is included
 * @param {boolean}  opts.throwError  Whether there is an error and an exception is thrown
 * @param {function}  opts.action   Contains custom default processing, such as not logged in processing
 * @param {object}  opts.headers   Default content type of request header: application / JSON
 * @param {number}  opts.timeout   The timeout is 60 seconds by default
 * @param {number}  opts.delay    Mock request delay
 *@ returns {function} {params, URL, headers, query, data, mock} data mix processing to get to URL, delete post to body, also replace routing parameters in createio encapsulation
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,
      headers: {'X-Requested-With': 'XMLHttpRequest', ...option.headers, ...optionData.headers},
    //Simple request processing
    if ( {
      if (typeof === 'object') {
        Object.keys( => {
          if (key[0] === ':' && {
            options.url = options.url.replace(key, encodeURIComponent([key]))
      if ((options.method || '').toLowerCase() === 'get' || (options.method || '').toLowerCase() === 'head') {
        options.query = Object.assign(, options.query)
      } else {
        options.body = Object.assign(, options.body)
    //Routing parameter processing
    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 parameter processing
    if (options.query) {
      const paramsArray = []
      Object.keys(options.query).forEach((key) => {
        if (options.query[key] !== undefined) {
      if (paramsArray.length > 0 &&\?/) === -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)
    if (options.headers['Content-Type'] === 'application/json' && options.body && typeof options.body !== 'string') {
      options.body = JSON.stringify(options.body)
    let retData = {success: false}
    //Mock processing
    if (options.mock) {
      retData = await new Promise((resolve) =>
        setTimeout(() => {
        }, 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 =
          retData.message  =Abnormal return of browser request: status code${ retData.status }`

    //Automatic processing of error messages
    if (options.showError && retData.success === false && retData.message && option.message) {
    //Handling action
    if (options.action) {
    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
One click to generate mock

According to the API cache interface cache and definitionxxx-mock.jsonFile.

# "build-mock": "node ./scripts/build-mock.js"
npm run build-mock mockAll 
#Single mock file:
npm run build-mock login
#Single mock interface:
npm run build-mock login.logout
npm run build-mock login.logout user

Specific code referencebuild-mock.js

mock.json File generation

In order to inject the mock data into the front-end code when the build is packaged, the mock.json The content of the file is as small as possible, and it will be changed according to the conf.json To dynamically generate mock.json If the mock item is not opened in the build, the content will be empty JSON data. Of course, the back-end interface agent also maps a copy in the memory mock.json The content of. Here are a few things to do:

  • Dynamically generated according to configuration mock.json Content of
  • Listen to the config folder to determine whether the configuration item of mock has changed and regenerate mock.json
// scripts/webpack- init.js  Initialize in the wenpack configuration file
const path = require('path')
const fs = require('fs')
const {syncWalkDir} = require('./util')
let confGlobal = {}
let mockJsonData = {}
exports.getConf = () => confGlobal
exports.getMockJson =() => mockJsonData

 *Initialization project configuration dynamic generation mock.json And 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' ? :
  confGlobal = confJson
  //1. According to the environment variable
  fs.writeFileSync(path.join(__dirname, '../config/conf.json'),  JSON.stringify(confGlobal, null, '\t'))
 //Generate mock file data
 const buildMock = (conf) => {
  //2. Dynamically generate mock data and read all the data in SRC folder- mock.json The end of the file is stored in io/ index.json In the document
  let mockJson = {}
  const mockFiles = syncWalkDir(path.join(__dirname, '../src'), (file) => /-mock.json$/.test(file))
  console.log('build mocks: ->>>>>>>>>>>>>>>>>>>>>>>')
  mockFiles.forEach((filePath) => {
    const p = path.parse(filePath)
    const mockKey =, - 5)
    console.log(mockKey, filePath)
    if (mockJson[mockKey]) {
      console.error (` same mock file name ${} exists', filepath)
    delete require.cache[require.resolve(filePath)]
    mockJson[mockKey] = require(filePath)
  //If it is a packaged environment, minimize the mock resource data
  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'))
 //Monitor the data in the configuration file directory config.js And config_ default.js
const confPath = path.join(__dirname, '../config')

if ((env = process.env.BUILD_ENV ? 'build' : 'dev') === 'dev') {, 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' ? :
      if (JSON.stringify(confJson) !== JSON.stringify(confGlobal)) {

Interface proxy processing

Onproxyreq and onproxyres

Realize the onproxyreq and onproxyres response processing in the above idea


// 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 = {
  //Agent preprocessing
  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 request: ${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
  //Response cache interface
  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'),
        path: reqPath,
        resHeader: res.headers,
        reqHeader: req.headers,
        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)})
  //Exception handling of back end service not started
  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 configuration

Introducing and using in webpack configuration

const config = require('.')
// config/webpack.config.js
const {init} = require('../scripts/webpack-init');
//Interface request local cache
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, '..', // the directory of the page loaded by the local server
    inline: true,
    port: config.port,
    publicPath: '/',
    historyApiFallback: {
      disableDotRule: true,
      //Indicates which paths are mapped to which HTML
      rewrites: config.rewrites,
    host: '',
    hot: true,
    proxy: config.proxy,


In fact, it is necessary for us to do a good job with mock in front-end practice. If the back-end of the project is eradicated, we can use mock to make the project run and find some implementation effects for code reuse. Currently, there are a lot of customized development for the implementation of mock process, but after it is really completed, the members of the team just use it or simply configure it.

About the front-end project deployment, I also shared a BFF layer, which is not perfect at present. I also shared it for your reference

Render-ServerThe main functions include:

  • One click deployment of NPM run deploy
  • Support cluster deployment configuration
  • It’s a file service
  • Is a static resource service
  • Online visual deployment front end project
  • Configure hot update
  • Online postman and interface document
  • Support front-end routing rendering, support template
  • Interface proxy and Path replacement
  • Web security supports Ajax request verification and referer verification
  • It supports plug-in development and online configuration, including front-end template parameter injection, request header injection, IP white list, interface mock, session, third-party login, etc

Recommended Today

Deeply analyze the principle and practice of RSA key

1、 Preface After experiencing many dark moments in life, when you read this article, you will regret and even be angry: why didn’t you write this article earlier?! Your darkest moments include: 1. Your project needs to be connected with the bank, and the other party needs you to provide an encryption certificate. You have […]