扫码出入库与web worker

3,063 阅读8分钟

我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了

大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(扫码枪相当于一个键盘),在一个一直聚焦的输入框输入扫到的条码,然后我这边监听到条码调接口录入库,成功后再语音播报扫码结果,同时刷新结果,刷新统计信息。

听上去很简单是不是,想象是美好的,可现实就残酷了,在初始版的时候,功能是做出来了,本地出入库都没问题,但是发到生产就悲催了,乱七八糟的问题

比如

  • 1.扫码枪精度的问题,扫码识别率低下,扫10次才能正确识别1次
  • 2.扫出来的码,断码,原本以为扫码枪扫一次就等同于我复制一个条码进输入框,可结果是扫码枪一次扫入,输入框接受的条码就像一个字符串流一样,一个字符一个字符进入的,这就导致中间间隔稍微长一点,就被错误的识别为另外一个条码(扫码是多个码连续扫入的)
  • 3.语音播报延迟,经常会有语音播放不出或者播放一半,这个..

这个就很让人无语,明明本地啥问题也没有

第一个问题,扫码枪精度,确实是有,因为我做的时候拿的扫码枪是一个有线的扫码枪,那识别率才叫一个高,准确率差不多95%,几乎没遇到解码啥的问题,可换成无线的扫码枪就傻眼,第一个问题就很烦,想到几千个客户没办法统一更换扫码枪,于是就想想优化一下条码编码呢, 我这边条码是用的jsBarcode组件,默认的编码类型CODE128,嗯~~问题会不会出在条码规范上呢

我去查了一下,条码的编码规范大致有以下几种

条码类型类别描述常见应用编码长度
UPC-A1D通用产品代码,常见于零售业零售商品12位数字
UPC-E1DUPC-A的压缩版本小型零售商品6位数字
EAN-131D欧洲商品编号,国际通用图书、零售商品13位数字
EAN-81DEAN-13的压缩版本小型商品8位数字
Code 391D可变长度,包含字母、数字和特殊字符工业、政府可变长度
Code 1281D高密度条码,表示所有128个ASCII字符物流、运输可变长度
Interleaved 2 of 5 (ITF)1D数字条码,每两个数字组成一对交错编码分销、仓储偶数位数字
QR Code2D可存储大量数据,包括文字、数字、二进制数据和汉字支付、信息分享、广告可变长度
Data Matrix2D高密度编码,适用于小型物品标识电子元器件、医疗设备可变长度
PDF4172D可编码大量数据身份证件、运输标签可变长度
Aztec Code2D高容错性,适用于票务和登机牌票务、登机牌可变长度

我这里着重说说CODE39和CODE128;我发现CODE39生成的条码比CODE128生成的长很多,我这把无线扫码枪扫很久都扫不出来,识别超慢,这个很奇怪,之前客户C端系统找技术查了一下,编码规范是CODE39,我就懵逼了,都是CODE39,为啥我们生成的码就识别这么慢,捣鼓了很久也没个结果,如果有哪位知道的可以给我说一下,就索性放弃这种编码模式,改用CODE128吧,查了一下,这是一种效率更高的编码方式,CODE39条码较长的主要原因在于它的编码效率较低,每个字符占用的空间较大,而CODE128通过更加紧凑和高效的编码方式,能够在同样的内容下生成更短的条码,于是撺掇同事把所有的条码都用CODE128生成,至此,扫码枪识别效率低的问题算事过去了

然后就是第二点,扫出来的码,断码问题,这个也因为换了短码好那么一点,可扫出来也经常有解码内容变长,的问题,暂时还在想办法优化

最后就是语音播报延迟,卡壳,甚至没有语音的情况,这个问题比较恼火,我这边组件是使用的开源库howler.js,这个库的优点就是兼容性好,可以播放包括mp3, opus, ogg, wav, aac, m4a, m4b, mp4, webm, 等多种格式,而且还支持分轨sprite播放,这个是我的最初的代码

import config from "./config";
import "./lib/howler.min";
const ENV = import.meta.env;

