Mock Service Worker 示例
全面的MSW (Mock Service Worker)示例,包含API模拟、GraphQL、WebSocket模拟和高级请求处理模式
💻 REST API 模拟基础 javascript
🟢 simple
⭐⭐
基本的REST API模拟,包含GET、POST、PUT、DELETE操作和响应处理
⏱️ 25 min
🏷️ msw, api mocking, rest, testing
Prerequisites:
JavaScript, REST API, HTTP methods, Testing basics
// Mock Service Worker - REST API Basics
// File: src/mocks/handlers.js
import { rest } from 'msw'
// 1. Basic GET request handler
export const handlers = [
// Get user by ID
rest.get('/api/users/:id', (req, res, ctx) => {
const { id } = req.params
// Mock user data
const users = {
'1': { id: 1, name: 'John Doe', email: '[email protected]', role: 'user' },
'2': { id: 2, name: 'Jane Smith', email: '[email protected]', role: 'admin' },
'3': { id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'user' }
}
const user = users[id]
if (user) {
return res(
ctx.status(200),
ctx.json(user)
)
}
return res(
ctx.status(404),
ctx.json({ error: 'User not found' })
)
}),
// Get all users with query parameters
rest.get('/api/users', (req, res, ctx) => {
const page = parseInt(req.url.searchParams.get('page') || '1')
const limit = parseInt(req.url.searchParams.get('limit') || '10')
const role = req.url.searchParams.get('role')
// Mock pagination
const allUsers = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'user' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'admin' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'user' },
{ id: 4, name: 'Alice Brown', email: '[email protected]', role: 'user' },
{ id: 5, name: 'Charlie Wilson', email: '[email protected]', role: 'moderator' }
]
let filteredUsers = allUsers
if (role) {
filteredUsers = allUsers.filter(user => user.role === role)
}
const startIndex = (page - 1) * limit
const endIndex = startIndex + limit
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
return res(
ctx.status(200),
ctx.json({
users: paginatedUsers,
pagination: {
page,
limit,
total: filteredUsers.length,
totalPages: Math.ceil(filteredUsers.length / limit)
}
})
)
}),
// POST - Create new user
rest.post('/api/users', async (req, res, ctx) => {
const userData = await req.json()
// Validation
if (!userData.name || !userData.email) {
return res(
ctx.status(400),
ctx.json({ error: 'Name and email are required' })
)
}
// Check if email already exists
const existingEmails = ['[email protected]', '[email protected]', '[email protected]']
if (existingEmails.includes(userData.email)) {
return res(
ctx.status(409),
ctx.json({ error: 'Email already exists' })
)
}
// Mock user creation
const newUser = {
id: Date.now(),
...userData,
role: userData.role || 'user',
createdAt: new Date().toISOString()
}
return res(
ctx.status(201),
ctx.json(newUser)
)
}),
// PUT - Update user
rest.put('/api/users/:id', async (req, res, ctx) => {
const { id } = req.params
const updateData = await req.json()
// Mock existing user
const existingUser = {
id: parseInt(id),
name: 'John Doe',
email: '[email protected]',
role: 'user',
updatedAt: new Date().toISOString()
}
// Validate update data
if (updateData.email && !updateData.email.includes('@')) {
return res(
ctx.status(400),
ctx.json({ error: 'Invalid email format' })
)
}
const updatedUser = {
...existingUser,
...updateData,
updatedAt: new Date().toISOString()
}
return res(
ctx.status(200),
ctx.json(updatedUser)
)
}),
// DELETE - Remove user
rest.delete('/api/users/:id', (req, res, ctx) => {
const { id } = req.params
// Simulate user deletion
if (id === '1') {
return res(
ctx.status(204)
)
}
return res(
ctx.status(404),
ctx.json({ error: 'User not found' })
)
}),
// File upload handler
rest.post('/api/upload', async (req, res, ctx) => {
const contentType = req.headers.get('content-type')
if (!contentType || !contentType.includes('multipart/form-data')) {
return res(
ctx.status(400),
ctx.json({ error: 'File upload required' })
)
}
// Mock file processing
return res(
ctx.status(200),
ctx.json({
message: 'File uploaded successfully',
filename: 'example.jpg',
size: 1024000,
url: 'https://example.com/files/example.jpg',
uploadedAt: new Date().toISOString()
})
)
}),
// Error simulation handler
rest.get('/api/error-test', (req, res, ctx) => {
const errorType = req.url.searchParams.get('type') || 'server'
switch (errorType) {
case 'server':
return res(
ctx.status(500),
ctx.json({ error: 'Internal server error', code: 'INTERNAL_ERROR' })
)
case 'network':
return res.networkError('Network connection failed')
case 'timeout':
return res(ctx.delay(10000), ctx.status(200), ctx.json({}))
case 'unauthorized':
return res(
ctx.status(401),
ctx.json({ error: 'Unauthorized', message: 'Please login first' })
)
default:
return res(
ctx.status(404),
ctx.json({ error: 'Endpoint not found' })
)
}
})
]
// File: src/mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
// Create server for Node.js environment (testing)
export const server = setupServer(...handlers)
// File: src/mocks/browser.js
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
// Create worker for browser environment (development)
export const worker = setupWorker(...handlers)
// File: src/setupTests.js (for Jest)
import '@testing-library/jest-dom'
import { server } from './mocks/server'
// Establish API mocking before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Reset any request handlers that we may add during the tests
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished
afterAll(() => server.close())
// File: src/index.js (development setup)
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { worker } from './mocks/browser'
// Start Mock Service Worker in development
if (process.env.NODE_ENV === 'development') {
worker.start({
onUnhandledRequest: 'warn',
serviceWorker: {
url: '/mockServiceWorker.js'
}
}).then(() => {
console.log('Mock Service Worker started successfully')
}).catch(error => {
console.error('Failed to start Mock Service Worker:', error)
})
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
// Usage Example Component
// File: src/components/UserManager.js
import React, { useState, useEffect } from 'react'
export default function UserManager() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
fetchUsers()
}, [])
const fetchUsers = async () => {
try {
setLoading(true)
setError(null)
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setUsers(data.users || [])
} catch (error) {
setError(error.message)
} finally {
setLoading(false)
}
}
const createUser = async (userData) => {
try {
setLoading(true)
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to create user')
}
const newUser = await response.json()
setUsers(prev => [...prev, newUser])
return newUser
} catch (error) {
setError(error.message)
throw error
} finally {
setLoading(false)
}
}
const updateUser = async (id, userData) => {
try {
setLoading(true)
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to update user')
}
const updatedUser = await response.json()
setUsers(prev => prev.map(user =>
user.id === parseInt(id) ? updatedUser : user
))
return updatedUser
} catch (error) {
setError(error.message)
throw error
} finally {
setLoading(false)
}
}
const deleteUser = async (id) => {
try {
setLoading(true)
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error('Failed to delete user')
}
setUsers(prev => prev.filter(user => user.id !== parseInt(id)))
} catch (error) {
setError(error.message)
throw error
} finally {
setLoading(false)
}
}
if (loading && users.length === 0) {
return <div>Loading users...</div>
}
if (error) {
return <div>Error: {error}</div>
}
return (
<div>
<h1>User Manager</h1>
<button onClick={fetchUsers}>Refresh Users</button>
{users.map(user => (
<div key={user.id} style={{ marginBottom: '1rem', padding: '1rem', border: '1px solid #ccc' }}>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
<button onClick={() => updateUser(user.id, { name: user.name + ' (Updated)' })}>
Update
</button>
<button onClick={() => deleteUser(user.id)} style={{ marginLeft: '0.5rem' }}>
Delete
</button>
</div>
))}
</div>
)
}
💻 GraphQL API 模拟 javascript
🟡 intermediate
⭐⭐⭐⭐
完整的GraphQL模拟,包含查询、变更、订阅和复杂数据关系
⏱️ 35 min
🏷️ msw, graphql, mocking, subscriptions
Prerequisites:
GraphQL, React Query/Apollo Client, JavaScript ES6+, Subscription concepts
// Mock Service Worker - GraphQL API Mocking
// File: src/mocks/graphqlHandlers.js
import { graphql, GraphQLWsLink } from 'msw'
import { graphql } from 'msw/node'
import { setupServer } from 'msw/node'
// Mock database
const mockDatabase = {
users: [
{ id: '1', name: 'John Doe', email: '[email protected]', posts: ['1', '3'] },
{ id: '2', name: 'Jane Smith', email: '[email protected]', posts: ['2'] },
{ id: '3', name: 'Bob Johnson', email: '[email protected]', posts: [] }
],
posts: [
{ id: '1', title: 'First Post', content: 'This is my first post', authorId: '1', tags: ['javascript', 'react'] },
{ id: '2', title: 'GraphQL Basics', content: 'Learning GraphQL', authorId: '2', tags: ['graphql', 'tutorial'] },
{ id: '3', title: 'Testing with MSW', content: 'How to mock APIs with MSW', authorId: '1', tags: ['testing', 'msw'] }
],
comments: [
{ id: '1', postId: '1', authorId: '2', content: 'Great post!', createdAt: '2024-01-01T00:00:00Z' },
{ id: '2', postId: '1', authorId: '3', content: 'Thanks for sharing', createdAt: '2024-01-02T00:00:00Z' }
]
}
// GraphQL Handlers
export const graphqlHandlers = [
// Query: Get all users
graphql.query('GetUsers', (req, res, ctx) => {
const { limit, offset, filter } = req.variables
let users = mockDatabase.users
// Apply filters
if (filter?.name) {
users = users.filter(user =>
user.name.toLowerCase().includes(filter.name.toLowerCase())
)
}
// Apply pagination
const limitNum = limit || 10
const offsetNum = offset || 0
const paginatedUsers = users.slice(offsetNum, offsetNum + limitNum)
return res(
ctx.data({
users: paginatedUsers.map(user => ({
...user,
posts: mockDatabase.posts.filter(post => user.posts.includes(post.id))
})),
usersCount: users.length
})
)
}),
// Query: Get user by ID
graphql.query('GetUser', (req, res, ctx) => {
const { id } = req.variables
const user = mockDatabase.users.find(u => u.id === id)
if (!user) {
return res(
ctx.errors([
{
message: 'User not found',
locations: [{ line: 1, column: 1 }],
path: ['user']
}
])
)
}
const userPosts = mockDatabase.posts.filter(post => user.posts.includes(post.id))
return res(
ctx.data({
user: {
...user,
posts: userPosts.map(post => ({
...post,
comments: mockDatabase.comments.filter(comment => comment.postId === post.id)
}))
}
})
)
}),
// Query: Get posts with optional filters
graphql.query('GetPosts', (req, res, ctx) => {
const { limit, offset, authorId, tags } = req.variables
let posts = mockDatabase.posts
// Filter by author
if (authorId) {
posts = posts.filter(post => post.authorId === authorId)
}
// Filter by tags
if (tags && tags.length > 0) {
posts = posts.filter(post =>
tags.some(tag => post.tags.includes(tag))
)
}
// Pagination
const limitNum = limit || 10
const offsetNum = offset || 0
const paginatedPosts = posts.slice(offsetNum, offsetNum + limitNum)
return res(
ctx.data({
posts: paginatedPosts.map(post => ({
...post,
author: mockDatabase.users.find(user => user.id === post.authorId),
comments: mockDatabase.comments.filter(comment => comment.postId === post.id)
})),
postsCount: posts.length
})
)
}),
// Query: Search posts
graphql.query('SearchPosts', (req, res, ctx) => {
const { query, limit = 10 } = req.variables
if (!query || query.trim() === '') {
return res(
ctx.data({
searchResults: [],
searchCount: 0
})
)
}
const searchTerm = query.toLowerCase()
const matchingPosts = mockDatabase.posts.filter(post =>
post.title.toLowerCase().includes(searchTerm) ||
post.content.toLowerCase().includes(searchTerm) ||
post.tags.some(tag => tag.toLowerCase().includes(searchTerm))
)
const results = matchingPosts.slice(0, limit)
return res(
ctx.data({
searchResults: results.map(post => ({
...post,
author: mockDatabase.users.find(user => user.id === post.authorId),
matchScore: calculateMatchScore(post, searchTerm)
})),
searchCount: matchingPosts.length
})
)
}),
// Mutation: Create user
graphql.mutation('CreateUser', async (req, res, ctx) => {
const { input } = req.variables
const { name, email } = input
// Validation
if (!name || !email) {
return res(
ctx.errors([
{
message: 'Name and email are required',
locations: [{ line: 1, column: 1 }],
path: ['createUser']
}
])
)
}
// Check for duplicate email
const existingUser = mockDatabase.users.find(u => u.email === email)
if (existingUser) {
return res(
ctx.errors([
{
message: 'Email already exists',
extensions: { code: 'DUPLICATE_EMAIL' }
}
])
)
}
// Create new user
const newUser = {
id: (mockDatabase.users.length + 1).toString(),
name,
email,
posts: [],
createdAt: new Date().toISOString()
}
mockDatabase.users.push(newUser)
return res(
ctx.delay(300), // Simulate network delay
ctx.data({
createUser: newUser
})
)
}),
// Mutation: Create post
graphql.mutation('CreatePost', async (req, res, ctx) => {
const { input } = req.variables
const { title, content, authorId, tags = [] } = input
// Validate author exists
const author = mockDatabase.users.find(u => u.id === authorId)
if (!author) {
return res(
ctx.errors([
{
message: 'Author not found',
extensions: { code: 'AUTHOR_NOT_FOUND' }
}
])
)
}
// Create new post
const newPost = {
id: (mockDatabase.posts.length + 1).toString(),
title,
content,
authorId,
tags,
createdAt: new Date().toISOString()
}
mockDatabase.posts.push(newPost)
// Add post to author's posts
author.posts.push(newPost.id)
return res(
ctx.delay(500), // Simulate network delay
ctx.data({
createPost: {
...newPost,
author
}
})
)
}),
// Mutation: Update post
graphql.mutation('UpdatePost', async (req, res, ctx) => {
const { id, input } = req.variables
const postIndex = mockDatabase.posts.findIndex(p => p.id === id)
if (postIndex === -1) {
return res(
ctx.errors([
{
message: 'Post not found',
extensions: { code: 'POST_NOT_FOUND' }
}
])
)
}
// Update post
const updatedPost = {
...mockDatabase.posts[postIndex],
...input,
updatedAt: new Date().toISOString()
}
mockDatabase.posts[postIndex] = updatedPost
const author = mockDatabase.users.find(u => u.id === updatedPost.authorId)
return res(
ctx.delay(200),
ctx.data({
updatePost: {
...updatedPost,
author
}
})
)
}),
// Mutation: Delete post
graphql.mutation('DeletePost', async (req, res, ctx) => {
const { id } = req.variables
const postIndex = mockDatabase.posts.findIndex(p => p.id === id)
if (postIndex === -1) {
return res(
ctx.errors([
{
message: 'Post not found',
extensions: { code: 'POST_NOT_FOUND' }
}
])
)
}
// Remove post from database
const deletedPost = mockDatabase.posts.splice(postIndex, 1)[0]
// Remove post ID from author's posts
const author = mockDatabase.users.find(u => u.id === deletedPost.authorId)
if (author) {
author.posts = author.posts.filter(postId => postId !== id)
}
// Remove related comments
mockDatabase.comments = mockDatabase.comments.filter(
comment => comment.postId !== id
)
return res(
ctx.delay(100),
ctx.data({
deletePost: {
id: deletedPost.id,
success: true
}
})
)
}),
// Mutation: Add comment
graphql.mutation('AddComment', async (req, res, ctx) => {
const { postId, content, authorId } = req.variables
// Validate post and author exist
const post = mockDatabase.posts.find(p => p.id === postId)
const author = mockDatabase.users.find(u => u.id === authorId)
if (!post) {
return res(
ctx.errors([
{
message: 'Post not found',
extensions: { code: 'POST_NOT_FOUND' }
}
])
)
}
if (!author) {
return res(
ctx.errors([
{
message: 'Author not found',
extensions: { code: 'AUTHOR_NOT_FOUND' }
}
])
)
}
// Create new comment
const newComment = {
id: (mockDatabase.comments.length + 1).toString(),
postId,
authorId,
content,
createdAt: new Date().toISOString()
}
mockDatabase.comments.push(newComment)
return res(
ctx.delay(150),
ctx.data({
addComment: {
...newComment,
author
}
})
)
})
]
// Helper function for search score calculation
function calculateMatchScore(post, searchTerm) {
let score = 0
const lowerSearchTerm = searchTerm.toLowerCase()
// Title matches get higher score
if (post.title.toLowerCase().includes(lowerSearchTerm)) {
score += 10
}
// Content matches
if (post.content.toLowerCase().includes(lowerSearchTerm)) {
score += 5
}
// Tag matches
post.tags.forEach(tag => {
if (tag.toLowerCase().includes(lowerSearchTerm)) {
score += 3
}
})
return score
}
// GraphQL Subscription handler (experimental)
export const subscriptionHandlers = [
graphql.operation('PostCreated', (req, res, ctx) => {
// This simulates a real-time subscription
// In a real scenario, you'd use WebSocket connections
return res(
ctx.data({
postCreated: {
id: 'new-post-id',
title: 'New Post Created',
content: 'This is a newly created post',
authorId: '1',
createdAt: new Date().toISOString()
}
})
)
})
]
// Setup server for GraphQL
export const graphqlServer = setupServer(...graphqlHandlers)
// File: src/mocks/graphqlServer.js
import { setupServer } from 'msw/node'
import { graphqlHandlers } from './graphqlHandlers'
export const server = setupServer(...graphqlHandlers)
// File: src/mocks/graphqlBrowser.js
import { setupWorker } from 'msw/browser'
import { graphqlHandlers } from './graphqlHandlers'
export const worker = setupWorker(...graphqlHandlers)
// Usage Example - React Component
// File: src/components/PostManager.js
import React, { useState, useEffect } from 'react'
import { useQuery, useMutation, useSubscription } from '@apollo/client'
import { gql } from '@apollo/client'
const GET_POSTS = gql`
query GetPosts($limit: Int, $offset: Int, $authorId: ID, $tags: [String]) {
posts(limit: $limit, offset: $offset, authorId: $authorId, tags: $tags) {
id
title
content
createdAt
author {
id
name
email
}
comments {
id
content
createdAt
author {
name
}
}
tags
}
postsCount
}
`
const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
createdAt
author {
id
name
email
}
tags
}
}
`
const POST_CREATED_SUBSCRIPTION = gql`
subscription PostCreated {
postCreated {
id
title
content
author {
name
}
createdAt
}
}
`
export default function PostManager() {
const [limit] = useState(10)
const [offset, setOffset] = useState(0)
const [filter, setFilter] = useState({})
const { data, loading, error, refetch } = useQuery(GET_POSTS, {
variables: { limit, offset, ...filter }
})
const [createPost, { loading: creating }] = useMutation(CREATE_POST, {
onCompleted: () => {
refetch()
}
})
// Subscription for real-time updates (if supported)
const { data: subscriptionData } = useSubscription(POST_CREATED_SUBSCRIPTION)
const handleCreatePost = async (postData) => {
try {
await createPost({
variables: {
input: {
...postData,
authorId: '1' // Mock current user
}
}
})
} catch (error) {
console.error('Error creating post:', error)
}
}
if (loading) return <div>Loading posts...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<h1>Post Manager</h1>
{/* New post creation form */}
<div style={{ marginBottom: '2rem', padding: '1rem', border: '1px solid #ccc' }}>
<h2>Create New Post</h2>
<form onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
handleCreatePost({
title: formData.get('title'),
content: formData.get('content'),
tags: formData.get('tags')?.split(',').map(tag => tag.trim())
})
}}>
<div style={{ marginBottom: '0.5rem' }}>
<input
name="title"
placeholder="Post title"
required
style={{ width: '100%', padding: '0.5rem' }}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<textarea
name="content"
placeholder="Post content"
required
style={{ width: '100%', padding: '0.5rem', minHeight: '100px' }}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<input
name="tags"
placeholder="Tags (comma-separated)"
style={{ width: '100%', padding: '0.5rem' }}
/>
</div>
<button
type="submit"
disabled={creating}
style={{ padding: '0.5rem 1rem', backgroundColor: '#0070f3', color: 'white' }}
>
{creating ? 'Creating...' : 'Create Post'}
</button>
</form>
</div>
{/* Filters */}
<div style={{ marginBottom: '1rem' }}>
<input
placeholder="Filter by author ID"
onChange={(e) => setFilter(prev => ({
...prev,
authorId: e.target.value || undefined
}))}
style={{ marginRight: '0.5rem', padding: '0.25rem' }}
/>
<input
placeholder="Filter by tags (comma-separated)"
onChange={(e) => setFilter(prev => ({
...prev,
tags: e.target.value ? e.target.value.split(',').map(tag => tag.trim()) : undefined
}))}
style={{ padding: '0.25rem' }}
/>
</div>
{/* Posts list */}
<div>
<h2>Posts ({data?.postsCount || 0})</h2>
{data?.posts?.map(post => (
<div key={post.id} style={{ marginBottom: '1rem', padding: '1rem', border: '1px solid #ddd' }}>
<h3>{post.title}</h3>
<p>By: {post.author?.name}</p>
<p>{post.content}</p>
<div style={{ fontSize: '0.8rem', color: '#666' }}>
Tags: {post.tags?.join(', ') || 'None'} |
Created: {new Date(post.createdAt).toLocaleDateString()}
</div>
{/* Comments */}
<div style={{ marginTop: '1rem' }}>
<h4>Comments ({post.comments?.length || 0})</h4>
{post.comments?.map(comment => (
<div key={comment.id} style={{
marginLeft: '1rem',
padding: '0.5rem',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
marginBottom: '0.5rem'
}}>
<strong>{comment.author?.name}:</strong> {comment.content}
<div style={{ fontSize: '0.7rem', color: '#999' }}>
{new Date(comment.createdAt).toLocaleDateString()}
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Pagination */}
<div style={{ marginTop: '2rem', textAlign: 'center' }}>
<button
onClick={() => setOffset(prev => Math.max(0, prev - limit))}
disabled={offset === 0}
style={{ marginRight: '1rem', padding: '0.5rem 1rem' }}
>
Previous
</button>
<span>Page {Math.floor(offset / limit) + 1}</span>
<button
onClick={() => setOffset(prev => prev + limit)}
disabled={!data?.posts || data.posts.length < limit}
style={{ marginLeft: '1rem', padding: '0.5rem 1rem' }}
>
Next
</button>
</div>
{/* Real-time updates */}
{subscriptionData?.postCreated && (
<div style={{
position: 'fixed',
bottom: '1rem',
right: '1rem',
padding: '1rem',
backgroundColor: '#4CAF50',
color: 'white',
borderRadius: '4px'
}}>
<strong>New Post Alert!</strong><br/>
"{subscriptionData.postCreated.title}" by {subscriptionData.postCreated.author?.name}
</div>
)}
</div>
)
}
💻 高级 MSW 模式 javascript
🔴 complex
⭐⭐⭐⭐⭐
高级MSW技术,包含动态响应、WebSocket模拟、身份验证流程和性能测试
⏱️ 45 min
🏷️ msw, advanced, authentication, websockets, performance
Prerequisites:
MSW basics, JavaScript advanced, WebSocket concepts, Authentication flows, Performance testing
// Advanced Mock Service Worker Patterns
// File: src/mocks/advancedHandlers.js
import { rest, graphql, WebSocketLink } from 'msw'
import { setupServer } from 'msw/node'
// 1. Dynamic Response Generation
export const dynamicHandlers = [
// Dynamic user generator
rest.get('/api/dynamic-users/:count', (req, res, ctx) => {
const count = parseInt(req.params.count) || 10
const seed = req.url.searchParams.get('seed') || 'default'
// Seeded random number generator for consistent responses
const random = createSeededRandom(seed)
const users = Array.from({ length: count }, (_, index) => ({
id: index + 1,
name: generateRandomName(random),
email: `user${index + 1}@${generateRandomDomain(random)}`,
age: Math.floor(random() * 50) + 18,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}-${index}`,
bio: generateRandomBio(random),
skills: generateRandomSkills(random),
createdAt: new Date(Date.now() - random() * 365 * 24 * 60 * 60 * 1000).toISOString()
}))
return res(
ctx.json({
users,
generated: new Date().toISOString(),
seed
})
)
}),
// Simulated real-time data updates
rest.get('/api/stocks/:symbol', (req, res, ctx) => {
const { symbol } = req.params
const initialPrice = Math.random() * 1000 + 50
// Generate realistic stock price fluctuations
const generatePriceHistory = () => {
let currentPrice = initialPrice
return Array.from({ length: 100 }, (_, index) => {
const change = (Math.random() - 0.5) * 10
currentPrice = Math.max(1, currentPrice + change)
return {
timestamp: Date.now() - (99 - index) * 60000, // Last 100 minutes
price: parseFloat(currentPrice.toFixed(2)),
volume: Math.floor(Math.random() * 1000000) + 10000
}
})
}
return res(
ctx.json({
symbol,
currentPrice: parseFloat(initialPrice.toFixed(2)),
change: parseFloat(((Math.random() - 0.5) * 20).toFixed(2)),
changePercent: parseFloat(((Math.random() - 0.5) * 5).toFixed(2)),
volume: Math.floor(Math.random() * 10000000) + 100000,
history: generatePriceHistory()
})
)
})
]
// 2. Authentication Flow Handlers
export const authHandlers = [
// Login endpoint
rest.post('/api/auth/login', async (req, res, ctx) => {
const { email, password } = await req.json()
// Simulate authentication delay
await ctx.delay(800)
// Mock user database
const users = [
{ email: '[email protected]', password: 'admin123', role: 'admin' },
{ email: '[email protected]', password: 'user123', role: 'user' },
{ email: '[email protected]', password: 'mod123', role: 'moderator' }
]
const user = users.find(u => u.email === email && u.password === password)
if (!user) {
return res(
ctx.status(401),
ctx.json({
error: 'Invalid credentials',
code: 'INVALID_CREDENTIALS'
})
)
}
// Generate JWT-like token
const token = generateMockToken(user)
const refreshToken = generateMockToken(user, 'refresh')
return res(
ctx.json({
user: {
id: user.email === '[email protected]' ? 1 :
user.email === '[email protected]' ? 2 : 3,
email: user.email,
role: user.role,
permissions: getPermissionsForRole(user.role)
},
tokens: {
accessToken: token,
refreshToken: refreshToken,
expiresIn: 3600
}
})
)
}),
// Token refresh
rest.post('/api/auth/refresh', async (req, res, ctx) => {
const { refreshToken } = await req.json()
if (!refreshToken) {
return res(
ctx.status(401),
ctx.json({ error: 'Refresh token required' })
)
}
// Validate refresh token (simplified)
try {
const payload = parseMockToken(refreshToken)
if (payload.type !== 'refresh') {
return res(
ctx.status(401),
ctx.json({ error: 'Invalid refresh token' })
)
}
// Generate new access token
const newAccessToken = generateMockToken(payload.user)
return res(
ctx.json({
accessToken: newAccessToken,
expiresIn: 3600
})
)
} catch (error) {
return res(
ctx.status(401),
ctx.json({ error: 'Invalid refresh token' })
)
}
}),
// Protected route middleware
rest.get('/api/protected/profile', (req, res, ctx) => {
const authHeader = req.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res(
ctx.status(401),
ctx.json({ error: 'Authorization required' })
)
}
const token = authHeader.slice(7)
try {
const payload = parseMockToken(token)
// Check token expiration
if (payload.exp < Date.now() / 1000) {
return res(
ctx.status(401),
ctx.json({ error: 'Token expired' })
)
}
// Return user profile
return res(
ctx.json({
user: payload.user,
permissions: getPermissionsForRole(payload.user.role)
})
)
} catch (error) {
return res(
ctx.status(401),
ctx.json({ error: 'Invalid token' })
)
}
})
]
// 3. WebSocket Mocking
export const wsHandlers = [
// WebSocket connection for real-time chat
rest.post('/api/chat/connect', (req, res, ctx) => {
return res(
ctx.json({
wsUrl: 'wss://mock-chat.example.com/ws',
userId: Math.random().toString(36).substr(2, 9),
sessionId: generateMockSessionId()
})
)
})
]
// WebSocket server simulation
export const mockWebSocketServer = {
connections: new Map(),
simulateConnection(userId, sessionId) {
return {
userId,
sessionId,
connected: true,
lastActivity: Date.now(),
messageCount: 0
}
},
simulateMessage(fromUserId, toUserId, content) {
return {
id: Math.random().toString(36).substr(2, 9),
from: fromUserId,
to: toUserId,
content,
timestamp: Date.now(),
status: 'delivered'
}
}
}
// 4. Performance Testing Handlers
export const performanceHandlers = [
// Simulate slow API responses
rest.get('/api/slow-endpoint', (req, res, ctx) => {
const delay = parseInt(req.url.searchParams.get('delay')) || 2000
return res(
ctx.delay(delay),
ctx.json({
message: `Response delayed by ${delay}ms`,
timestamp: Date.now()
})
)
}),
// Simulate large data responses
rest.get('/api/large-dataset', (req, res, ctx) => {
const size = parseInt(req.url.searchParams.get('size')) || 1000
const largeDataset = Array.from({ length: size }, (_, index) => ({
id: index + 1,
name: `Item ${index + 1}`,
description: `This is a detailed description for item ${index + 1}`,
metadata: {
category: `Category ${(index % 10) + 1}`,
tags: [`tag${index % 5}`, `tag${(index + 1) % 5}`],
rating: Math.random() * 5,
price: parseFloat((Math.random() * 1000).toFixed(2)),
inStock: Math.random() > 0.2,
createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString()
}
}))
return res(
ctx.json({
data: largeDataset,
total: largeDataset.length,
generatedAt: Date.now(),
size: JSON.stringify(largeDataset).length
})
)
}),
// Simulate concurrent requests stress test
rest.get('/api/stress-test', (req, res, ctx) => {
const iterations = parseInt(req.url.searchParams.get('iterations')) || 100
// Simulate CPU-intensive work
const result = []
for (let i = 0; i < iterations; i++) {
result.push({
iteration: i,
fibonacci: fibonacci(Math.min(i, 30)),
timestamp: Date.now()
})
}
return res(
ctx.delay(100),
ctx.json({
iterations,
results: result,
completedAt: Date.now()
})
)
})
]
// 5. Error Simulation Handlers
export const errorHandlers = [
// Configurable error scenarios
rest.get('/api/error-scenario/:type', (req, res, ctx) => {
const { type } = req.params
switch (type) {
case 'timeout':
return res(
ctx.delay(30000), // 30 second timeout
ctx.status(200),
ctx.json({ message: 'This will timeout' })
)
case 'network-error':
return res.networkError('Network connection failed')
case 'server-error':
return res(
ctx.status(500),
ctx.json({
error: 'Internal server error',
details: 'Something went terribly wrong',
timestamp: Date.now(),
requestId: generateRequestId()
})
)
case 'rate-limit':
return res(
ctx.status(429),
ctx.set('Retry-After', '60'),
ctx.json({
error: 'Rate limit exceeded',
message: 'Too many requests, please try again later',
retryAfter: 60
})
)
case 'maintenance':
return res(
ctx.status(503),
ctx.set('Retry-After', '3600'),
ctx.json({
error: 'Service unavailable',
message: 'We are currently under maintenance',
estimatedDowntime: '1 hour'
})
)
default:
return res(
ctx.status(400),
ctx.json({
error: 'Invalid error scenario',
validScenarios: ['timeout', 'network-error', 'server-error', 'rate-limit', 'maintenance']
})
)
}
}),
// Flaky endpoint that fails intermittently
rest.get('/api/flaky-endpoint', (req, res, ctx) => {
const failureRate = parseFloat(req.url.searchParams.get('failureRate')) || 0.3
if (Math.random() < failureRate) {
return res(
ctx.status(500),
ctx.json({
error: 'Random failure occurred',
failureRate,
timestamp: Date.now()
})
)
}
return res(
ctx.json({
message: 'Success!',
failureRate,
timestamp: Date.now()
})
)
})
]
// 6. Request/Response Interception and Modification
export const interceptionHandlers = [
// Modify request data
rest.post('/api/upload', async (req, res, ctx) => {
const originalBody = await req.json()
// Log and modify the request
console.log('Original request:', originalBody)
const modifiedBody = {
...originalBody,
processedAt: Date.now(),
enhanced: true
}
return res(
ctx.status(200),
ctx.json({
message: 'File uploaded successfully',
originalData: originalBody,
processedData: modifiedBody,
serverSide: true
})
)
}),
// Response transformation
rest.get('/api/data/*', (req, res, ctx) => {
// Let the original request go through
return fetch(req).then(response => {
if (!response.ok) {
return response
}
return response.json().then(data => {
// Transform the response
const transformedData = {
...data,
enhanced: true,
processedAt: Date.now(),
metadata: {
requestUrl: req.url,
method: req.method,
userAgent: req.headers.get('user-agent')
}
}
return res(
ctx.json(transformedData)
)
})
}).catch(() => {
return res(
ctx.status(500),
ctx.json({ error: 'Failed to fetch original data' })
)
})
})
]
// Utility Functions
function createSeededRandom(seed) {
let m = 0x80000000
let a = 1103515245
let c = 12345
let state = Math.abs(seed.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) || 1
return function() {
state = (a * state + c) % m
return state / m
}
}
function generateRandomName(random) {
const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Charlie', 'Diana', 'Edward', 'Fiona']
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']
return firstNames[Math.floor(random() * firstNames.length)] + ' ' +
lastNames[Math.floor(random() * lastNames.length)]
}
function generateRandomDomain(random) {
const domains = ['example.com', 'test.org', 'demo.net', 'sample.io', 'mock.dev']
return domains[Math.floor(random() * domains.length)]
}
function generateRandomBio(random) {
const bios = [
'Software developer passionate about clean code',
'Tech enthusiast and lifelong learner',
'Building amazing web applications',
'Full-stack developer with creative flair',
'Open source contributor and mentor'
]
return bios[Math.floor(random() * bios.length)]
}
function generateRandomSkills(random) {
const allSkills = ['JavaScript', 'React', 'Node.js', 'Python', 'TypeScript', 'GraphQL', 'Docker', 'AWS']
const skillCount = Math.floor(random() * 4) + 2
const selectedSkills = []
for (let i = 0; i < skillCount; i++) {
const skill = allSkills[Math.floor(random() * allSkills.length)]
if (!selectedSkills.includes(skill)) {
selectedSkills.push(skill)
}
}
return selectedSkills
}
function generateMockToken(user, type = 'access') {
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
const payload = btoa(JSON.stringify({
sub: user.email,
role: user.role,
type,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (type === 'refresh' ? 86400 : 3600),
user: { email: user.email, role: user.role }
}))
const signature = btoa('mock-signature')
return `${header}.${payload}.${signature}`
}
function parseMockToken(token) {
const [, payload] = token.split('.')
return JSON.parse(atob(payload))
}
function getPermissionsForRole(role) {
const permissions = {
admin: ['read', 'write', 'delete', 'manage_users'],
moderator: ['read', 'write', 'moderate'],
user: ['read', 'write_own']
}
return permissions[role] || ['read']
}
function generateMockSessionId() {
return 'session_' + Math.random().toString(36).substr(2, 9)
}
function generateRequestId() {
return 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
}
function fibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b]
}
return b
}
// Export all handlers
export const allHandlers = [
...dynamicHandlers,
...authHandlers,
...wsHandlers,
...performanceHandlers,
...errorHandlers,
...interceptionHandlers
]
// Setup comprehensive server
export const advancedServer = setupServer(...allHandlers)
// File: src/mocks/advancedBrowser.js
import { setupWorker } from 'msw/browser'
import { allHandlers } from './advancedHandlers'
export const worker = setupWorker(...allHandlers)
// Usage Example - Advanced React Component
// File: src/components/AdvancedApiDemo.js
import React, { useState, useEffect, useCallback } from 'react'
export default function AdvancedApiDemo() {
const [dynamicUsers, setDynamicUsers] = useState([])
const [stockData, setStockData] = useState(null)
const [authUser, setAuthUser] = useState(null)
const [performanceData, setPerformanceData] = useState(null)
const [errorScenario, setErrorScenario] = useState('')
const [loading, setLoading] = useState(false)
// Generate dynamic users
const generateUsers = useCallback(async (count = 10, seed = 'default') => {
setLoading(true)
try {
const response = await fetch(`/api/dynamic-users/${count}?seed=${seed}`)
const data = await response.json()
setDynamicUsers(data.users)
} catch (error) {
console.error('Error generating users:', error)
} finally {
setLoading(false)
}
}, [])
// Fetch stock data
const fetchStockData = useCallback(async (symbol) => {
try {
const response = await fetch(`/api/stocks/${symbol}`)
const data = await response.json()
setStockData(data)
} catch (error) {
console.error('Error fetching stock data:', error)
}
}, [])
// Login simulation
const handleLogin = useCallback(async (email, password) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await response.json()
if (response.ok) {
setAuthUser(data.user)
localStorage.setItem('accessToken', data.tokens.accessToken)
return data
} else {
throw new Error(data.error)
}
} catch (error) {
console.error('Login error:', error)
throw error
}
}, [])
// Fetch protected data
const fetchProtectedData = useCallback(async () => {
try {
const token = localStorage.getItem('accessToken')
const response = await fetch('/api/protected/profile', {
headers: { 'Authorization': `Bearer ${token}` }
})
const data = await response.json()
return data
} catch (error) {
console.error('Error fetching protected data:', error)
throw error
}
}, [])
// Performance test
const runPerformanceTest = useCallback(async (iterations = 100) => {
setLoading(true)
try {
const response = await fetch(`/api/stress-test?iterations=${iterations}`)
const data = await response.json()
setPerformanceData(data)
} catch (error) {
console.error('Performance test error:', error)
} finally {
setLoading(false)
}
}, [])
// Test error scenarios
const testErrorScenario = useCallback(async (type) => {
setLoading(true)
setErrorScenario(type)
try {
const response = await fetch(`/api/error-scenario/${type}`)
const data = await response.json()
console.log('Error scenario data:', data)
} catch (error) {
console.error(`Error scenario ${type}:`, error)
} finally {
setLoading(false)
setErrorScenario('')
}
}, [])
useEffect(() => {
// Initialize with some data
generateUsers(10, 'demo')
fetchStockData('AAPL')
}, [generateUsers, fetchStockData])
return (
<div style={{ padding: '2rem', fontFamily: 'Arial, sans-serif' }}>
<h1>Advanced API Demo</h1>
{/* Dynamic Users */}
<section style={{ marginBottom: '2rem' }}>
<h2>Dynamic User Generation</h2>
<div style={{ marginBottom: '1rem' }}>
<input
type="number"
placeholder="Count"
defaultValue="10"
style={{ marginRight: '0.5rem', padding: '0.5rem' }}
onChange={(e) => {
const count = parseInt(e.target.value)
const seed = 'dynamic-' + Date.now()
generateUsers(count, seed)
}}
/>
<button onClick={() => generateUsers(10, 'random-' + Date.now())}>
Generate New Users
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '1rem' }}>
{dynamicUsers.map(user => (
<div key={user.id} style={{ padding: '1rem', border: '1px solid #ddd', borderRadius: '8px' }}>
<img src={user.avatar} alt={user.name} style={{ width: '50px', height: '50px', borderRadius: '50%' }} />
<h4>{user.name}</h4>
<p>{user.email}</p>
<p>Age: {user.age}</p>
<p>{user.bio}</p>
<div>
{user.skills.map(skill => (
<span key={skill} style={{
display: 'inline-block',
padding: '0.25rem 0.5rem',
margin: '0.125rem',
backgroundColor: '#e0e0e0',
borderRadius: '12px',
fontSize: '0.75rem'
}}>
{skill}
</span>
))}
</div>
</div>
))}
</div>
</section>
{/* Stock Data */}
<section style={{ marginBottom: '2rem' }}>
<h2>Real-time Stock Simulation</h2>
<div style={{ marginBottom: '1rem' }}>
<input
type="text"
placeholder="Stock symbol"
defaultValue="AAPL"
onChange={(e) => fetchStockData(e.target.value)}
style={{ padding: '0.5rem', marginRight: '0.5rem' }}
/>
</div>
{stockData && (
<div style={{ padding: '1rem', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<h3>{stockData.symbol}</h3>
<p style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
${stockData.currentPrice}
</p>
<p style={{ color: stockData.change >= 0 ? 'green' : 'red' }}>
{stockData.change >= 0 ? '+' : ''}{stockData.change} ({stockData.changePercent}%)
</p>
<p>Volume: {stockData.volume.toLocaleString()}</p>
</div>
)}
</section>
{/* Authentication Demo */}
<section style={{ marginBottom: '2rem' }}>
<h2>Authentication Flow</h2>
{!authUser ? (
<div>
<button onClick={() => handleLogin('[email protected]', 'admin123')}>
Login as Admin
</button>
<button onClick={() => handleLogin('[email protected]', 'user123')} style={{ marginLeft: '0.5rem' }}>
Login as User
</button>
</div>
) : (
<div>
<p>Logged in as: {authUser.email}</p>
<p>Role: {authUser.role}</p>
<p>Permissions: {authUser.permissions.join(', ')}</p>
<button onClick={async () => {
try {
await fetchProtectedData()
alert('Protected data accessed successfully!')
} catch (error) {
alert('Error accessing protected data: ' + error.message)
}
}}>
Test Protected Route
</button>
<button onClick={() => {
setAuthUser(null)
localStorage.removeItem('accessToken')
}} style={{ marginLeft: '0.5rem' }}>
Logout
</button>
</div>
)}
</section>
{/* Performance Testing */}
<section style={{ marginBottom: '2rem' }}>
<h2>Performance Testing</h2>
<div style={{ marginBottom: '1rem' }}>
<input
type="number"
placeholder="Iterations"
defaultValue="100"
style={{ marginRight: '0.5rem', padding: '0.5rem' }}
/>
<button onClick={() => {
const input = document.querySelector('input[placeholder="Iterations"]')
runPerformanceTest(parseInt(input.value))
}}>
Run Stress Test
</button>
</div>
{performanceData && (
<div style={{ padding: '1rem', backgroundColor: '#f0f0f0', borderRadius: '8px' }}>
<p>Completed {performanceData.iterations} iterations</p>
<p>Results processed in {performanceData.completedAt - performanceData.results[0].timestamp}ms</p>
<p>Final Fibonacci value: {performanceData.results[performanceData.results.length - 1]?.fibonacci}</p>
</div>
)}
</section>
{/* Error Scenarios */}
<section style={{ marginBottom: '2rem' }}>
<h2>Error Scenario Testing</h2>
<div style={{ marginBottom: '1rem' }}>
{['timeout', 'network-error', 'server-error', 'rate-limit', 'maintenance'].map(type => (
<button
key={type}
onClick={() => testErrorScenario(type)}
disabled={loading && errorScenario === type}
style={{ marginRight: '0.5rem', marginBottom: '0.5rem', padding: '0.5rem' }}
>
Test {type.replace('-', ' ')}
</button>
))}
</div>
{errorScenario && (
<div style={{ padding: '1rem', backgroundColor: '#fff3cd', borderRadius: '8px' }}>
<p>Testing error scenario: <strong>{errorScenario}</strong></p>
{loading && <p>Loading... (this may take a while for timeout scenarios)</p>}
</div>
)}
</section>
{loading && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '2rem', borderRadius: '8px' }}>
<p>Loading...</p>
</div>
</div>
)}
</div>
)
}