Bootstrap

electron-vue 台称串口对接 SerialPort

大致流程

1.首先找一个串口工具(sscom5.12.1)试试读取串口是否成功连上;

2.创建electron-vue的项目

3.安装依赖,调整版本,启动项目;(在electron中使用串口_electron 串口_Jack_Kee的博客-CSDN博客

4.学习SerialPort Usage | Node SerialPort工具;

5.开发入口文件,测试是否成功获取串口信息

6.成功获取串口后,接收台称数据

7.根据台称说明书与协议,解析台称数据(数值,单位,正负,精度...)

8.完成称重需求

9.完成打印需求

10.完成自动更新

11.防止开启多个应用

1.串口工具大概样子

通过切换端口号和波特率,可以查看哪个是台称串口及波特率,我本机的端口号COM3 波特率9600是台秤的串口信息。

2.创建electron项目(起步 · electron-vue

跟着官方步骤 创建electron-vue项目,成功启动项目(启动,打包)

 "electron": "^17.4.11",

3.引入serialport
npm install serialport --save

然后,安装electron-rebuild(用来重新编译serialport包):

npm install --save-dev electron-rebuild

重新编译serialport包

.\node_modules\.bin\electron-rebuild.cmd
 

提示Rebuild Complete,表示编译完成

4.按照该文章,实现最终效果注意安装依赖版本,

此注明我项目使用的各个版本

"dependencies": {
		"@serialport/parser-byte-length": "^11.0.0",
		"axios": "^0.18.0",
		"electron-updater": "^5.3.0",
		"element-ui": "^2.8.2",
		"lodash": "^4.17.21",
		"multispinner": "^0.2.1",
		"qrcodejs2": "^0.0.2",
		"serialport": "^11.0.0",
		"vue": "^2.5.16",
		"vue-electron": "^1.0.6",
		"vue-router": "^3.0.1",
		"vuex": "^3.0.1",
		"vuex-electron": "^1.0.0",
		"vuex-persistedstate": "^4.1.0"
	},
	"devDependencies": {
		"ajv": "^6.5.0",
		"babel-core": "^6.26.3",
		"babel-loader": "^7.1.4",
		"babel-minify-webpack-plugin": "^0.3.1",
		"babel-plugin-component": "^1.1.1",
		"babel-plugin-transform-runtime": "^6.23.0",
		"babel-preset-env": "^1.7.0",
		"babel-preset-stage-0": "^6.24.1",
		"babel-register": "^6.26.0",
		"cfonts": "^2.1.2",
		"chalk": "^2.4.1",
		"copy-webpack-plugin": "^4.5.1",
		"cross-env": "^5.1.6",
		"css-loader": "^0.28.11",
		"del": "^3.0.0",
		"devtron": "^1.4.0",
		"electron": "^17.4.11",
		"electron-builder": "^23.6.0",
		"electron-debug": "^1.5.0",
		"electron-devtools-installer": "^2.2.4",
		"electron-rebuild": "^3.2.9",
		"file-loader": "^1.1.11",
		"html-webpack-plugin": "^3.2.0",
		"listr": "^0.14.3",
		"mini-css-extract-plugin": "0.4.0",
		"node-loader": "^0.6.0",
		"node-sass": "^4.9.2",
		"sass-loader": "^7.0.3",
		"style-loader": "^0.21.0",
		"url-loader": "^1.0.1",
		"vue-html-loader": "^1.2.4",
		"vue-loader": "^15.2.4",
		"vue-style-loader": "^4.1.0",
		"vue-template-compiler": "^2.5.16",
		"webpack": "^4.15.1",
		"webpack-cli": "^3.0.8",
		"webpack-dev-server": "^3.1.4",
		"webpack-hot-middleware": "^2.22.2",
		"webpack-merge": "^4.1.3"
	}
 
4. SerialPort (注意不同版本使用方式不同)

serialport:10.x.x

import { SerialPort } from 'serialport'

serialport:7.x.x

const SerialPort = require('serialport')

项目操作案例:

const { ByteLengthParser } = require("@serialport/parser-byte-length");
let ports = []; // 串口list
let mainWindow = null;  // electron 窗口

function registerIpcEvent() {
  // 发送可选串口
  SerialPort.list().then((ports) => {
    setTimeout(() => {
      mainWindow.webContents.send("send-port-info", ports);
    }, 1000);
  });
  //  关闭窗口
  app.on("window-all-closed", function () {
    if (process.platform !== "darwin") {
      ports.forEach((e) => {
        e && e.close();
      });
      app.quit();
    }
  });
  ipcMain.on("closeApp", () => {
    ports.forEach((e) => {
      e && e.close();
    });
    app.quit();
  });

  // 重新初始化串口
  ipcMain.on("init-port", (event, args) => {
    initPort(args);
  });
  // 关闭串口
  ipcMain.on("close-serialport", (event, args) => {
    const currentPort = ports.find((i) => i.path === args.name);
    currentPort && currentPort.close();
    if (currentPort && !currentPort.isOpen) {
      event.reply("close-serialport", { name: args.name });
    }
    ports = ports.filter((e) => e !== currentPort);
  });

  // 打开某个串口
  ipcMain.on("open-serialport", (event, args) => {
    const serialport = new SerialPort(
      {
        path: args.name,
        baudRate: +args.baudRate,
      },
      (err) => {
        console.log(err);
        if (err) {
           event.reply("open-serialport", {
             hasError: true,
             ...args,
             message: err,
           });
        } else {
          event.reply("open-serialport", args);
          ports.push(serialport);
        }
      }
    );
    // 根据说明书数据量字节17一次传输 也可以通过其他方式截取
    const parser = serialport.pipe(new ByteLengthParser({ length: 17  }));
    parser.on("data", (data) => {
     // xxxxxxx 
    });
    serialport.on("open", () => {});
  });
}

function initPort(bool) {
  if (bool) {
    SerialPort.list().then((ports) => {
      setTimeout(() => {
        mainWindow.webContents.send("send-port-info", ports);
      }, 1000);
    });
  }
}

还可以通过下面的形式截断串口数据 

正则匹配:parser-regex  

 一种转换流,它使用正则表达式来拆分传入的文本。

要使用Regex解析器,请提供一个正则表达式来拆分传入的文本。数据作为可由编码选项控制的字符串发出(默认为utf8)

const { SerialPort } = require('serialport')
const { ReadlineParser } = require('@serialport/parser-readline')
const port = new SerialPort({ path: '/dev/ROBOT', baudRate: 14400 })

const parser = port.pipe(new ReadlineParser({ delimiter: '\r\n' }))
parser.on('data', console.log)

分隔符截取:parser-readline 

在接收到换行符后发出数据的转换流。若要使用Readline分析器,请提供分隔符(默认为\n)。数据以可由编码选项控制的字符串形式发出(默认为utf8)。

const { SerialPort } = require('serialport')
const { RegexParser } = require('@serialport/parser-regex')
const port = new SerialPort({ path: '/dev/ROBOT', baudRate: 14400 })

const parser = port.pipe(new RegexParser({ regex: /[\r\n]+/ }))
parser.on('data', console.log)
5.开发入口文件
const { app, BrowserWindow, ipcMain, Menu, dialog } = require("electron");
const { SerialPort } = require("serialport");
const _ = require("lodash");
const { ByteLengthParser } = require("@serialport/parser-byte-length");
import { updateHandle } from "../renderer/utils/update.js";

let ports = [];
const path = require("path");
const fs = require("fs");


//读取本地更新配置json文件 包含是否调试 请求地址 更新地址...
const File_path = path
  .join(path.resolve("./../"), "/config.json")
  .replace(/\\/g, "/");
const config_res = JSON.parse(fs.readFileSync(File_path, "utf-8"));
let mainWindow = null;
const winURL =
  process.env.NODE_ENV === "development"
    ? `http://localhost:9080`
    : `file://${__dirname}/index.html`;


app.whenReady().then(() => {
  createWindow();

  registerIpcEvent();
  // getPrinterList(); 注释打印
  config_res.IS_DEBUG && createMenu();
  app.on("activate", function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: config_res.IS_DEBUG, // 是否有边框窗口
    fullscreen: !config_res.IS_DEBUG, // 全屏
    webPreferences: {
      // preload: path.join(__dirname, "preload.js"),
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
      webviewTag: true,
      webSecurity: false,
    },
  });
  config_res.IS_DEBUG ? mainWindow.webContents.openDevTools() : ""; // 生产模式调试开关

  setTimeout(() => {
    mainWindow.loadURL(winURL);
  }, 1000);
  // 监听崩溃
  mainWindow.webContents.on("crashed", () => {
    const options = {
      type: "error",
      title: "系统意外终止",
      message: "可点击以下按钮",
      buttons: ["点击重启", "直接退出"],
    };
    dialog.showMessageBox(options, (index) => {
      if (index === 0) reloadWindow(mainWindow);
      else app.quit();
    });
  });

}

function createMenu() {
  // main
  const template = [
    {
      label: "调试",
      click: function (item, focusedWindow) {
        if (focusedWindow) {
          config_res.IS_DEBUG && focusedWindow.toggleDevTools();
        }
      },
    },

  ];
  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}
function initPort(bool) {
  if (bool) {
    SerialPort.list().then((ports) => {
      setTimeout(() => {
        mainWindow.webContents.send("send-port-info", ports);
      }, 1000);
    });
  }
}
 
function registerIpcEvent() {
  // 发送可选串口
  SerialPort.list().then((ports) => {
    setTimeout(() => {
      mainWindow.webContents.send("send-port-info", ports);
    }, 1000);
  });
  //  关闭窗口
  app.on("window-all-closed", function () {
    if (process.platform !== "darwin") {
      ports.forEach((e) => {
        e && e.close();
      });
      app.quit();
    }
  });
  ipcMain.on("closeApp", () => {
    ports.forEach((e) => {
      e && e.close();
    });
    app.quit();
  });

  // 重新初始化串口
  ipcMain.on("init-port", (event, args) => {
    initPort(args);
  });
  // 关闭串口
  ipcMain.on("close-serialport", (event, args) => {
    const currentPort = ports.find((i) => i.path === args.name);
    currentPort && currentPort.close();
    if (currentPort && !currentPort.isOpen) {
      event.reply("close-serialport", { name: args.name });
    }
    ports = ports.filter((e) => e !== currentPort);
  });

  // 打开某个串口
  ipcMain.on("open-serialport", (event, args) => {
    const serialport = new SerialPort(
      {
        path: args.name,
        baudRate: +args.baudRate,
      },
      (err) => {
        console.log(err);
        if (err) {
           event.reply("open-serialport", {
             hasError: true,
             ...args,
             message: err,
           });
        } else {
          event.reply("open-serialport", args);
          ports.push(serialport);
        }
      }
    );
    // 数据量字节截取拉大 获取大区间集合
    const parser = serialport.pipe(new ByteLengthParser({ length: 17 * 10 }));
    parser.on("data", (data) => {
      let dataString = data.toString("hex");
 
      sendData(event, {
       // xxxxx 需要发送给页面的信息
      });
    });
    serialport.on("open", () => {});
  });
}
function sendData(event, value) {
  event.sender.send("send-data", value);
}
 
 
6.需要展示串口信息的页面

1. 发送init-port 初始化请求

const { ipcRenderer } = require("electron");

ipcRenderer.send("init-port", true);

 2.页面接收解析后的串口信息函数

  data() {
    return {
      portNameList: [], // 多称选择
      openedPort: [], // 已经打开的称信息
      currPortData: 0.0, // 显示重量
      currName: "", // 称名称
      currRate: "9600", // 波特率
      sign: "", //秤 正 负
      rateOption: rateOption,// 波特率选择
    };
  },  
  watch: {
    currName(val, oldval) {
      if (val !== "" && oldval !== "") {
        this.openOrclose("open", oldval);
      }
    },
  },  
 methods: {
  listSerialPorts() {
      ipcRenderer.on("open-serialport", (event, args) => {
        console.log("open-serialport", args);
        if (args.hasError) {
          alert(args.message);
        } else {
          this.openedPort.push(args.name);
          console.log("open", this.openedPort);
        }
      });

      ipcRenderer.on("close-serialport", (event, args) => {
        const index = this.openedPort.findIndex((i) => i === args.name);
        this.openedPort.splice(index, 1);
        console.log("close", this.openedPort);
      });

      ipcRenderer.on("send-port-info", (event, args) => {
        for (let port of args) {
          this.portNameList.push({
            portName: `称台${this.portNameList.length + 1}`,
            name: port.path,
            path: port.path,
          });
        }

        this.currName = this.portNameList && this.portNameList[0].name;
        console.log("send-port-info获取的串口", this.portNameList);
        this.openOrclose("open");
      });

      ipcRenderer.on("send-data", (event, args) => {
        console.log("send-data", args, this.resData);
        this.currPortData = args.value; // 接收传入的信息 用于展示
       });
    },

    openOrclose(type, oldname = "") {
      if (this.portNameList.length <= 0) {
        this.$message.error("台称读取失败,请重新进入页面或重新进入系统!");
        return;
      }
      const name = this.currName;
      const baudRate = this.currRate;
      if (oldname !== "" && this.openedPort.includes(oldname)) {
        this.sendMessageToMain("close-serialport", { name: oldname });
      }
      if (type === "close") {
        if (this.openedPort.includes(name)) {
          this.sendMessageToMain("close-serialport", { name });
        }
      } else {
        this.sendMessageToMain("open-serialport", {
          name: this.currName,
          baudRate: this.currRate,
        });
      }
    },
}
7.根据台称说明书与协议,解析台称数据(数值,单位,正负,精度...)

先看说明书梅特勒托利多METTLER TOLEDO

  

 1.从说明书的是数据含有17/18字节 我通过走数据查到是17个字节

 const bufferArray = dataString.split("0d")[1]; 
// 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0d

 2.连续输出格式 5-10字节代表的称指示重量,11-16字节表示皮重重量

 

3.数据是没有小数点的要通过解析算出小数点的位置和正负号

4.状态A,状态B,状态C 需要解析分别表示一些内容

 状态A说明,main.js里面具体操作

      let dataString = data.toString("hex");
      const bufferArray = dataString.split("0d")[1]; 
      // 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0d
      const bufferfloat = bufferArray.slice(2, 4); 
      // 小数点位置 截取byte第二位 根据说明书转为二进制查看0,1,2位的值判断小数点位置
      let float = parseInt(bufferfloat, 16).toString(2).substr(-3); 
      //16进制转为2进制 截取后三位;

      let de = 1;

      if (float === "100") {
        de = 100;
      } else if (float === "101") {
        de = 1000;
      } else if (float === "110") {
        de = 10000;
      } else if (float === "111") {
        de = 100000;
      }
      
      var value = Number(weight) / de; 

  状态B说明,main.js里面具体操作

 

      let dataString = data.toString("hex");
      const bufferArray = dataString.split("0d")[1]; 
        // 02 34 30 20 20 20 20 32 30 34 20 20 20 30 30 30 0d
      const buffersign = bufferArray.slice(4, 6); 
        // 单位值位置 截取byte第2位 共2位
      let signCode = parseInt(buffersign, 16).toString(2).substr(-2, 1); 
        //16进制转为2进制 截取倒数第2位; 从倒数第2位开始截取1个长度的字符
        // signCode 0=正1=负

   状态C说明,main.js里面具体操作

 


      let dataString = data.toString("hex");
      const bufferArray = dataString.split("0d")[1]; 
        // 02 34 30 20 /20 20 20 32 30 34/ 20 20 20 30 30 30 0d
      const bufferunit = bufferArray.slice(6, 8); // 单位值位置 截取byte第3位 共2位
      let unitCode = parseInt(bufferunit, 16).toString(2).substr(-3);
      let unit = null;
      
if (unitCode === "000") {
        unit = "kg";
      } else {
        unit = "g";
      }

8.称重需求 main.js 文件处理
  // 打开某个串口
  ipcMain.on("open-serialport", (event, args) => {
    const serialport = new SerialPort(
      {
        path: args.name,
        baudRate: +args.baudRate,
      },
      (err) => {
        console.log(err);
        if (err) {
           event.reply("open-serialport", {
             hasError: true,
             ...args,
             message: err,
           });
        } else {
          event.reply("open-serialport", args);
          ports.push(serialport);
        }
      }
    );
    // 数据量字节17为一组 但发送太过频繁 所以增大10倍 减少页面渲染压力
    const parser = serialport.pipe(new ByteLengthParser({ length: 17 * 10 }));
    parser.on("data", (data) => {
      let dataString = data.toString("hex");
      const bufferArray = dataString.split("0d")[1]; 
        // 02 34 30 20 /20 20 20 32 30 34/ 20 20 20 30 30 30 0d
      const bufferfloat = bufferArray.slice(2, 4); 
        // 小数点位置 截取byte第二位 根据说明书转为二进制查看0,1,2位的值判断小数点位置
      const buffersign = bufferArray.slice(4, 6); 
        // 正负号位置 截取byte第2位 共2位
      const bufferunit = bufferArray.slice(6, 8); 
        // 单位值位置 截取byte第3位 共2位
      const bufferweight = bufferArray.slice(8, 20);
         // 重量值位置 截取byte第5-10位 共6位
      const bufferpeel = bufferArray.slice(20, 32); 
        // 皮重值位置 截取byte第11-16位 共6位

      let float = parseInt(bufferfloat, 16).toString(2).substr(-3); 
        //16进制转为2进制 截取后三位;
      let signCode = parseInt(buffersign, 16).toString(2).substr(-2, 1); 
        //16进制转为2进制 截取倒数第2位; 从倒数第2位开始截取1个长度的字符。
      let unitCode = parseInt(bufferunit, 16).toString(2).substr(-3);

      let weight = hexToString(bufferweight.toString("hex"));
      let peel = hexToString(bufferpeel.toString("hex"));

      let de = 1;
      let unit = null;
      let sign =  signCode;  // sign 0=正1=负
      if (float === "100") {
        de = 100;
      } else if (float === "101") {
        de = 1000;
      } else if (float === "110") {
        de = 10000;
      } else if (float === "111") {
        de = 100000;
      }
      
      if (unitCode === "000") {
        unit = "kg";
      } else {
        unit = "g";
      }
      var value = Number(weight) / de;
      console.log("weights",value,float,weight,unit,peel,bufferArray,dataString,sign);
      sendData(event, {
        value,
        float,
        weight,
        unit,
        de,
        peel,
        bufferArray,
        dataString,
        sign
      });
    });
    serialport.on("open", () => {});
  });

function sendData(event, value) {
  event.sender.send("send-data", value);
}

/**
 * 将16进制字符串进行分组,每两个一组
 * @param  {[String]} str [16进制字符串]
 * @return {[Array]}     [16进制数组]
 */
//处理中文乱码问题
function utf8to16(str) {
  var out, i, len, c;
  var char2, char3;
  out = "";
  len = str.length;
  i = 0;
  while (i < len) {
    c = str.charCodeAt(i++);
    switch (c >> 4) {
      case 0:
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:
      case 6:
      case 7:
        out += str.charAt(i - 1);
        break;
      case 12:
      case 13:
        char2 = str.charCodeAt(i++);
        out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
        break;
      case 14:
        char2 = str.charCodeAt(i++);
        char3 = str.charCodeAt(i++);
        out += String.fromCharCode(
          ((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
        );
        break;
    }
  }
  return out;
}

function hexToString(str) {
  // 30 空格 0d跨行
  var val = "",
    len = str.length / 2;
  for (var i = 0; i < len; i++) {
    val += String.fromCharCode(parseInt(str.substr(i * 2, 2), 16));
  }
  return utf8to16(val);
}

detail.vue 页面组装秤重量

console.log("weights",value,float,weight,unit,peel,bufferArray,dataString,sign);
      // 接收到数据 处理数据展示样子 带上小数点,正负号,显示称设置的皮重
      ipcRenderer.on("send-data", (event, args) => {
        if (
          this.resData &&
          ["kg", "千克", "公斤"].includes(this.resData.unit)
        ) {
          if (args.unit === "kg") {
            this.currPortData = args.value;
          } else {
            this.currPortData = args.value / 1000;
          }
        } else if (
          ["g", "克", "斤", "ml", "毫升"].includes(this.resData.unit)
        ) {
          if (args.unit === "g") {
            this.currPortData = args.value;
          } else {
            this.currPortData = args.value * 1000;
          }
        }
        if (args.sign !== "") {
          this.sign = args.sign === "0" ? "" : "-";
        }
      });
9.完成打印需求

1.主进程中初始化的时候请求打印机列表

2.发送事件到渲染进程,带上获取到的打印机列表参数

3.编写页面: webview 标签嵌入静态html文件path 获取打印机列表函数  触发打印的函数 监听渲染完成事件

4.外部触发打印组件的打印函数 传入需要打印的数据

5.打印组件 执行打印函数 获取列表 判断打印机状态 执行打印 获取打印参数 发起webview.send 传输数据

6.静态文件通过ipcRenderer.on("webview-print-render", xxx )接收打印组件发起的数据传输

7.静态文件通过传入的参数 渲染出正确的html页面 通过webview-print-do 发出渲染完成的消息

8.打印组件通过监听webview.addEventListener("ipc-message" ,xxx)执行

 webview.print  打印成功 

 查询打印机列表方法

1.主进程中初始化的时候请求打印机列表

2.发送事件到渲染进程,带上获取到的打印机列表参数

function getPrinterList() {
  // 在主线程下,通过ipcMain对象监听渲染线程传过来的getPrinterList事件
  ipcMain.on("getPrinterList", async (event) => {
    //在主线程中获取打印机列表
    const list = await mainWindow.webContents.getPrintersAsync();
    console.log(list, "getPrinterList");

    //通过webContents发送事件到渲染线程,同时将打印机列表也传过去
    mainWindow.webContents.send("getPrinterList", list);
  });
}

 app初始化的时候获取打印机列表


app.whenReady().then(() => {
  
  getPrinterList();

});

  打印机监控页面print.vue组件 等待发起打印需求

3.编写页面: webview 标签嵌入静态html文件path 获取打印机列表函数  触发打印的函数 监听渲染完成事件

<template>
  <div class="container">
    <webview
      id="printWebview"
      ref="printWebview"
      :src="fullPath"
      nodeintegration
      plugins
      webpreferences="contextIsolation=no"
    />
  </div>
</template>
<script>
import { ipcRenderer } from "electron";
import path from "path";

export default {
  name: "printer",

  props: {
    PrintEq: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      printList: [],
      printDeviceName: "",
      fullPath: null, // 静态html地址 等到传入传输渲染完毕后 执行打印功能
      messageBox: null,
      htmlData: {},
    };
  },

  async mounted() {
    const { BQ_PRINT_NAME } = this.$rootConfig;
    switch (this.PrintEq) {
      case BQ_PRINT_NAME: // 标签打印机
        this.fullPath = path.join(__static, "print/bq_print.html");
        break;
    }
    const webview = this.$refs.printWebview;
    webview.addEventListener("ipc-message", async (event) => {
      if (event.channel === "webview-print-do") {
        webview
          .print({
            silent: true,
            printBackground: true,
            deviceName: this.PrintEq,
          })
          .then((res) => {
            console.log(res);
          })
          .catch((err) => {
            console.log(err);
          })
          .finally(() => {
            // this.messageBox.close();
          });
      }
    });
  },

  methods: {
    print(val) {
      this.htmlData = val;
      this.getPrintListHandle();
    },
    // 获取打印机列表
    getPrintListHandle() {
      // 改用ipc异步方式获取列表,解决打印列数量多的时候导致卡死的问题
      ipcRenderer.send("getPrinterList");
      ipcRenderer.once("getPrinterList", (event, data) => {
        // 过滤可用打印机
        this.printList = data.filter((element) => element.status === 0);
        // 1.判断是否有打印服务
        if (this.printList.length <= 0) {
          this.$message({
            message: "打印服务异常,请尝试重启电脑",
            type: "error",
            offset: 100,
          });
          this.$emit("cancel");
        } else {
          this.checkPrinter();
        }
      });
    },

    // 2.判断打印机状态
    checkPrinter() {
      // 本地获取打印机
      const printerName = this.PrintEq;
      this.PrintEq; // 设置默认打印机名称 小票POSXP、杯贴POSBT
      const printer = this.printList.find(
        (device) => device.name === printerName
      );
      // 有打印机设备并且状态正常直接打印
      if (printer && printer.status === 0) {
        this.printDeviceName = printerName;
        this.printRender();
      } else if (printerName === "") {
        this.$message({
          message: "请先设置其他打印机",
          type: "error",
          duration: 1500,
          onClose: () => {
            // this.dialogVisible = false
          },
        });
        this.$emit("cancel");
      } else {
        this.$message({
          message: "当前打印机不可用,请重新设置",
          type: "error",
          duration: 1500,
          onClose: () => {
            // this.dialogVisible = false
          },
        });
        this.$emit("cancel");
      }
    },

    printRender() {
      // console.log(this.printDeviceName,'+',this.htmlData)
      this.messageBox = this.$message({
        message: "打印中,请稍后",
        type: "success",
        duration: 1500,
        offset: 100,
      });
      // 获取<webview>节点
      let webview = this.$refs.printWebview;
      // 发送信息到<webview>里的页面
      webview.send("webview-print-render", {
        printName: this.printDeviceName,
        htmlData: this.htmlData,
      });
    },
  },
};
</script>
<style scoped>
.container {
  position: absolute;
  left: 0;
  top: 7rem;
  width: 180px;
  height: 300px;
  z-index: -1;
  display: none;
}

webview {
  width: 170px !important;
  height: calc(100vh - 1rem) !important;
  top: 0 !important;
  right: 0;
  bottom: 0;
  left: 0;
  position: relative !important;
  visibility: visible !important;
}
</style>

 发起打印请求页面,传入参数

4.外部触发打印组件的打印函数 传入需要打印的数据

    <printer
      ref="pos_bq_print"
      :print-eq="BQ_PRINT_NAME"
      :html-data="BQHtmlData"
      @complete="clearBqHtmlData"
    ></printer>



    goPrint() {
         this.BQHtmlData = {
               //xxxx
          };
           // 调用print组件的打印函数
          this.$refs.pos_bq_print.print(this.BQHtmlData); // 打印杯贴       
      } 
     

5.打印组件 执行打印函数 获取列表 判断打印机状态 执行打印 获取打印参数 发起webview.send 传输数据

6.静态文件通过ipcRenderer.on("webview-print-render", xxx )接收打印组件发起的数据传输

7.静态文件通过传入的参数 渲染出正确的html页面 通过webview-print-do 发出渲染完成的消息

静态html文件


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <script src="../js/vue.js"></script>
  <title>标签</title>
  <style type="text/css">
    @page {
      margin: 0;
    }

    body {
      width: 260px;
      height: 130px;
    }

    * {
      padding: 0;
      margin: 0;
      font-size: 12px;
      font-family: PingFang SC;
      color: #000000;
    }

    #app {
      width: 260px;
      height: 130px;
      padding: 3px 10px;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
    }

    h3 {
      text-align: center;
      margin-bottom: 10px;
    }

    .bq-print {
      display: flex;
      justify-content: space-between;
    }

    .desc-box {
      letter-spacing: 1px;
      width: 55%;
      margin-right: 0px;
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
    }

    .desc {
      word-break: break-all;
      display: flex;
      justify-content: flex-start;
      width: 100%;
      flex-wrap: wrap;
      margin-bottom: 4px;
    }

    .desc-date {
      text-align: center;
    }

    .qrcode-box {
      width: calc(100% - 55% - 20px);
    }

    .qrcode img {
      width: 80%;
      height: 80%;
      box-sizing: border-box;
      padding: 1px;
    }
  </style>
</head>

<body>
  <div id="app">
    <div class="bq-print">
      <div class="desc-box">
       
        <div class='desc'>产品: {{product.prodName }}</div>
        <div class='desc'>xxxx</div>
        <div class='desc'>xxxx</div>
        <div class='desc'>xxxx</div>
      </div>
      <div class="qrcode-box">
        <div class="qrcode" id='qrCodeUrl' ref="qrCodeUrl"></div>
        <div class="desc-date">{{ product.date }}</div>
      </div>
    </div>
</body>

<script>
  //引入ipcRenderer对象
  const { ipcRenderer } = require("electron");
  //生成二维码
  let QRCode = require("qrcodejs2");
  new Vue({
    el: "#app",
    data: {
      product: {},
    },
    mounted() {
// 监听外部发送进来的数据 执行渲染
      ipcRenderer.on("webview-print-render", (event, info) => {
        this.product = info.htmlData;
        if (info.htmlData.QRCODE) {
          this.creatQrCode()
          const date = info.htmlData.QRCODE.split('~')[2]
          this.product.date = this.formatDate(Number(date), 2)
        }
        // 执行渲染
        ipcRenderer.sendToHost("webview-print-do");
      });
    },
    methods: {
      // 创建二维码
      creatQrCode(url) {
        this.$refs.qrCodeUrl.innerHTML = ''
        // this.product.QRCODE_INNERHTML;
        let qrcode = new QRCode(this.$refs.qrCodeUrl, {
          text: this.product.QRCODE, // 需要转换为二维码的内容
          width: 90,
          height: 90,
          colorDark: "#000000",
          colorLight: "#ffffff",
          correctLevel: QRCode.CorrectLevel.L,
        });
      },
     
    },
  });
</script>

</html>

8.打印组件通过监听webview.addEventListener("ipc-message" ,xxx)执行

 webview.print  打印成功 

10.完成自动更新

 自动更新监控文件


import { autoUpdater } from "electron-updater";

import { ipcMain } from "electron";

import { app } from "electron";

let mainWindow = null;

export function updateHandle(window, feedUrl) {
  mainWindow = window;
  let message = {
    error: "检查更新出错",
    checking: "正在检查更新……",
    updateAva: "检测到新版本,正在下载……",
    updateNotAva: "现在使用的就是最新版本,不用更新",
  };
  //设置更新包的地址
  autoUpdater.setFeedURL(feedUrl);
  //监听升级失败事件
  autoUpdater.on("error", function (error) {
    sendUpdateMessage({
      cmd: "error",
      message: error,
    });
  });
  //监听开始检测更新事件
  autoUpdater.on("checking-for-update", function (message) {
    sendUpdateMessage({
      cmd: "checking-for-update",
      message: message,
    });
  });

  //监听发现可用更新事件
  autoUpdater.on("update-available", function (message) {
    sendUpdateMessage({
      cmd: "update-available",
      message: message,
    });
  });

  //监听没有可用更新事件
  autoUpdater.on("update-not-available", function (message) {
    sendUpdateMessage({
      cmd: "update-not-available",
      message: message,
    });
  });

  // 更新下载进度事件
  autoUpdater.on("download-progress", function (progressObj) {
    sendUpdateMessage({
      cmd: "download-progress",
      message: progressObj,
    });
  });
  //监听下载完成事件
  autoUpdater.on(
    "update-downloaded",
    function (event, releaseNotes, releaseName, releaseDate, updateUrl) {
      sendUpdateMessage({
        cmd: "update-downloaded",
        message: {
          releaseNotes,
          releaseName,
          releaseDate,
          updateUrl,
        },
      });
      //退出并安装更新包
      autoUpdater.quitAndInstall();
    }
  );

  // 接收渲染进程消息,开始检查更新
  ipcMain.on("checkForUpdate", (e, arg) => {
    //执行自动更新检查
    // sendUpdateMessage({cmd:'checkForUpdate',message:arg})
    autoUpdater.checkForUpdates();
  });

}

//给渲染进程发送消息
function sendUpdateMessage(text) {
  mainWindow.webContents.send("message", text);
}

mian.js进程 引入文件

import { updateHandle } from "../renderer/utils/update.js";

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: config_res.IS_DEBUG, // 是否有边框窗口
    fullscreen: !config_res.IS_DEBUG, // 全屏
    webPreferences: {
      // preload: path.join(__dirname, "preload.js"),
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
      webviewTag: true,
      webSecurity: false,
    },
  });
 
  /**
   * 检测版本更新
   * 设置版本更新地址,即将打包后的latest.yml文件和exe文件同时放在
   * http://xxxx/test/version/对应的服务器目录下,该地址和package.json的publish中的url保持一致
   */
  let feedUrl = config_res.UPDATE_URL; // 更新地址
  updateHandle(mainWindow, feedUrl);
  // 监听崩溃
}

 以下两个文件为主要资源包 均需要放在在线更新地址里,检测到更新后才会拉去更新

yml文件是主要更新监控是否有更新 一定要替换该文件在在线更新地址里,不然检测不到更新

11.防止开启多个应用

已经开启后 再次双击打开会打开当前的窗口至于首层

// 防止开启多个应用
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    //输入从第二个实例中接收到的数据
    //有人试图运行第二个实例,我们应该关注我们的窗口
    if (mainWindow && !mainWindow.isDestroyed()) {
      if (mainWindow.isMinimized()) {
        mainWindow.restore();
      }
      mainWindow.focus();
    }
  });
  //创建myWindow,加载应用的其余部分,etc...

;