pengzhanbo / vite-plugin-mock-dev-server

🚀mock-dev-server is injected into the vite development environment to simulate requests and data responses.在vite 开发环境中注入 mock-dev-server, 模拟请求和数据响应
https://vite-plugin-mock-dev-server.netlify.app
MIT License
169 stars 14 forks source link
fakerjs graphql mock mock-server mockjs proxy typescript vite vite-plugin vitejs websocket

vite-plugin-mock-dev-server



Vite Plugin for API mock dev server.


npm node-current npm peer dependency version npm
GitHub Workflow Status fossa status


English | 简体中文



Features

Documentation

See the documentation for more details.

Netlify Status

Install

# npm
npm i -D vite-plugin-mock-dev-server
# yarn
yarn add vite-plugin-mock-dev-server
# pnpm
pnpm add -D vite-plugin-mock-dev-server

Usage

vite.config.ts

import { defineConfig } from 'vite'
import mockDevServerPlugin from 'vite-plugin-mock-dev-server'

export default defineConfig({
  plugins: [
    mockDevServerPlugin(/* plugin options */),
  ],
  // The fields defined here can also be used in mock.
  define: {},
  server: {
    // plugin will read `server.proxy`
    proxy: {
      '^/api': { target: 'http://example.com' }
    }
  }
})

The plugin will read the configuration of server.proxy or options.prefix, and enable mock matching for matched URLs.

The plugin will also read the define configuration, which supports direct use in mock files.

Edit Mock File

By default, write mock data in the mock directory of your project's root directory:

mock/**/*.mock.ts :

import { defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/api/test',
  body: { a: 1, b: 2 }
})

Methods

mockDevServerPlugin(pluginOptions)

vite plugin

vite.config.ts

import { defineConfig } from 'vite'
import mockDevServerPlugin from 'vite-plugin-mock-dev-server'

export default defineConfig({
  plugins: [
    mockDevServerPlugin({ /* plugin options */ }),
  ]
})

defineMock(mockOptions)

Mock Options Type Helper

import { defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/api/test',
  body: {}
})

createDefineMock(transformer)

Return a custom defineMock function to support preprocessing of mock config.

import path from 'node:path'
import { createDefineMock } from 'vite-plugin-mock-dev-server'

// Preprocessed mock url
const defineAPIMock = createDefineMock((mock) => {
  mock.url = path.join('/api', mock.url)
})

export default defineApiMock({
  url: '/test' // Complete as '/api/test'
})

createSSEStream(req, res)

Create a Server-sent events write stream to support mocking EventSource.

import { createSSEStream, defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/api/sse',
  response: (req, res) => {
    const sse = createSSEStream(req, res)
    sse.write({ event: 'message', data: { message: 'hello world' } })
    sse.end()
  }
})

Plugin Options

prefix

wsPrefix

cwd

include

exclude

reload

cors

log

formidableOptions

cookiesOptions

bodyParserOptions

build

priority

Mock Options

http mock

import { defineMock } from 'vite-plugin-mock-dev-server'
export default defineMock({
  url: '/api/test',
  body: { message: 'hello world' }
})

websocket mock

import { defineMock } from 'vite-plugin-mock-dev-server'
export default defineMock({
  url: '/socket.io',
  ws: true,
  setup(wss) {
    wss.on('connection', (ws, req) => {
      console.log('connected')
    })
  }
})

options.url

options.enabled

options.method

options.type

options.headers

options.status

options.statusText

options.delay

options.body

options.response

options.cookies

options.validator

options.ws

options.setup

interface WebSocketSetupContext {
  /**
   * When defining WSS, you may perform some automatic or looping tasks.
   * However, when hot updating, the plugin will re-execute `setup()`,
   * which may result in duplicate registration of listening events and looping tasks
   * such as setTimeout. You can use `onCleanup()` to clear these automatic or looping tasks.
   */
  onCleanup: (cleanup: () => void) => void
}

Request/Response Enhance

When defining methods using headers, body, and response, the plugin adds new content to the request and response parameters.

In Request:

The original type of request is http.IncomingMessage. The plugin adds data such as query, params, body, refererQuery, and the getCookie(name) method for obtaining cookie information on this basis.

type Request = http.IncomingMessage & {
  query: object
  params: object
  body: any
  refererQuery: object
  getCookie: (name: string, option?: Cookies.GetOption) => string | undefined
}

In Response:

The original type of response is http.ServerResponse<http.IncomingMessage>. The plugin adds setCookie(name, value) method for configuration cookies on this basis.

type Response = http.ServerResponse<http.IncomingMessage> & {
  setCookie: (
    name: string,
    value?: string | null,
    option?: Cookies.SetOption,
  ) => void
}

Share Mock Data

Due to each mock file being compiled as a separate entry point, the local files they depend on are also compiled within. Additionally, each mock file has an independent scope. This means that even if multiple mock files collectively depend on a data.ts file, they cannot share data. If one mock file modifies the data in data.ts, other mock files will not receive the updated data.

To address this, the plugin offers a defineMockData function, which allows using data.ts as a shared data source within mock files.

