在线音频分析平台(前端设计)

源码展示于 github: 前端项目:
https://github.com/Zranshi/Audio-analysis-web-client
后端项目:
https://github.com/Zranshi/Audio-analysis-web-server

本章节我们主要探讨本项目的前端设计的部分.

由于我们需求的功能大致上是 上传->处理->展示 的流程, 所以需要我们完成: 上传文件、接收数据、展示处理后的信息, 这三个方面.

文件上传

文件上传的大部分操作都在 <el-upload> 有响应的参数可以配置, 在此只讲解需要自己编写的钩子函数. 通过官方文档我们可以看到, <el-upload> 有如下几个钩子函数:
upload 钩子函数

我仅使用了 before-upload、on-success 和 on-remove .

其中, before-upload 用于在上传之前进行判断, 在满足所有条件以后, 返回 true 则可以使上传操作正常进行, 如果为 false 则上传操作终止. 在此我们分别判断文件是否为 wav 类型, 是否小于 30MB , 上传列表个数是否 小于 1 . 如果不满足条件则会弹出错误窗口提醒用户.

每次上传成功后, 会触发 on-success 钩子, 因此我们可以在这个函数中将上传列表中的文件放入上传字典中. 之所以不把这个操作放在 before-upload 中进行, 是因为如果运行页面时服务器没有开启, 文件也将被放入上传字典中, 并且无法在页面上显示删除操作, 从而造成 bug, 但如果是将静态文件放入服务器中, 将没有影响.

在此之外, 为了保存每次上传的列表, 我使用了一个字典来保存每次上传文件的名称, 如果名称相同则会覆盖已有的文件. 如果不同, 则会增加新的键值对. 并且使用 Object.keys(this.fileDict).length 来获取字典的长度,并加以判断. 之所以选择用字典来保存上传文件的列表, 是因为我发现如果使用列表保存, 会使得删除时需要遍历一般整个列表才可以完成操作, 而字典则有很好的时间复杂度.

on-remove 是为了在删除上传列表的某个文件时, 能够同步地删除字典中的键值对.

methods: {
  before_upload(file) {
    const isMav = file.type === 'audio/x-wav' || file.type === 'audio/wav';
    const isLt30M = file.size / 1024 / 1024 < 30;
    const isNum = Object.keys(this.fileDict).length < 1;
    if (!isMav) {
      this.$message.error('上传文件只能是wav音频文件格式!');
    } else if (!isLt30M) {
      this.$message.error('最大上传30MB大小的文件!');
    } else if (!isNum) {
      this.$message.error(`最多上传 1 个文件`);
    }
    return isMav && isLt30M && isNum
  },
  handle_remove(file) {
    delete this.fileDict[`${file.name}`]
  },
  handle_success(response, file) {
    this.fileDict[`${file.name}`] = file.name;
  },
},

数据传输

前端与后端进行数据传输一般使用 axios 封装和拦截请求请求. 虽然我被没有需要设置的地方, 但为了使代码看得更加简洁, 我还是进行了基础的封装. 其中 axios.js 封装和劫持了 http 数据, data.js 用来封装 api 接口.

为了封装 http 请求, 我们创建一个 HttpRequest 类来封装. 其中固定不变的参数写在构造函数中, 需要进行配置的参数可以在 getInsideConfig 中进行项目级配置. 对于每个不同的请求, 通过形参 options 来进行设置.对于所有的配置, 我使用 Object.assign() 函数来将配置结合起来. ( Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。 )

对于每个请求, 我们都可以对其在 interceptors 函数中进行拦截. 但由于本项目没有过多的需求, 细节部分请自行设置.

由于我们总是要返回一个 axios 对象, 我们可以将该对象通过 request 函数生成, 并结合来所有配置的参数以及拦截. 最后返回 axios 对象.

class HttpRequest {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.queue = {};
  }
  getInsideConfig() {
    return {
      baseUrl: this.baseUrl,
      header: {
        // 请求头配置
      },
    };
  }
  interceptors(instance) {
    instance.interceptors.request.use(config => {
      // 处理config
      return config;
    });
    instance.interceptors.response.use(
      res => {
        // 处理响应
        return res;
      },
      error => {
        // 请求出问题了,处理问题
        console.log(error);
        return { error: '网络出错了' };
      }
    );
  }
  request(options) {
    // 创造实例对象
    const instance = axios.create();
    options = Object.assign(this.getInsideConfig(), options);
    this.interceptors(instance, options.url);
    return instance(options);
  }
}

由于本项目现阶段只需要编写连个 api 接口, 一个是上传文件的 get_wav , 这个接口已经被封装在<el-upload> 中. 所以我只需要在 data.js 中封装获取数据的 show_data.

export const show_data = () => {
  return axios.request({
    url: `${axios.baseUrl}/show_data/`,
    method: 'get',
  });
};

接下来就可以在页面中调用了. 由于数据量可能过大, 为了防止点击按钮后页面停滞不动, 我采用了异步调用的方式.

async get_data() {
  let res = await show_data()
  res = res['data']
  // wav音频文件信息部分
  this.audio_parmas = res['audio_params'];
  // 波形图部分
  this.wave_data_l = res['data'][0];
  this.wave_data_r = res['data'][1];
  this.x = res['time'];
  // 频谱图部分
  this.xfp = res['xfp'];
  this.freq = res['freq'];
},

展示数据

关于数据展示, 我觉得有困难的无非就是两点. 一是数据的传输, 包括从后端将处理的数据传输到前端, 和将数据从父组件中传入到子组件; 二是实现图表的动态绑定, 能够在传入数据时修改图表并能够自适应窗口的改变.

由于前后端之间的数据传输已经在上节讲述过了, 这里我们探讨父组件向子组件的传值. 父组件注册子组件后, 可以在子组件标签中判定自定义属性:

<!--波形图,横轴为时间,纵轴为幅度-->
<waveform
  title="音频分析波形图"
  :data_l="wave_data_l"
  :data_r="wave_data_r"
  :x="x"
  v-if="radio === '1'"
></waveform>
<!--频谱图,横轴为频率,纵轴为幅度-->
<spectrogram title="音频分析频谱图" :data="xfp" :x="freq" v-if="radio === '2'"></spectrogram>

以 v-bind 方式将值绑定到子组件. 然后子组件在 props 属性中接收

  props: {
    title: {type: String, default: '未赋值的标题'},
    x: {type: Array},
    data: {type: Array},
  },

由于主组件的数据修改无法影响到 echart 实例的数据, 因此实现动态数据绑定需要我们自己完成. 我们可以使用 watch 来监听数据的变化, 如果变化了, 就将 option 更新, 然后调用 echart.setOption 函数即可实现 echart 图表的更新了.

watch: {
  data: {
    handler() {
      this.option.series[0].data = this.data;
      this.option.xAxis.data = this.x;
      this.chart.setOption(this.option);
    },
  }
}

正是你花费在玫瑰上的时间才使得你的玫瑰花珍贵无比