3、数据模拟:Mock.js 数据模拟、接口封装(Axios)、Promise 与 async/await 异步处理

做可视化大屏,最头疼的是什么?

我个人觉得,不是图表画不出来,而是——数据呢?

后端接口还没写好,前端怎么开发?总不能干等着吧。所以这一章,我们就来解决这个核心问题:在没有真实接口的情况下,如何让前端先跑起来

3.1 为什么需要 Mock 数据?

我在项目中遇到过好几次这种情况:

产品说「大屏下周要演示」,后端说「接口还在联调」。你想想看,前端总不能对着空页面干瞪眼吧?

所以 Mock 数据就派上用场了。说白了,就是模拟后端返回的数据,让前端能独立开发、独立调试。

核心原则:Mock 数据的结构必须和真实接口完全一致。字段名、数据类型、嵌套层级,一个都不能差。否则上线前还得改一遍,那就白 Mock 了。

3.2 Mock.js 快速上手

Mock.js 是个好东西。它不仅能生成随机数据,还能拦截 Ajax 请求。我习惯把它用在开发环境,等后端接口就绪后,一键切换。

安装与引入

npm install mockjs --save-dev

然后在项目中创建一个 mock 文件夹,专门放模拟数据。

基本用法

// mock/data.js
import Mock from 'mockjs'

const data = Mock.mock({
  'list|10': [{
    'id|+1': 1,
    'name': '@cname',
    'age|18-60': 1,
    'city': '@city',
    'salary|5000-20000': 1
  }]
})

export default data

这里有几个常用的占位符:

占位符 说明 示例结果
@cname 随机中文姓名 张三
@city 随机城市 北京
@date 随机日期 2024-03-15
@integer(1, 100) 随机整数 42
@float(0, 100, 2, 2) 随机浮点数 67.89
小技巧:Mock.js 支持正则表达式。比如 /[A-Z][a-z]{5}/ 可以生成随机英文单词。我在模拟设备编号时经常用这个。

拦截接口请求

这才是 Mock.js 的真正威力——它可以直接拦截 Axios 发起的请求。

// mock/index.js
import Mock from 'mockjs'

Mock.mock('/api/user/list', 'get', {
  code: 200,
  message: 'success',
  'data|20': [{
    'id|+1': 1,
    'name': '@cname',
    'age|20-50': 1,
    'department': '@ctitle(2,4)'
  }]
})

这样,当 Axios 请求 /api/user/list 时,Mock.js 会自动拦截并返回模拟数据。前端完全感知不到区别。

注意:Mock.js 拦截的是 XMLHttpRequest 对象。如果你用 fetch 发请求,Mock.js 是拦不住的。这时候可以用 MSW(Mock Service Worker)替代。

3.3 Axios 接口封装

直接在每个组件里写 Axios 请求?那可不行。我见过太多项目,接口散落在各个组件里,改个 baseURL 要改几十个文件。

所以,统一封装是必须的

创建 Axios 实例

// 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;charset=utf-8'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 可以在这里加 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      // 统一错误处理
      console.error('接口异常:', res.message)
      return Promise.reject(new Error(res.message))
    }
    return res
  },
  error => {
    console.error('网络错误:', error.message)
    return Promise.reject(error)
  }
)

export default service
为什么要封装?三个理由:统一处理 token、统一处理错误、统一转换数据格式。你想想看,如果每个组件都写一遍错误处理,那得多累。

封装 API 模块

// api/user.js
import request from '@/utils/request'

export function getUserList(params) {
  return request({
    url: '/api/user/list',
    method: 'get',
    params
  })
}

export function addUser(data) {
  return request({
    url: '/api/user/add',
    method: 'post',
    data
  })
}

export function deleteUser(id) {
  return request({
    url: `/api/user/delete/${id}`,
    method: 'delete'
  })
}

这样做的好处是:接口变更时,只需要改一个文件。我在一个老项目里接手时,发现接口散落在 30 多个组件里,改起来简直想哭。后来花了一周全部重构,从此清爽多了。

3.4 Promise 与 async/await

说到异步处理,就绕不开 Promise。但说实话,Promise 的链式调用写多了,回调地狱是没了,但「链式地狱」又来了。

Promise 基础

Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变,就不会再变。

// 基本用法
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const data = { name: '张三' }
    resolve(data)
    // 如果出错:reject(new Error('出错了'))
  }, 1000)
})

promise.then(data => {
  console.log(data) // { name: '张三' }
}).catch(err => {
  console.error(err)
})

async/await 语法糖

我个人强烈推荐用 async/await。它让异步代码看起来像同步代码,可读性高太多了。

// 用 async/await 改写
async function fetchUserData() {
  try {
    const response = await getUserList({ page: 1, size: 10 })
    console.log('用户数据:', response.data)
    return response.data
  } catch (error) {
    console.error('获取用户数据失败:', error)
    throw error
  }
}
避坑指南:我曾经犯过一个错误——在 forEach 里用 await。结果发现 forEach 不等待异步操作完成。正确的做法是用 for...of 循环,或者 Promise.all。

实际场景:大屏数据加载

大屏项目通常需要同时加载多个接口。比如同时获取用户数据、销售数据、地图数据。这时候用 Promise.all 最合适。

async function loadDashboardData() {
  try {
    const [userData, salesData, mapData] = await Promise.all([
      getUserList({ page: 1, size: 100 }),
      getSalesData({ date: '2024-03-15' }),
      getMapData({ region: '全国' })
    ])

    return {
      users: userData.data,
      sales: salesData.data,
      map: mapData.data
    }
  } catch (error) {
    console.error('大屏数据加载失败:', error)
    // 可以在这里显示错误提示
    showErrorToast('数据加载异常,请稍后重试')
  }
}

为什么要用 Promise.all?因为这三个接口互不依赖,可以并行请求。如果串行请求,总耗时就是三个接口耗时之和;并行请求,耗时只等于最慢的那个接口。

性能优化小贴士:大屏项目里,数据加载速度直接影响用户体验。能用 Promise.all 就别用串行。我见过一个项目,三个接口串行加载,首屏等了 6 秒。改成并行后,直接降到 2 秒。

3.5 完整流程串联

好了,我们把 Mock、Axios、async/await 串起来,看看完整的开发流程:

  1. 定义接口文档:和后端约定好接口地址、请求方式、返回数据结构
  2. 编写 Mock 数据:按照接口文档,用 Mock.js 生成模拟数据
  3. 封装 Axios:创建请求实例,配置拦截器
  4. 编写 API 模块:每个接口对应一个函数,返回 Promise
  5. 在组件中调用:用 async/await 获取数据,渲染到图表
  6. 联调切换:后端接口就绪后,关闭 Mock,直接请求真实接口

这个流程我在多个大屏项目里验证过,非常稳定。你想想看,前端不用等后端,后端不用催前端,各干各的,效率自然就上来了。

最后提醒:上线前一定要确认 Mock 已经关闭。我见过有人把 Mock 代码带到生产环境,结果用户看到的数据全是随机生成的假数据。嗯,那场面,相当尴尬。

下一章,我们会把这些数据真正渲染到图表上。准备好了吗?