class VoiceReport {
  public list = [];
  constructor() {
    this.initVoice();
  }
  // 目录放在@/assets/voice/ 下面
  public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
  public voiceNameList = Object.keys(this.voiceList);
  // 初始化语音播报器列表
  initVoice = () => {
    config.forEach((v) => {
      const item = {
        name: `Ref${v.codeType}${v.codeKey}`,
        code: v.codeKey,
        codeName: v.codeName,
        voice: "",
      };
      const voiceIndex = this.voiceNameList.findIndex((voice) =>
        String(voice).includes(v.codeKey)
      );
      if (voiceIndex > -1) {
        item.path = this.voiceNameList[voiceIndex];
        item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
      }
      this.list.push(item);
    });
  };
  // 播放
  play = (code: string) => {
    const Stream = this.list.find((v) => v.code == code);
    let StreamVoide = null;
    if (ENV?.DEV) {
      StreamVoide = Stream?.path;
    } else {
      StreamVoide = Stream?.voice?.default;
    }
    // 提供的条码不在列表中
    if (!StreamVoide) return;
    try {
      const sound = new Howl({
        src: [StreamVoide],
        volume: 1.0,
        html5: true,
        onplayerror: (e) => {
          console.log("error", e);
        },
      });
      sound.play();
    } catch (e) {
      console.log(e);
    }
  };
}

export default VoiceReport;

这个倒是能放,可能不能优化呢

我首先想到的是就从播放器本身优化呢,我想着会不会是加载的延迟或者加载文件过多,想着将所有的文件进行合并,再生成sprite信息,弄是弄了,可是不论如何就是load报错,我再把这个多个mp3合并成一个文件@/assets/voice/fullStack.mp3,进行生成sprite,来加载,加载是加载上来了,可同样遇到播放错误,播放的track根本不是我期望的那个

这个是错误代码:


import config from "./config";
import "./lib/howler.min";
import fullVoice from "@/assets/voice/fullStack.mp3";
const ENV = import.meta.env;

class player {
  public list: any = [];
  public player: any = {};
  constructor() {
    this.initVoice();
  }
  // 目录放在@/assets/voice/ 下面
  public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
  public fullVoice: any = fullVoice;
  public voiceNameList = Object.keys(this.voiceList);
  public sprite: any = {};
  public streamVoide: any = [];
  // 时间戳转换为秒
timeStringToSeconds = (timeStr: string) => {
  const parts = timeStr.split(":");
  const hours = parseInt(parts[0]);
  const minutes = parseInt(parts[1]);
  const seconds = parseInt(parts[2]);

  return hours * 3600 + minutes * 60 + seconds;
}
  // 初始化语音播报器列表
  initVoice = () => {
    config.forEach((v, index) => {
      const item = {
        name: `Ref${v.codeType}${v.codeKey}`,
        code: v.codeKey,
        codeName: v.codeName,
        voice: {},
        path: "",
        duration: this.timeStringToSeconds(v.duration ?? 0) * 1000,
        durationStart: 0,
        durationEnd: 0,
      };
      item.durationStart = !index ? 0 : this.list[index - 1].durationEnd;
      item.durationEnd = item.durationStart + item.duration;
      this.sprite[v.codeKey] = [item.durationStart, item.durationEnd];
      const voiceIndex = this.voiceNameList.findIndex((voice) =>
        String(voice).includes(v.codeKey)
      );
      if (voiceIndex > -1) {
        item.path = this.voiceNameList[voiceIndex];
        item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
      }
      this.list.push(item);
      /* eslint-disable */
      // @ts-ignore
      this.streamVoide.push(ENV?.DEV ? item.path : item.voice?.default);
    });
    /* eslint-disable */
    // @ts-ignore
      this.player = new Howl({
        src: this.streamVoide,
        volume: 1.0,
        html5: true,
        sprite: this.sprite,
        onplayerror: (e: any) => {
          console.log("play error", e);
        },
        onload: (e: any) => {
          console.log("error", e);
        }
      });
      window.player = this.player;
      console.log(this.sprite, this.player, fullVoice)
  };
  // 播放
  play = (code: string) => {
    try {
      this.player.play(code);
    } catch (e) {
      console.log(e);
    }
  };
}

export default player;