type defineMockData<T> = (
  key: string, // key
  initialData: T, // initial data
) => [getter, setter] & { value: T }

Examples

data.ts

import { defineMockData } from 'vite-plugin-mock-dev-server'

export default defineMockData('posts', [
  { id: '1', title: 'title1', content: 'content1' },
  { id: '2', title: 'title2', content: 'content2' },
])

*.mock.ts

import { defineMock } from 'vite-plugin-mock-dev-server'
import posts from './data'

export default defineMock([
  {
    url: '/api/posts',
    body: () => posts.value
  },
  {
    url: '/api/posts/delete/:id',
    body: (params) => {
      const id = params.id
      posts.value = posts.value.filter(post => post.id !== id)
      return { success: true }
    }
  }
])

Tips:

The defineMockData function relies solely on the shared data support provided by memory. If persistent mock data is required, it is recommended to use a nosql database like lowdb or level.

Custom-Path-Matching-Priority

Custom rules only affect links with dynamic parameters, such as: /api/user/:id

The priority of the path matching rules built into the plugin can already meet most needs, but if you need more flexible customization of the matching rule priority, you can use the priority parameter.

Exp:

import { defineConfig } from 'vite'
import mockPlugin from 'vite-plugin-mock-dev-server'

export default defineConfig({
  plugins: [
    mockPlugin({
      priority: {
        // The priority of matching rules is global.
        // The rules declared in this option will take priority over the default rules.
        // The higher the position of the rule in the array, the higher the priority.
        global: ['/api/:a/b/c', '/api/a/:b/c', '/api/a/b/:c'],
        // For some special cases where the priority of certain rules needs to be adjusted,
        // this option can be used. For example, when a request matches both Rule A and Rule B,
        // and Rule A has a higher priority than Rule B, but it is desired for Rule B to take effect.
        special: {
          // When both A and B or C match, and B or C is at the top of the sort order,
          // insert A into the top position.
          // The `when` option is used to further constrain the priority adjustment to
          // be effective only for certain requests.
          '/api/:a/:b/c': {
            rules: ['/api/a/:b/:c', '/api/a/b/:c'],
            when: ['/api/a/b/c']
          },
          // If no `when` is specified, it means that all requests matching the rules need to have their priorities adjusted. It can be abbreviated as `[key]: [...rules]`
          '/api/:a/b': ['/api/a/:b'],
        }
      }
    })
  ]
})

Tip:

priority although it can adjust the priority, most of the time you do not need to do so. For some special requests, you can use static rules instead of priority, as static rules always have the highest priority.

Examples

mock/**/*.mock.{ts,js,mjs,cjs,json,json5}

See more examples: example

exp: Match /api/test, And returns a response body content with empty data

export default defineMock({
  url: '/api/test',
})

exp: Match /api/test , And returns a static content data

export default defineMock({
  url: '/api/test',
  body: { a: 1 },
})

exp: Only Support GET Method

export default defineMock({
  url: '/api/test',
  method: 'GET'
})

exp: In the response header, add a custom header and cookie

export default defineMock({
  url: '/api/test',
  headers: { 'X-Custom': '12345678' },
  cookies: { 'my-cookie': '123456789' },
})
export default defineMock({
  url: '/api/test',
  headers({ query, body, params, headers }) {
    return { 'X-Custom': query.custom }
  },
  cookies() {
    return { 'my-cookie': '123456789' }
  }
})

exp: Define multiple mock requests for the same URL and match valid rules with validators

export default defineMock([
  // Match /api/test?a=1
  {
    url: '/api/test',
    validator: {
      query: { a: 1 },
    },
    body: { message: 'query.a == 1' },
  },
  // Match /api/test?a=2
  {
    url: '/api/test',
    validator: {
      query: { a: 2 },
    },
    body: { message: 'query.a == 2' },
  },
  {
    // `?a=3` will resolve to `validator.query`
    url: '/api/test?a=3',
    body: { message: 'query.a == 3' }
  },
  // Hitting the POST /api/test request, and in the request body,
  // field a is an array that contains items with values of 1 and 2.
  {
    url: '/api/test',
    method: ['POST'],
    validator: { body: { a: [1, 2] } }
  }
])

exp: Response Delay

export default defineMock({
  url: '/api/test',
  delay: 6000, // delay 6 seconds
})

exp: The interface request failed

export default defineMock({
  url: '/api/test',
  status: 502,
  statusText: 'Bad Gateway'
})

exp: Dynamic route matching

export default defineMock({
  url: '/api/user/:userId',
  body({ params }) {
    return { userId: params.userId }
  }
})

The userId in the route will be resolved into the request.params object.

exp: Use the buffer to respond data

import { Buffer } from 'node:buffer'

// Since the default value of type is json,
// although buffer is used for body during transmission,
// the content-type is still json.
export default defineMock({
  url: 'api/buffer',
  body: Buffer.from(JSON.stringify({ a: 1 }))
})
// When the type is buffer, the content-type is application/octet-stream.
// The data passed in through body will be converted to a buffer.
export default defineMock({
  url: 'api/buffer',
  type: 'buffer',
  // Convert using Buffer.from(body) for internal use
  body: { a: 1 }
})

