1 背景
架构设计:VueJS + Spring Cloud微服务架构
功能要求:
-
调用小票打印机打印小票,功能和超市收银结算功能相同
-
使用NWJS包装VueJS前端代码实现exe安装包和可执行文件
2 调查
经过调查,主要有如下几种思路。
2.1 思路1:使用IP+Port方式调用网络打印机
代码如下,只需要调用node的net模块即可。详情请看参考1。
var net = require('net');
var client = new net.Socket();
var buffer; // Buffer类型,放你的打印指令,具体的小票打印指令可以搜索ESC/POS指令
client.connect(port, ip, function () {
client.write(buffer, function(){});
这种方式没有去尝试,因为不清楚IP地址和端口。这边的需求是小票机直连PC的,不是网络方式。
2.2 思路2:NodeJS IPP协议
Github上有一个project名为ipp,Internet Printing Protocol即网络打印协议。IPP协议始于90年代,一直沿用至今。目前有数以百万计的打印机都支持该协议。
可以使用如下方式验证你的打印机是否支持IPP协议:
ipp检测本机打印机方法如下:
var mdns = require('mdns'),
browser = mdns.createBrowser(mdns.tcp('ipp'));
mdns.Browser.defaultResolverSequence[1] = 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]});
browser.on('serviceUp', function (rec) {
console.log(rec.name, 'http://'+rec.host+':'+rec.port+'/'+rec.txtRecord.rp);
browser.start();
//example output...
//HP LaserJet 400 M401dn (972E51) http://CP01.local:631/ipp/printer
//HP LaserJet Professional P1102w http://P1102W.local:631/printers/Laserjet
//Officejet Pro 8500 A910 [611B21] http://HPPRINTER.local:631/ipp/printer
官方示例:
var ipp = require('ipp');
var PDFDocument = require('pdfkit');
//make a PDF document
var doc = new PDFDocument({margin:0});
doc.text(".", 0, 780);
doc.output(function(pdf){
// 这里需要打印机的url,可以使用上面的方法得到
var printer = ipp.Printer("http://NPI977E4E.local.:631/ipp/printer");
var msg = {
"operation-attributes-tag": {
"requesting-user-name": "William",
"job-name": "My Test Job",
"document-format": "application/pdf"
data: pdf
// 执行打印任务,这里打印的是PDF
printer.execute("Print-Job", msg, function(err, res){
console.log(res);
这种方式没有尝试,在说明书里面没找到这个IPP协议。也没有尝试使用上面的JavaScript代码进行检测。
具体请看参考2和参考3。
2.3 Lodop打印控件
官方示例如下:
看样子还是比较简单的,但是需要exe文件安装浏览器控件或者Web服务。这个会加大实施运维的难度,所以放弃了。
具体请看参考4、参考5和参考6。
2.4 使用Node Printer模块
在参考7中使用了NWJS,且打印的效果和我们的项目预期非常相似。
其封装了nodejs的printer模块,并且是在NWJS中调用的小票机进行打印。
下面的解决方案将以此调查结果进行开展。
3 解决
下面将一步步介绍如何在NWJS中调用小票打印机。为了尽量描述项目实际情况且不包含任何公司机密信息,下面的例子都是网上可见的。
注意:项目中NWJS和VueJS是分成两个项目,二者通过NWJS中index.html中的iframe建立联系。当然也可以把二者合一,这个可以参照nw-seed-vue项目(
https://github.com/anchengjian/vue-nw-seed
)。
3.1 准备VueJS项目
为了简化VueJS项目,这里直接使用vue-cli创建一个hello world项目。
创建后的项目如下:
3.2 准备NWJS项目
NWJS原本的打包工作不是自动化的,不过NWJS官方推荐了自动化打包工具:
https://github.com/evshiron/nwjs-builder-phoenix
我们直接使用官方的示例作为初始NWJS项目:
https://github.com/evshiron/nwjs-builder-phoenix/tree/master/assets/project
注意:这里用的NWJS是0.14.7版本,不要问我为什么,因为它支持XP系统!
附XP下Chrome浏览器下载地址:
https://dl.google.com/release2/h8vnfiy7pvn3lxy9ehfsaxlrnnukgff8jnodrp0y21vrlem4x71lor5zzkliyh8fv3sryayu5uk5zi20ep7dwfnwr143dzxqijv/49.0.2623.112_chrome_installer.exe
3.2.1 检出并安装依赖
项目检出后,执行npm install命令安装依赖,具体项目结构如下:
3.2.2 调整package.json中的配置
这个Sample写的无力吐槽,我们按照
https://github.com/evshiron/nwjs-builder-phoenix
官方给出的Getting Started改造一下吧。
官方是在package.json中添加如下两段代码:
{
// 1. 指定NWJS版本为0.14.7
"build": {
"nwVersion": "0.14.7"
// 2.指定npm脚本
// 移除不需要的构建任务:linux-x86,linux-x64,mac-x64
// 替换掉NWJS镜像(国内还是淘宝镜像好使):https://dl.nwjs.io/改成https://npm.taobao.org/mirrors/nwjs/
// 本地运行x64版本
"scripts": {
"dist": "build --tasks win-x86,win-x64 --mirror https://npm.taobao.org/mirrors/nwjs/ .",
"start": "run --x64 --mirror https://npm.taobao.org/mirrors/nwjs/ ."
修改上述配置后,需要重新npm install一下。
3.2.3 改造index.html页面
原本的index.html如下:启动完NWJS后就关闭了。
<html>
<head></head>
<script>
process.exit(parseInt(nw.App.argv[0]));
</script>
</body>
</html>
index.html页面需要嵌入iframe,地址就是VueJS项目运行地址:
<html>
<head></head>
<style>
/* 有一丢丢滚动条,隐藏掉 */
body {
overflow: hidden;
/* 调整iframe为100%大小,并去掉默认的边框 */
#iframe {
width: 100%;
height: 100%;
border: none;
</style>
<iframe id="iframe" src="http://localhost:8080/"></iframe>
</body>
</html>
上面设置的仅仅是iframe的大小,但是本身NWJS运行的窗口却很小,需要调整下。
修改package.json配置文件,调整NWJS窗口大小:详情参照
https://nwjs.org.cn/doc/api/Manifest-Format.html#%E7%AA%97%E5%8F%A3%E5%AD%90%E5%AD%97%E6%AE%B5
运行如下命令,查看NWJS程序:
npm run start
打开的窗口如下:
3.3 安装打印相关依赖
在参考7(
https://juejin.im/post/5c6a77816fb9a049f23d50d4
)中是这样做的:使用node printer模块,然后自己封装esc/pos指令。
这里也是使用node printer,但是使用的是chn-escpos来处理ESC/POS指令。
3.3.1 添加chn-escpos依赖
在package.json中的"dependencies"中添加如下依赖:
"chn-escpos": "1.1.4"
先不急执行npm install。
3.3.2 编译node printer
安装nw-gyp参照:
https://www.npmjs.com/package/nw-gyp
安装node printer参照:
https://github.com/tojocky/node-printer
汇总下,就变成下面这样:
# !!!不确定是否这个需要安装
npm install -g node-gyp
# 安装时间有点长,还会安装Python。nw-gyp需要它!
# 参考:https://www.npmjs.com/package/windows-build-tools
# 可以指定python镜像地址:--python_mirror=https://npm.taobao.org/mirrors/python/
npm --python_mirror=https://npm.taobao.org/mirrors/python/ --vs2015 install --global windows-build-tools
# 全局安装nw-gyp
npm install -g nw-gyp
# 安装依赖:chn-escpos、printer...
npm install
# 切换到刚刚npm install后的printer目录下
cd node_modules/printer
# 使用nw-gyp rebuild:Runs clean, configure and build all in a row
# 这样就得到了针对NWJS0.14.7版本的64位的printer模块
nw-gyp rebuild --target=0.14.7 --arch=x64 --dist-url=https://npm.taobao.org/mirrors/nwjs --python=C:\Users\63450\.windows-build-tools\python27\python.exe --msvs_version=2015
关于32位printer模块的编译:
# 一定要用32位node!
# arch改成ia32
nw-gyp rebuild --target=0.14.7 --arch=ia32 --dist-url=https://npm.taobao.org/mirrors/nwjs --python=C:\Users\63450\.windows-build-tools\python27\python.exe --msvs_version=2015
3.4 调用打印相关API
打印函数提供方:NWJS项目调用chn-escpos实现
打印函数调用方:vue-hello-world项目
二者通讯方式:window.postMessage(
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
)
3.4.1 NWJS项目提供函数用于打印
在index.html页面添加如下代码:
<script>
// 初始化iframe的src
var iframe = document.getElementById("iframe");
var domain = "http://localhost:8080/";
iframe.src = domain;
// 检查下node printer装好了没有,如果弹不出来,那么你的node printer没有编译好!
var printer = require('chn-escpos');
alert(printer);
// 监听message事件,这个就是window.postMessage触发的事件
window.addEventListener('message', function(e) {
var data = e.data;
// 别的地方可能也会触发message事件,所以需要过滤
if (data.type === 'print') {
print(data);
function print(data) {
new printer(data.printerName, function(err, msg) {
// 处理回调
if (err || msg) {
iframe.contentWindow.postMessage({err: err, msg: msg, type: 'printCallback'}, domain);
if (err) {
return;
// 这里写实际的打印逻辑
this.line(3);
this.text('测试打印');
this.line(3);
// 执行打印并处理回调
this.print(function(err, msg) {
iframe.contentWindow.postMessage({err: err, msg: msg, type: 'printCallback'}, domain);
</script>
3.4.2 vue-hello-world调用NWJS提供的打印函数
在HelloWorld.vue中添加如下代码:原谅我还不会写ES6语法
<script>
// 监听message事件,处理回调
window.addEventListener('message', function(e) {
var data = e.data;
// 别的地方可能也会触发message事件,所以需要过滤
if (data.type === 'printCallback') {
console.log(data);
// 调用打印机进行打印
window.parent.postMessage({printerName: 'OneNote', type: 'print'}, '*');
</script>
3.4.3 验证打印
启动vue-hello-world和NWJS项目,先看到弹窗,说明node printer编译好了:
然后可以看到OneNote被调用起来,并打印出了回调函数值:
OK!!!
3.5 XP下存在的问题
XP32下,chn-escpos调用打印机打印功能会导致NWJS直接闪退:
[0422/180048:WARNING:pe_image_reader.cc(144)] CodeView debug entry of unexpected size in C:\WINDOWS\system32\OLEACC.dll
后来调试发现,只要在打印前不获取打印机信息就不会闪退,出问题的代码如下:
这个问题解决不掉,只能不去获取打印机信息了。
我们可以把chn-escpos相关代码全部提取出来,放到chn-escpose-printer.js中:
var iconv = require('iconv-lite'),
cmds = {
INITIAL_PRINTER: '\x1B\x40', //Initial paper
NEW_LINE: '\x0A', //Add new line
PAPER_CUTTING: '\x1d\x56\x41', //Cut paper
LINE_HEIGHT: '\x1b\x32', //Normal line height
LINE_HEIGHT_B: '\x1b\x33\x6e', //Normal line height large
CHN_TEXT: '\x1b\x52\x0f', //CHN text
// text style
TXT_NORMAL: '\x1d\x21\x00', // Normal text
TXT_SIZE: '\x1d\x21', // Double height text
TXT_UNDERL_OFF: '\x1b\x2d\x00', // Underline font OFF
TXT_UNDERL_ON: '\x1b\x2d\x01', // Underline font 1-dot ON
TXT_UNDERL2_ON: '\x1b\x2d\x02', // Underline font 2-dot ON
TXT_BOLD_OFF: '\x1b\x45\x00', // Bold font OFF
TXT_BOLD_ON: '\x1b\x45\x01', // Bold font ON
TXT_ALIGN_L: '\x1b\x61\x00', // Left justification
TXT_ALIGN_C: '\x1b\x61\x01', // Centering
TXT_ALIGN_R: '\x1b\x61\x02', // Right justification
TXT_FONT_A: '\x1b\x4d\x00', // Font type A
TXT_FONT_B: '\x1b\x4d\x01', // Font type B
TXT_FONT_C: '\x1b\x4d\x02', // Font type C
TXT_FONT_D: '\x1b\x4d\x48', // Font type D
TXT_FONT_E: '\x1b\x4d\x31', // Font type E
//barcode
BARCODE_TXT_OFF: '\x1d\x48\x00', // HRI barcode chars OFF
BARCODE_TXT_ABV: '\x1d\x48\x01', // HRI barcode chars above
BARCODE_TXT_BLW: '\x1d\x48\x02', // HRI barcode chars below
BARCODE_TXT_BTH: '\x1d\x48\x03', // HRI barcode chars both above and below
BARCODE_FONT_A: '\x1d\x66\x00', // Font type A for HRI barcode chars
BARCODE_FONT_B: '\x1d\x66\x01', // Font type B for HRI barcode chars
BARCODE_HEIGHT: '\x1d\x68\x64', // Barcode Height [1-255]
BARCODE_WIDTH: '\x1d\x77\x03', // Barcode Width [2-6]
//一维码
BARCODE_UPC_A: '\x1d\x6b\x00', // Barcode type UPC-A
BARCODE_UPC_E: '\x1d\x6b\x01', // Barcode type UPC-E
BARCODE_EAN13: '\x1d\x6b\x02', // Barcode type EAN13
BARCODE_EAN8: '\x1d\x6b\x03', // Barcode type EAN8
BARCODE_CODE39: '\x1d\x6b\x04', // Barcode type CODE39
BARCODE_ITF: '\x1d\x6b\x05', // Barcode type ITF
BARCODE_NW7: '\x1d\x6b\x06', // Barcode type NW7
//二维码,from http://stackoverflow.com/questions/23577702/printing-qr-codes-through-an-esc-pos-thermal-printer
QRCODE_SIZE_MODAL: '\x1D\x28\x6B\x03\x00\x31\x41\x32\x00', //Select the model,[49 x31, model 1] [50 x32, model 2] [51 x33, micro qr code]
QRCODE_SIZE: '\x1D\x28\x6B\x03\x00\x31\x43', //Set the size of module
QRCODE_ERROR: '\x1D\x28\x6B\x03\x00\x31\x45\x31', //Set n for error correction [48 x30 -> 7%] [49 x31-> 15%] [50 x32 -> 25%] [51 x33 -> 30%]
QRCODE_AREA_LSB: '\x1D\x28\x6B', //Store the data in the symbol storage area LSB
QRCODE_AREA_MSB: '\x31\x50\x30', //Store the data in the symbol storage area MSB
QRCODE_PRINT:'\x1D\x28\x6B\x03\x00\x31\x51\x30', //Print the symbol data in the symbol storage area
CASHBOX_OPEN: '\x1B\x70\x00\xFF\xFF', //Open casebox
BEEP:'\x1b\x42' //beep
node_printer = require("printer"),
BufferHelper = require('bufferhelper');
* 打印任务
* @param {string} printer_name 打印机名
* @param {function} callback function(err,msg),当获取打印机后执行,如果不存在指定打印机,返回err信息
var printer = function(printer_name, callback) {
if (!printer_name) {
printer_name = node_printer.getDefaultPrinterName();
this.printer = printer_name;
/* 解决在XP系统下获取打印机信息崩溃的问题
try {
node_printer.getPrinter(this.printer);
} catch (err) {
if (callback) callback.call(this, err, 'Can\'t find the printer');
return false;
this._queue = new BufferHelper();
this._writeCmd('INITIAL_PRINTER');
this._writeCmd('CHN_TEXT');
if (callback) callback.call(this, null, 'Get printer success');
printer.prototype = {
* 打印文字
* @param {string} text 文字内容
* @param {boolen} inline 是否换行
* @return {object} 当前对象
text: function(text, inline) {
if (text) {
this._queue.concat(iconv.encode(text, 'GBK'));
if (!inline) this._writeCmd('NEW_LINE');
return this;
* 打印空行
* @param {number} number 行数
* @return {object} 当前对象
line: function(number) {
number = number || 1;
for (var i = 0; i < number; i++) {
this._writeCmd('NEW_LINE');
return this;
* 设置对其
* @param {string} align 居中类型,L/C/R
* @return {object} 当前对象
setAlign: function(align) {
this._writeCmd('TXT_ALIGN_' + align.toUpperCase());
return this;
* 设置字体
* @param {string} family A/B/C/D/E
* @return {object} 当前对象
setFont: function(family) {
this._writeCmd('TXT_FONT_' + family.toUpperCase());
return this;
* 设置行距
* @param {number} hex 16进制数据,如'\x05'
setLineheight: function(hex) {
this._writeCmd('LINE_HEIGHT');
if (hex) {
//console.log('\x1b\x33'+hex);
this._queue.concat(new Buffer('\x1b\x33' + hex));
//设置默认行间距
return this;
* 设置格式(加粗,下拉)
* @param {string} type B/U/U2/BU/BU2
* @return {object} 当前对象
setStyle: function(type) {
switch (type.toUpperCase()) {
case 'B':
this._writeCmd('TXT_UNDERL_OFF');
this._writeCmd('TXT_BOLD_ON');
break;
case 'U':
this._writeCmd('TXT_BOLD_OFF');
this._writeCmd('TXT_UNDERL_ON');
break;
case 'U2':
this._writeCmd('TXT_BOLD_OFF');
this._writeCmd('TXT_UNDERL2_ON');
break;
case 'BU':
this._writeCmd('TXT_BOLD_ON');
this._writeCmd('TXT_UNDERL_ON');
break;
case 'BU2':
this._writeCmd('TXT_BOLD_ON');
this._writeCmd('TXT_UNDERL2_ON');
break;
case 'NORMAL':
default:
this._writeCmd('TXT_BOLD_OFF');
this._writeCmd('TXT_UNDERL_OFF');
break;
return this;
* 设定字体尺寸
* @param {string} size 2/null
* @return {object} 当前对象
setSize: function(size) {
this._writeCmd('TXT_NORMAL');
this._writeCmd('LINE_HEIGHT');
switch(parseInt(size)){
case 2:
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x10'));
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x01'));
break;
case 3:
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x32'));
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x02'));
break;
case 4:
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x48'));
this._queue.concat(new Buffer(cmds['TXT_SIZE']+'\x03'));
break;
return this;
* 二维码
* @param {string} code 打印内容
* @param {string} type 打印类型,UPC-A(11-12)/UPC-E(11-12,不可用)/EAN13(默认,12-13)/EAN8(7-8)/CODE39(1-255,不可用)/ITF(1-255偶数,不可用)/NW7(1-255,不可用)
* @param {number} width 宽度
* @param {number} height 高度
* @param {string} position OFF/ABV/BLW/BTH
* @param {string} font 字体A/B
* @return {object} 当前对象
barcode: function(code, type, width, height, position, font) {
if (width >= 1 || width <= 255) {
this._writeCmd('BARCODE_WIDTH');
if (height >= 2 || height <= 6) {
this._writeCmd('BARCODE_HEIGHT');
this._writeCmd('BARCODE_FONT_' + (font || 'A').toUpperCase());
this._writeCmd('BARCODE_TXT_' + (position || 'BLW').toUpperCase());
this._writeCmd('BARCODE_' + ((type || 'EAN13').replace('-', '_').toUpperCase()));
this._queue.concat(new Buffer(code));
return this;
* 打印二维码,需要打印机支持
* @param {string} text 打印文字内容
* @param {string} size 二维码大小,16进制字符串,如'\x01'.默认为'\x06'
* @param {string} lsb (text长度+3)%256转16进制后的字符,如'\x01';
* @param {[type]} msb (text长度+3)/256取整转16进制后的字符,如'\x00';
* @return {object} 当前对象
qrcode:function(text,size,lsb,msb){
size=size?size:'\x06';
if(!/^[\w\:\/\.\?\&\=]+$/.test(text)){
this.text('二维码请使用英文和数字打印');
return this;
this._writeCmd('QRCODE_SIZE_MODAL');
this._queue.concat(new Buffer(cmds['QRCODE_SIZE']+size));
this._writeCmd('QRCODE_ERROR');
this._queue.concat(new Buffer(cmds['QRCODE_AREA_LSB']+lsb+msb+cmds['QRCODE_AREA_MSB']));
this._queue.concat(new Buffer(text));
this._writeCmd('QRCODE_PRINT');
return this;
* 蜂鸣警报
* @param {string} times 蜂鸣次数,16进制,1-9.默认'\x09'
* @param {string} interval 蜂鸣间隔,16进制,实际间隔时间为interval*50ms,默认'\x01'
* @return {object} 当前对象
beep:function(times,interval){
times=times?times:'\x09';
interval=interval?interval:'\x01';
this._queue.concat(new Buffer(cmds['BEEP']+times+interval));
return this;
* 打开钱箱
* @return {object} 当前对象
openCashbox: function() {
this._writeCmd('CASHBOX_OPEN');
return this;
* 编译指定语法字符串为print方法
* @param {string} string 语法字符串
* @return {object} 当前对象
compile: function(string) {
if (typeof string != 'string') {
console.log('必须为字符串');
return this;
var _this = this;
//替换换行
var tpl = string.replace(/[\n\r]+/g, '/n')
//替换函数
.replace(/<%([\s\S]+?)%>/g, function(match, code) {
return '",true);\n' + _this._renderFunc(code) + '\nobj.text("';
//替换换行
.replace(/\/n/g, '");\nobj.text("');
tpl = 'obj.text("' + tpl + '")';
new Function('obj', tpl).call(this, this);
return this;
* 执行命令
* @param {string} cmd 命令名
_writeCmd: function(cmd) {
if (cmds[cmd]) {
this._queue.concat(new Buffer(cmds[cmd]));
_renderFunc: function(string) {
var _this = this,
status = true;
string = string.trim();
//函数名生命
var func = string.replace(/^([\S]+?):/, function(match, code) {
var func_name = _this._escape(code);
if (!_this[func_name]) {
//无效函数
status = false;
console.log('解析模板出错没有名为' + func_name + '的方法');
return 'obj.' + func_name + ':';
//函数变量
}).replace(/:([\S]+?)$/, function(match, code) {
var func_var = _this._escape(code).split(','),
tpl_var = '';
var length = func_var.length;
for (var i = 0; i < length; i++) {
//%u hack
var cur_func_var = func_var[i];
if(/%u/.test(func_var[i])){
cur_func_var=cur_func_var.replace(/%u/g,'u');
tpl_var += '"' + cur_func_var + '",';
tpl_var = tpl_var.replace(/\,$/, '');
return '(' + tpl_var + ');';
if (status) return func
else return '';
* 预留跨域攻击防御
* @param {string} string 目标内容
* @return {string} 转换后
_escape: function(string) {
string = unescape(string.replace(/\u/g, "%u")); //转换unicode为正常符号
string = string.replace(/[\<\>\"\'\{\}]/g, '');
return string;
* 执行打印
* @param {Function} callback function(err,msg),当执行打印后,回调该函数,打印错误返回err信息
print: function(callback) {
this._writeCmd('PAPER_CUTTING');
this._writeCmd('INITIAL_PRINTER');
this.sendCmd(callback);
* 发送命令
* @param {Function} callback function(err,msg),当执行打印后,回调该函数,打印错误返回err信息
sendCmd:function(callback){
var _this = this;
node_printer.printDirect({
data: _this._queue.toBuffer(),
printer: _this.printer,
type: "RAW",
success: function() {
callback.call(_this, null, 'Print successed');
_this._queue.empty();
error: function(err) {
callback.call(_this, null, 'Print failed');
* 清空打印内容
* @return {object} 当前对象
empty: function() {
this._queue.empty();
return this;
module.exports = printer;
然后调整package.json中的依赖:之前的chn-escpose删掉
"dependencies": {
"printer": "^0.4.0",
"bufferhelper": "^0.2.1"
再调整index页面对原有chn-escpose的引入就可以了:
var printer = require('./chn-escpos-printer');
alert(printer);
print({printerNamer:"One"});
alert(11);
详情参见nwjs-node-printer-xp.rar。
附
1. NWJS项目:nwjs-project.zip
2. vue-hello-world项目:vue-hello-world.zip
3. 编译好的printer(64位、32位)模块:node printer.zip.001、node printer.zip.002、node printer.zip.003,三个一起解压,然后把64位或者32位放到你的node_modules下面,这样就不需要install这个printer模块了
demo下载链接:
https://download.csdn.net/download/u012383839/16031727
参考
1. [NodeJs 如何驱动小票打印机打印小票?](
https://segmentfault.com/q/1010000008790881/a-1020000008802345
)
2. [NodeJS使用ipp协议打印](
https://www.iteye.com/blog/liyunpeng-2098669
)
3. [NodeJS ipp](
https://github.com/williamkapke/ipp
)
4. [nwjs打印小票案例,包含二维码](
https://www.jianshu.com/p/003aad570c08
)
5. [lodop官方打印示例](
http://www.lodop.net/demolist/PrintSample1.html
)
6. [VueJS中使用lodop](
http://www.c-lodop.com/faq/pp35.html
)
7. [如何通过Nw.js纯前端实现调用热敏打印机打印小票?](
https://juejin.im/post/5c6a77816fb9a049f23d50d4
)
8. [NWJS自动化打包工具:nwjs-builder-phoenix](
https://nwjs.org.cn/doc/user/Package-and-Distribute.html
)
9. [github: nwjs-builder-phoenix](
https://github.com/evshiron/nwjs-builder-phoenix
)
10. [ESC/POS指令](
https://wenku.baidu.com/view/9a165cb8647d27284a735128.html
)
11. [npm chn-escpos](
https://www.npmjs.com/package/chn-escpos
)
12. [关于在nw里使用require('printer')和nw.require('printer')报错的问题](
https://blog.csdn.net/qq_39702364/article/details/82800935
)
13. [在nw.js要如何优雅的使用node-printer](
https://www.jianshu.com/p/b3c558ddb914
)
14. [npm nw-gyp](
https://www.npmjs.com/package/nw-gyp
)
15. [window.postMessage](
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
)
16. [NW.js安装原生node模块node-printer控制打印机](
https://www.cnblogs.com/pannysp/p/9687743.html
)
17. [在NW.js中安装Node原生模块](
https://nwjs.org.cn/doc/user/Advanced/Use-Native-Node-Modules.html
)
18. [A seed project with vue and nwjs ](
https://github.com/anchengjian/vue-nw-seed
)