到现在还在持续找解决方案中, 最后,不得不把希望寄托在异步任务请求导致阻塞主线程这个猜想上,因为每完成一次扫码,会发起三个请求

  • 入库请求
  • 刷新结果列表请求
  • 刷新统计请求

这么多请求一起,接口稍微一慢就有可能导致播放卡顿的问题 这个在我经过一段时间的搜索之后发现,发现webworker可以处理这个问题

web worker
根据MDN的说法
Web Workers 是 Web 内容在后台线程中运行脚本的一种简单方法。工作线程可以在不干扰用户界面的情况下执行任务。此外,他们还可以使用 fetch() 或 XMLHttpRequest API 发出网络请求。创建后,工作人员可以通过将消息发布到该代码指定的事件处理程序来向创建它的 JavaScript 代码发送消息(反之亦然)。

既然是独立于主线程之外的一个,那就不可避免的会遇到身份验证和通信的问题,对于发起的请求没有携带身份信息,这个好办,就自己在封装一个axios方法fetch,将身份信息传过去ok,这里主要贴一下worker的内容,也很简单

import type { WorkerMessageDataType } from "../types/types";
import fetch from "@/utils/fetch";
import { throttle } from "lodash";
let Ajax: any = null;

// 从主线程接受数据
self.onmessage = function (e: WorkerMessageDataType) {
  console.log("Worker: 收到请求", e);
  const type = e.data?.type || "";
  const data = e.data?.data || {};
  // 一定要初始化
  if (type == "init") {
    const headers: any = e.data?.headers;
    Ajax = fetch(headers);
  }
  // 请求刷新统计数据
  if (type == "refreshScanCountData") refreshScanCountData();
  // 请求刷新列表扫码结果
  if (type == "refreshDataList") refreshDataList();
  // 请求入库
  if (type == "checkAddIntoStock") checkAddIntoStock(data);
};

// 向主线程发送数据
const sedData = (type: string, data: object) => {
  const param = {
    type,
    data: data || {},
  };
  self.postMessage(param);
};

// 刷新统计数据,查询统计信息api
const refreshScanCountData = throttle(() => {
  Ajax({
    method: "post",
    url: `/api/CountStatistics`,
    data: {},
  }).then((res: any) => {
    sedData("refreshScanCountData", res);
  });
}, 500);

// 刷新扫码结果数据
const refreshDataList = throttle(() => {
  Ajax({
    method: "post",
    url: `/api/scanToStorage/page`,
    data: {},
  }).then((res: any) => {
    sedData("refreshDataList", res);
  });
}, 500);

// 请求入库
const checkAddIntoStock = (data: { barcode: string; [x: string]: any }) => {
  Ajax({
    method: "post",
    url: `/api/scanToStorage`,
    data,
  })
    .then((res: any) => {
      // 刷新统计数据
      refreshScanCountData();
      // 刷新列表
      refreshDataList();
      sedData("checkAddIntoStock", {
        barcode: data.barcode,
        ...res,
        status: true,
      });
    })
    .catch(() => {
      sedData("checkAddIntoStock", {
        barcode: data.barcode,
        status: false,
      });
    });
};

在主线程页面写一个方法,初始化一下这个worker

// 加载worker
const initWorker = () => {
  const headers = {
    Authorization: "bearer " + sessionStorage.getItem("token"),
    token: sessionStorage.getItem("token"),
    currRoleId: sessionStorage.getItem("roleId"),
  };
  // 初始化,加入身份信息
  WebWorker.postMessage({ type: "init", headers });
  // 从worker接受消息
  WebWorker.onmessage = (e) => {
    console.log("Main script: Received result", e.data);
    const type = e.data?.type || "";
    const data = e.data?.data || {};

    // 异步更新统计信息
    if (type == "refreshScanCountData") {
      ScanCountData.value = data;
    }
    // 刷新表格数据
    if (type == "refreshDataList") {
      dataTable.value.updateData(data);
    }
  };
};

这样就可以了,即便是这样,依然还有好多问题没解决,这个是我的第一篇文章,难免有错误疏漏,这个需求并没结束,我还会持续跟进更新的