exp: Response file type

Simulate file download, and pass in the file reading stream.

import { createReadStream } from 'node:fs'

export default defineMock({
  url: '/api/download',
  // When you are unsure of the type, you can pass in the file name for internal parsing by the plugin.
  type: 'my-app.dmg',
  body: () => createReadStream('./my-app.dmg')
})
<a href="https://github.com/pengzhanbo/vite-plugin-mock-dev-server/blob/main/api/download" download="my-app.dmg">Download File</a>

exp: Use mockjs:

import Mock from 'mockjs'

export default defineMock({
  url: '/api/test',
  body: Mock.mock({
    'list|1-10': [{
      'id|+1': 1
    }]
  })
})

You need install mockjs

exp: Use response to customize the response

export default defineMock({
  url: '/api/test',
  response(req, res, next) {
    const { query, body, params, headers } = req
    console.log(query, body, params, headers)

    res.status = 200
    res.setHeader('Content-Type', 'application/json')
    res.end(JSON.stringify({
      query,
      body,
      params,
    }))
  }
})

exp: Use json / json5

{
  "url": "/api/test",
  "body": {
    "a": 1
  }
}

exp: multipart, upload files.

use formidable to support.

<form action="/api/upload" method="post" enctype="multipart/form-data">
  <p>
    <span>file: </span>
    <input type="file" name="files" multiple="multiple">
  </p>
  <p>
    <span>name:</span>
    <input type="text" name="name" value="mark">
  </p>
  <p>
    <input type="submit" value="submit">
  </p>
</form>

fields files mapping to formidable.File

export default defineMock({
  url: '/api/upload',
  method: 'POST',
  body(req) {
    const body = req.body
    return {
      name: body.name,
      files: body.files.map((file: any) => file.originalFilename),
    }
  },
})

exp: Graphql

import { buildSchema, graphql } from 'graphql'

const schema = buildSchema(`
type Query {
  hello: String
}
`)
const rootValue = { hello: () => 'Hello world!' }

export default defineMock({
  url: '/api/graphql',
  method: 'POST',
  body: async (request) => {
    const source = request.body.source
    const { data } = await graphql({ schema, rootValue, source })
    return data
  },
})
fetch('/api/graphql', {
  method: 'POST',
  body: JSON.stringify({ source: '{ hello }' })
})

exp: WebSocket Mock

// ws.mock.ts
export default defineMock({
  url: '/socket.io',
  ws: true,
  setup(wss, { onCleanup }) {
    const wsMap = new Map()
    wss.on('connection', (ws, req) => {
      const token = req.getCookie('token')
      wsMap.set(token, ws)
      ws.on('message', (raw) => {
        const data = JSON.parse(String(raw))
        if (data.type === 'ping')
          return
        // Broadcast
        for (const [_token, _ws] of wsMap.entires()) {
          if (_token !== token)
            _ws.send(raw)
        }
      })
    })
    wss.on('error', (err) => {
      console.error(err)
    })
    onCleanup(() => wsMap.clear())
  }
})
// app.ts
const ws = new WebSocket('ws://localhost:5173/socket.io')
ws.addEventListener('open', () => {
  setInterval(() => {
    // heartbeat
    ws.send(JSON.stringify({ type: 'ping' }))
  }, 1000)
}, { once: true })
ws.addEventListener('message', (raw) => {
  console.log(raw)
})

示例: EventSource Mock

// sse.mock.ts
import { createSSEStream, defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/api/sse',
  response(req, res) {
    const sse = createSSEStream(req, res)
    let count = 0
    const timer = setInterval(() => {
      sse.write({
        event: 'count',
        data: { count: ++count },
      })
      if (count >= 10) {
        sse.end()
        clearInterval(timer)
      }
    }, 1000)
  },
})
// app.js
const es = new EventSource('/api/sse')

es.addEventListener('count', (e) => {
  console.log(e.data)
})

Mock Services

In some scenarios, it may be necessary to use the data provided by mock services for display purposes, but the project may have already been packaged, built and deployed without support from Vite and this plugin's mock service. Since this plugin supports importing various node modules in mock files at the design stage, the mock file cannot be inline into client build code.

To meet such scenarios, on one hand, the plugin provides support under vite preview, and on the other hand, it also builds a small independent mock service application that can be deployed to relevant environments during vite build. This can then be forwarded through other HTTP servers like Nginx to actual ports for mock support.

The default output is built into the directory dist/mockServer, generating files as follows:

./mockServer
├── index.js
├── mock-data.js
└── package.json

In this directory, execute npm install to install dependencies, and then execute npm start to start the mock server.

The default port is 8080.

You can access related mock interfaces through localhost:8080/.

Links

Contributors

All Contributors

pengzhanbo
pengzhanbo

📖 🤔 💡 💻
jiadesen
jiadesen

🤔 🐛
yogibaba
yogibaba

💻
pfdgithub
pfdgithub

💻 🐛
chuyuan du
chuyuan du

💻
hlwen
hlwen

🐛 💻

LICENSE

The plugin is licensed under the MIT License

FOSSA Status