☰
Vue项目实战:API数据获取与页面渲染全流程
2026/10/27 23:01:44 网站建设 项目流程

1. Vue项目实战:从API数据获取到页面渲染全流程解析

作为前端开发的核心技能之一,Vue与后端API的数据交互是每个开发者必须掌握的实战能力。在实际项目中,我经常遇到新手开发者对数据流转过程理解不清晰的问题——明明接口已经调通,却不知道如何优雅地将数据呈现在页面上。本文将基于我多年Vue项目经验,从接口调用到数据绑定的完整链路进行拆解,重点解决三个核心问题:如何规范地请求API、如何处理异步响应、如何高效更新视图。

2. 环境准备与项目基础配置

2.1 初始化Vue项目

推荐使用Vue CLI创建项目(确保已安装Node.js):

npm install -g @vue/cli vue create vue-api-demo cd vue-api-demo

2.2 安装必要依赖

现代Vue项目通常需要以下库:

npm install axios vue-router

注意:虽然浏览器原生支持fetch API,但axios提供了更完善的错误处理和拦截器机制,建议作为生产环境首选

3. API请求核心实现

3.1 创建axios实例

在src/utils/request.js中配置基础请求实例:

import axios from 'axios' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截器 service.interceptors.request.use(config => { // 可在此处添加token等认证信息 return config }, error => { return Promise.reject(error) }) // 响应拦截器 service.interceptors.response.use( response => { const res = response.data // 根据后端约定处理状态码 if (res.code !== 200) { return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { return Promise.reject(error) } ) export default service

3.2 封装API模块

建议按业务模块组织API请求,例如src/api/user.js:

import request from '@/utils/request' export function getUserList(params) { return request({ url: '/user/list', method: 'get', params }) } export function createUser(data) { return request({ url: '/user/create', method: 'post', data }) }

4. 组件中的数据绑定实践

4.1 类组件写法(Options API)

<template> <div> <ul> <li v-for="user in userList" :key="user.id"> {{ user.name }} - {{ user.email }} </li> </ul> </div> </template> <script> import { getUserList } from '@/api/user' export default { data() { return { userList: [] } }, created() { this.fetchData() }, methods: { async fetchData() { try { const res = await getUserList({ page: 1 }) this.userList = res.data.list } catch (error) { console.error('获取用户列表失败:', error) } } } } </script>

4.2 组合式API写法(Composition API)

<template> <!-- 同上 --> </template> <script setup> import { ref, onMounted } from 'vue' import { getUserList } from '@/api/user' const userList = ref([]) const fetchData = async () => { try { const res = await getUserList({ page: 1 }) userList.value = res.data.list } catch (error) { console.error('获取用户列表失败:', error) } } onMounted(() => { fetchData() }) </script>

5. 高级技巧与性能优化

5.1 分页加载实现

// 在组合式API中 const loading = ref(false) const page = ref(1) const total = ref(0) const loadMore = async () => { if (loading.value) return loading.value = true try { const res = await getUserList({ page: page.value }) userList.value = [...userList.value, ...res.data.list] total.value = res.data.total page.value++ } finally { loading.value = false } } // 配合滚动监听使用 window.addEventListener('scroll', () => { if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) { loadMore() } })

5.2 数据缓存策略

// 使用Pinia进行状态管理 import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ userList: [], lastFetchTime: null }), actions: { async fetchUsers(forceRefresh = false) { // 1小时内不重复请求 if (!forceRefresh && this.lastFetchTime && Date.now() - this.lastFetchTime < 3600000) { return } const res = await getUserList() this.userList = res.data.list this.lastFetchTime = Date.now() } } })

6. 常见问题排查指南

6.1 跨域问题解决方案

开发环境配置代理(vue.config.js):

module.exports = { devServer: { proxy: { '/api': { target: 'http://your-api-server.com', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

6.2 400错误常见原因

  1. 请求参数格式不符(如需要JSON但发送了FormData)
  2. 缺少必填字段
  3. 参数值类型错误(如字符串传了数字)
  4. 认证信息缺失或过期

6.3 数据更新但视图不渲染

典型场景及解决方案:

  1. 数组变更:使用push()等变异方法或this.$set()
  2. 对象新增属性:使用this.$set()或展开运算符
  3. 异步更新队列:在nextTick中操作DOM

7. 调试工具推荐

  1. Vue Devtools:组件状态检查
  2. Chrome Network面板:查看请求详情
  3. axios拦截器添加日志:
service.interceptors.request.use(config => { console.log('[Request]', config.method.toUpperCase(), config.url) return config })

8. 安全最佳实践

  1. 敏感数据过滤:
// 在响应拦截器中 const sensitiveFields = ['password', 'token'] service.interceptors.response.use(response => { if (response.data?.data) { sensitiveFields.forEach(field => { if (response.data.data[field]) { response.data.data[field] = '***' } }) } return response })
  1. 请求频率限制:
const requestQueue = new Map() service.interceptors.request.use(config => { const key = `${config.method}-${config.url}` if (requestQueue.has(key)) { return Promise.reject(new Error('请求过于频繁')) } requestQueue.set(key, true) setTimeout(() => requestQueue.delete(key), 1000) return config })

9. 项目结构建议

规范的API调用项目应包含:

src/ ├── api/ # API模块 │ ├── user.js # 用户相关 │ └── product.js # 产品相关 ├── stores/ # 状态管理 ├── utils/ │ └── request.js # axios封装 └── views/ └── UserList.vue # 业务组件

10. 测试策略

10.1 单元测试示例(使用Jest)

import { getUserList } from '@/api/user' import axios from 'axios' jest.mock('axios') describe('User API', () => { it('fetches user list successfully', async () => { const mockData = { list: [{ id: 1, name: 'Test' }] } axios.get.mockResolvedValue({ data: mockData }) const result = await getUserList({ page: 1 }) expect(result).toEqual(mockData) expect(axios.get).toHaveBeenCalledWith('/user/list', { params: { page: 1 } }) }) })

10.2 E2E测试建议(Cypress)

describe('User List', () => { it('displays users from API', () => { cy.intercept('GET', '/user/list', { fixture: 'users.json' }) cy.visit('/users') cy.get('li').should('have.length', 5) }) })

11. 性能监控方案

  1. 添加API性能埋点:
service.interceptors.request.use(config => { config.metadata = { startTime: Date.now() } return config }) service.interceptors.response.use(response => { const duration = Date.now() - response.config.metadata.startTime if (duration > 1000) { console.warn(`API响应缓慢: ${response.config.url} 耗时${duration}ms`) } return response })
  1. 使用web-vitals监控关键指标:
import { getCLS, getFID, getLCP } from 'web-vitals' function sendToAnalytics(metric) { if (metric.name === 'LCP' && metric.value > 2500) { console.warn('性能告警:', metric) } } getCLS(sendToAnalytics) getFID(sendToAnalytics) getLCP(sendToAnalytics)

12. 移动端适配要点

  1. 请求超时调整:
const service = axios.create({ timeout: navigator.connection?.effectiveType === 'slow-2g' ? 30000 : 10000 })
  1. 离线处理策略:
const fetchData = async () => { try { const res = await getUserList() // 更新本地缓存 localStorage.setItem('userList', JSON.stringify(res.data)) return res.data } catch (err) { if (err.message.includes('offline')) { // 从缓存读取 const cache = localStorage.getItem('userList') return cache ? JSON.parse(cache) : null } throw err } }

13. 服务端渲染(SSR)特别处理

在nuxt.js中的asyncData用法:

export default { async asyncData({ $axios }) { try { const res = await $axios.get('/user/list') return { userList: res.data.list } } catch (error) { return { userList: [] } } } }

14. 类型安全增强(TypeScript)

  1. 定义接口类型:
interface User { id: number name: string email: string } interface ApiResponse<T> { code: number message: string data: T } export function getUserList( params: { page: number } ): Promise<ApiResponse<{ list: User[] }>> { return request({ url: '/user/list', method: 'get', params }) }
  1. 组件中使用:
const userList = ref<User[]>([])

15. 微前端场景下的特殊处理

  1. 共享axios实例:
// 在主应用中 export const sharedAxios = axios.create({ /* 配置 */ }) // 在子应用中 import { sharedAxios } from 'main-app/utils'
  1. 接口前缀隔离:
const service = axios.create({ baseURL: window.__POWERED_BY_QIANKUN__ ? '/sub-app/api' : '/api' })

16. 灰度发布方案

  1. 接口版本控制:
service.interceptors.request.use(config => { if (localStorage.getItem('useNewAPI') === 'true') { config.headers['X-API-Version'] = 'v2' } return config })
  1. AB测试实现:
const apiEndpoint = Math.random() > 0.5 ? '/api/v2/user/list' : '/api/v1/user/list'

17. 大数据量优化策略

  1. 虚拟滚动实现:
<template> <RecycleScroller :items="userList" :item-size="50" key-field="id" > <template v-slot="{ item }"> <div>{{ item.name }}</div> </template> </RecycleScroller> </template>
  1. 分片加载技术:
const loadChunk = async (start, size) => { const res = await getUserList({ start, limit: size }) return res.data.list } // 使用IntersectionObserver触发分片加载

18. WebSocket实时更新方案

const socket = new WebSocket('wss://api.example.com') socket.onmessage = (event) => { const data = JSON.parse(event.data) if (data.type === 'user_update') { // 更新本地数据 const index = userList.value.findIndex(u => u.id === data.user.id) if (index >= 0) { userList.value.splice(index, 1, data.user) } } }

19. 前端缓存策略进阶

  1. 内存缓存实现:
const apiCache = new Map() export function getUserList(params) { const cacheKey = JSON.stringify(params) if (apiCache.has(cacheKey)) { return Promise.resolve(apiCache.get(cacheKey)) } return request({ url: '/user/list', method: 'get', params }).then(res => { apiCache.set(cacheKey, res) return res }) }
  1. 缓存失效策略:
// 在用户数据变更时清除相关缓存 export function updateUser(data) { apiCache.clear() // 简单实现 return request({ url: '/user/update', method: 'post', data }) }

20. 错误边界处理

  1. 全局错误捕获:
app.config.errorHandler = (err, vm, info) => { console.error(`[Vue Error] ${info}:`, err) // 上报错误监控系统 }
  1. 组件级错误处理:
<template> <ErrorBoundary> <UserList /> </ErrorBoundary> </template> <script> import ErrorBoundary from './ErrorBoundary.vue' export default { components: { ErrorBoundary } } </script>

21. 权限控制方案

  1. 接口权限拦截:
service.interceptors.request.use(config => { if (config.requireAuth && !store.getters.token) { return Promise.reject(new Error('需要登录')) } return config })
  1. 数据权限过滤:
function filterDataByPermission(data, permissions) { return data.filter(item => item.requiredPermission ? permissions.includes(item.requiredPermission) : true ) }

22. 文件上传下载处理

  1. 文件上传示例:
export function uploadFile(file) { const formData = new FormData() formData.append('file', file) return request({ url: '/upload', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) }
  1. 文件下载处理:
export function downloadFile(id) { return request({ url: `/download/${id}`, method: 'get', responseType: 'blob' }).then(res => { const url = URL.createObjectURL(res.data) const link = document.createElement('a') link.href = url link.download = 'filename.ext' link.click() URL.revokeObjectURL(url) }) }

23. 国际化的接口处理

  1. 语言头设置:
service.interceptors.request.use(config => { config.headers['Accept-Language'] = store.getters.language || 'zh-CN' return config })
  1. 多语言数据转换:
function transformI18nData(data, lang) { return data.map(item => ({ ...item, name: item[`name_${lang}`] || item.name })) }

24. 数据脱敏处理

  1. 敏感信息过滤:
function desensitizeData(data) { return { ...data, phone: data.phone?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'), idCard: data.idCard?.replace(/^(.{6}).+(.{4})$/, '$1********$2') } }
  1. 组件中使用:
const safeData = computed(() => desensitizeData(rawData.value))

25. 性能分析工具集成

  1. 使用Chrome DevTools Protocol:
function startProfiling() { return chrome.devtools.inspectedWindow.eval( 'console.profile("API Performance")' ) } function stopProfiling() { return chrome.devtools.inspectedWindow.eval( 'console.profileEnd("API Performance")' ) } // 在关键操作前后调用 startProfiling() await fetchData() stopProfiling()
  1. 自定义性能标记:
export function measureAPI(apiName, fn) { const start = performance.now() return fn().finally(() => { const duration = performance.now() - start console.log(`[Perf] ${apiName} took ${duration.toFixed(2)}ms`) }) } // 使用示例 measureAPI('getUserList', () => getUserList({ page: 1 }))

26. 前端数据Mock方案

  1. 开发环境Mock:
// mock/user.js export default [ { url: '/user/list', method: 'get', response: ({ query }) => { return { code: 200, data: { list: Array(10).fill(0).map((_, i) => ({ id: i + 1, name: `User ${i + 1}`, email: `user${i + 1}@example.com` })), total: 100 } } } } ]
  1. 使用Mock Service Worker:
// src/mocks/browser.js import { setupWorker, rest } from 'msw' import userMocks from './user' const worker = setupWorker(...userMocks) worker.start()

27. 数据持久化策略

  1. 本地存储封装:
export const storage = { get(key, defaultValue = null) { try { const value = localStorage.getItem(key) return value ? JSON.parse(value) : defaultValue } catch { return defaultValue } }, set(key, value) { localStorage.setItem(key, JSON.stringify(value)) }, remove(key) { localStorage.removeItem(key) } } // 使用示例 storage.set('userList', userList.value)
  1. IndexedDB高级存储:
const dbPromise = idb.openDB('vue-api-db', 1, { upgrade(db) { db.createObjectStore('users', { keyPath: 'id' }) } }) export async function saveUsers(users) { const db = await dbPromise const tx = db.transaction('users', 'readwrite') await Promise.all([ ...users.map(user => tx.store.put(user)), tx.done ]) }

28. 数据可视化集成

  1. 图表数据转换:
function transformToChartData(apiData) { return { labels: apiData.map(item => item.date), datasets: [{ label: '用户增长', data: apiData.map(item => item.count), backgroundColor: '#4CAF50' }] } }
  1. 在组件中使用:
<template> <Bar :data="chartData" /> </template> <script setup> import { Bar } from 'vue-chartjs' import { computed } from 'vue' const props = defineProps(['apiData']) const chartData = computed(() => transformToChartData(props.apiData) ) </script>

29. 移动端手势交互增强

  1. 下拉刷新实现:
import { useSwipe } from '@vueuse/core' const { isSwiping, direction } = useSwipe(refEl) watch([isSwiping, direction], ([swiping, dir]) => { if (swiping && dir === 'down' && window.scrollY === 0) { fetchData() } })
  1. 左滑操作处理:
<template> <div v-for="user in userList" :key="user.id" @touchstart="touchStart" @touchend="touchEnd" > <!-- 用户项内容 --> <div v-if="showActions" class="actions"> <button @click="editUser(user)">编辑</button> </div> </div> </template> <script setup> const touchStartX = ref(0) const showActions = ref(false) const touchStart = (e) => { touchStartX.value = e.touches[0].clientX } const touchEnd = (e) => { const diff = e.changedTouches[0].clientX - touchStartX.value showActions.value = diff < -50 // 左滑超过50px显示操作按钮 } </script>

30. 项目部署优化

  1. 环境变量配置:
# .env.production VUE_APP_BASE_API=https://api.example.com VUE_APP_ENV=production
  1. 接口地址动态配置:
// 支持部署后修改接口地址 window.config = { apiBaseUrl: process.env.VUE_APP_BASE_API } const service = axios.create({ baseURL: window.config.apiBaseUrl })
  1. 构建分析优化:
npm install --save-dev webpack-bundle-analyzer vue-cli-service build --report

31. 微交互优化技巧

  1. 加载状态反馈:
<template> <button @click="fetchData" :disabled="loading"> <span v-if="!loading">刷新数据</span> <span v-else> <LoadingSpinner /> 加载中... </span> </button> </template>
  1. 数据更新动画:
.user-item { transition: all 0.3s ease; } .user-item-update { animation: highlight 1.5s; } @keyframes highlight { 0% { background-color: rgba(255, 235, 59, 0.5); } 100% { background-color: transparent; } }

32. 无障碍访问支持

  1. ARIA属性增强:
<template> <div role="status" aria-live="polite" aria-atomic="true" > 当前加载了 {{ userList.length }} 条用户数据 </div> </template>
  1. 键盘导航支持:
function handleKeyDown(event) { if (event.key === 'ArrowDown') { // 向下导航逻辑 } else if (event.key === 'Enter') { // 确认选择逻辑 } }

33. 数据校验方案

  1. 前端表单校验:
const rules = { name: [ { required: true, message: '请输入姓名' }, { min: 2, max: 10, message: '长度在2到10个字符' } ], email: [ { type: 'email', message: '请输入正确的邮箱格式' } ] }
  1. API响应数据校验:
import { object, string, number } from 'yup' const userSchema = object({ id: number().required(), name: string().required().max(100), email: string().email().required() }) function validateUser(data) { return userSchema.validate(data) } // 在接口响应处理中 try { const user = await validateUser(apiData) } catch (err) { console.error('数据校验失败:', err) }

34. 前端监控集成

  1. 错误监控上报:
service.interceptors.response.use(undefined, error => { // 上报错误信息 if (window.trackJs) { trackJs.track(error) } return Promise.reject(error) })
  1. 性能数据采集:
export function trackAPIPerformance(name, duration) { if (window.analytics) { analytics.track('api_performance', { name, duration, timestamp: Date.now() }) } }

35. 代码分割优化

  1. 按需加载API模块:
const userModule = () => import('@/api/user') async function fetchUser() { const { getUserList } = await userModule() return getUserList() }
  1. 组件懒加载:
<template> <Suspense> <template #default> <AsyncUserList /> </template> <template #fallback> <Loading /> </template> </Suspense> </template> <script setup> const AsyncUserList = defineAsyncComponent(() => import('./UserList.vue') ) </script>

36. 测试数据生成

  1. 使用Mock.js生成测试数据:
import Mock from 'mockjs' export function generateMockUsers(count = 10) { return Mock.mock({ [`list|${count}`]: [{ 'id|+1': 1, name: '@cname', email: '@email', 'age|18-60': 1, 'gender|1': ['男', '女'] }] }).list }
  1. 在测试中使用:
test('should display user list', () => { const users = generateMockUsers(5) render(UserList, { props: { initialUsers: users } }) expect(screen.getAllByRole('listitem')).toHaveLength(5) })

37. 数据转换管道

  1. 创建转换工具函数:
export function transformUserData(users) { return users.map(user => ({ ...user, fullName: `${user.lastName} ${user.firstName}`, isAdult: user.age >= 18 })) }
  1. 在组件中使用:
const processedUsers = computed(() => transformUserData(rawUsers.value) )

38. 多标签页同步

  1. 使用BroadcastChannel同步数据:
const channel = new BroadcastChannel('user-updates') // 发送更新 function sendUpdate(data) { channel.postMessage({ type: 'user_update', data }) } // 接收更新 channel.onmessage = (event) => { if (event.data.type === 'user_update') { // 更新本地数据 } }
  1. 在接口请求后广播:
const res = await updateUser(data) sendUpdate(res.data)

39. 离线优先策略

  1. 实现离线缓存:
async function getUsersWithCache() { if (!navigator.onLine) { const cached = localStorage.getItem('userList') if (cached) return JSON.parse(cached) throw new Error('离线且无缓存数据') } try { const res = await getUserList() localStorage.setItem('userList', JSON.stringify(res.data)) return res.data } catch (err) { const cached = localStorage.getItem('userList') if (cached) return JSON.parse(cached) throw err } }
  1. 监听网络状态:
window.addEventListener('online', () => { fetchData() })

40. 数据压缩传输

  1. 请求压缩支持:
service.interceptors.request.use(config => { if (config.data && config.compress) { config.data = pako.deflate(JSON.stringify(config.data)) config.headers['Content-Encoding'] = 'deflate' } return config })
  1. 响应解压处理:
service.interceptors.response.use(response => { if (response.headers['content-encoding'] === 'deflate') { response.data = JSON.parse(pako.inflate(response.data, { to: 'string' })) } return response })

41. 地理位置集成

  1. 获取用户位置:
export function getUserLocation() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('Geolocation not supported')) } navigator.geolocation.getCurrentPosition( position => resolve({ lat: position.coords.latitude, lng: position.coords.longitude }), error => reject(error) ) }) }
  1. 结合API使用:
async function fetchNearbyUsers() { try { const location = await getUserLocation() const res = await getUsersByLocation(location) return res.data } catch (error) { console.error('获取附近用户失败:', error) return [] } }

42. 图片懒加载

  1. 实现图片懒加载:
<template> <img v-for="user in userList" :key="user.id" :data-src="user.avatar" class="lazy-image" alt="User Avatar" > </template> <script setup> import { useIntersectionObserver } from '@vueuse/core' const images = ref([]) onMounted(() => { images.value = Array.from(document.querySelectorAll('.lazy-image')) images.value.forEach(img => { useIntersectionObserver(img, ([{ isIntersecting }]) => { if (isIntersecting) { img.src = img.dataset.src } }) }) }) </script>
  1. 占位图处理:
.lazy-image { background: #f0f0f0 url('placeholder.jpg') no-repeat center; min-height: 200px; }

43. 数据导出功能

  1. 导出为CSV:
export function exportToCSV(data, filename = 'export.csv') { const csvContent = [ Object.keys(data[0]).join(','), ...data.map(item => Object.values(item).join(',')) ].join('\n') const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename link.click() }
  1. 在组件中使用:
const handleExport = () => { exportToCSV(userList.value, 'users.csv') }

44. 剪贴板集成

  1. 复制数据到剪贴板:
export function copyToClipboard(text) { return navigator.clipboard.writeText(text) } // 使用示例 copyToClipboard(JSON.stringify(userList.value, null, 2)) .then(() => console.log('复制成功')) .catch(err => console.error('复制失败:', err))
  1. 从剪贴板读取:
async function pasteFromClipboard() { try { const text = await navigator.clipboard.readText() return JSON.parse(text) } catch (err) { console.error('粘贴失败:', err) return null } }

45. 二维码集成

  1. 生成用户二维码:
import QRCode from 'qrcode' export function generateQRCode(text, options = {}) { return new Promise((resolve, reject) => { QRCode.toDataURL(text, options, (err, url) => { if (err) return reject(err) resolve(url) }) }) } // 使用示例 const qrCodeUrl = await generateQRCode(`user:${userId}`)
  1. 在组件中显示:
<template> <img :src="qrCodeUrl" alt="User QR Code"> </template> <script setup> const props = defineProps(['userId']) const qrCodeUrl = ref('') onMounted(async () => { qrCodeUrl.value = await generateQRCode(`user:${props.userId}`) }) </script>

46. 打印功能实现

  1. 打印用户列表:
export function printUserList(users) { const printWindow = window.open('', '_blank') printWindow.document.write(` <html> <head> <title>用户列表</title> <style> table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #000; padding: 8px; } </style> </head> <body> <h1>用户列表</h1> <table> <thead> <tr> <th>ID</th> <th>姓名</th> <th>邮箱</th> </tr> </thead> <tbody> ${users.map(user => ` <tr> <td>${user.id}</td> <td>${user.name}</td> <td>${user.email}</td> </tr> `).join('')} </tbody> </table> </body> </html> `) printWindow.document.close() printWindow.print() }
  1. 打印样式优化:
@media print { .no-print { display: none !important; } body { font-size: 12pt; color: #000; } }

47. 数据对比功能

  1. 实现数据差异比较:
export function compareData(oldData, newData) { const changes = {} Object.keys(newData).forEach(key => { if (JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])) { changes[key] = { old: oldData[key], new: newData[key] } } }) return changes }
  1. 在组件中使用:
const changes = computed(() => compareData(originalUser.value, editedUser.value) )

48. 数据版本控制

  1. 实现简单版本历史:
const userHistory = ref([]) function saveVersion(user) { userHistory.value.push({ timestamp: new Date(), data: JSON.parse(JSON.stringify(user)) }) } function restoreVersion(index) { return userHistory.value[index].data }
  1. 限制历史记录数量:
function saveVersion(user) {

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询