全部文章

<p>由于 jsbarcode 不支持 Code93 条码,所以只能自己动手。</p> <p>之前有想过用 <code>bwip-js</code> 替换 <code>jsbarcode</code>,但由于需求要支持更改条码内文字的字体,<code>bwip-js</code> 虽然也能<a href="https://juejin.cn/post/7297024168604549156">修改字体</a>,但是还要加载字体文件,这个方案需要修改的文件太多,也怕引入其它 bug,最终没有采取这个方案。</p> <p>首先,需要了解一下 Code 93</p> <h1>Code 93</h1> <p>1982 年 Intermec 公司设计;主要被加拿大邮政使用。<a href="https://en.wikipedia.org/wiki/Code_93">参考</a></p> <h2>允许的字符</h2> <p><code>0-9</code>、<code>A-Z</code>、<code>-</code>、<code>.</code>、<code>$</code>、<code>/</code>、<code>+</code>、<code>%</code>、<code>空格</code></p> <h2>结构</h2> <p><img src="/uploads/code93-structure.jpg" alt="" /></p> <h1>实战</h1> <h2>1 克隆 github 仓库</h2> <pre><code>git clone https://github.com/lindell/JsBarcode.git</code></pre> <h2>2 添加 Code93 代码,参考<a href="https://github.com/elysiumphase/bitgener/blob/master/src/Barcode/Code93.js">Code 93</a></h2> <p><code>src/barcodes</code> 下新建 <code>CODE93</code> 目录,新建 <code>index.js</code> 文件</p> <pre><code>// index.js // Encoding documentation: // https://en.wikipedia.org/wiki/Code_93#Encoding import Barcode from "../Barcode.js"; const encoding = [ "100010100", ... ]; class CODE93 extends Barcode { constructor(data, options) { data = data.toUpperCase(); super(data, options); } encode() { const data = this.data; const table = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%____*"; // _ => ($), (%), (/) et (+) const dataToEncode = data.toUpperCase(); const dataToEncodeLength = dataToEncode.length; let digit = ""; // start : * digit += encoding[47]; // digits for (let i = 0; i < dataToEncodeLength; i += 1) { digit += encoding[table.indexOf(dataToEncode.charAt(i))]; } // checksum let weightC = 0; let weightSumC = 0; let weightK = 1; // start at 1 because the right-most character is 'C' checksum let weightSumK = 0; for (let i = dataToEncodeLength - 1; i >= 0; i -= 1) { weightC = weightC === 20 ? 1 : weightC + 1; weightK = weightK === 15 ? 1 : weightK + 1; const index = table.indexOf(dataToEncode.charAt(i)); weightSumC += weightC * index; weightSumK += weightK * index; } const c = weightSumC % 47; weightSumK += c; const k = weightSumK % 47; digit += encoding[c]; digit += encoding[k]; // stop : * digit += encoding[47]; // terminaison bar digit += "1"; const encodedData = { data: digit, text: dataToEncode, }; return encodedData; } valid() { return /^[0-9a-zA-Z-. $/+%]+$/.test(this.data); } } export { CODE93 }; </code></pre> <h2>3 编译打包</h2> <p>在 <code>jsbarcode</code> 仓库根目录执行 <code>npm run build</code></p> <h2>4 本地调试</h2> <ol> <li>在 <code>jsbarcode</code> 仓库根目录执行 <code>npm link</code></li> <li>在使用 <code>jsbarcode</code> 的仓库执行 <code>npm link jsbarcode</code></li> <li>使用 <pre><code class="language-javascript">JsBarcode("#barcode", "1234", { format: "CODE93", width: 4, height: 40, displayValue: false, });</code></pre></li> <li>显示结果</li> </ol> <p><img src="/uploads/code93.jpg" alt="" /></p> <h1>参考</h1> <p><a href="https://en.wikipedia.org/wiki/Code_93">维基百科 Code 93</a></p> <p><a href="https://github.com/elysiumphase/bitgener/blob/master/src/Barcode/Code93.js">Code 93</a></p>
详情
<p>继上一篇文章<a href="http://pcdeng.com/rust-libusb-gnu.html">《Windows 11 搭建 Rust USB 开发环境之 GNU 版》</a>,这篇主要总结一下安装 <code>msvc</code> 工具链的要点。</p> <h1>前置</h1> <ol> <li>Windows 11 系统</li> <li><a href="https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/">Microsoft C++ 生成工具</a>,我装的是 2022 版,还有一些依赖,如 <code>Windows 11 SDK</code>,参考 <a href="https://zhuanlan.zhihu.com/p/507532813">Windows上Rust所依赖的msvc到底怎么装?</a></li> <li>rust 及其相关工具,如 <code>cargo</code>、<code>rustup</code>、<code>rustc</code>。一般安装好 rust,这些工具都会包含在内。</li> <li>下载 <code>libusb</code> 编译好的库,我下载的是 <a href="https://github.com/libusb/libusb/releases/download/v1.0.26/libusb-1.0.26-binaries.7z">libusb-1.0.26</a></li> </ol> <h1>实战</h1> <p>项目根目录假定是 <code>D:\projects\rust\libusb-demo</code></p> <h2>创建一个 rust 项目</h2> <p>打开一个<code>命令提示符</code>后,执行以下命令</p> <pre><code class="language-cmd">cargo new libusb-demo cd libusb-demo cargo add libusb code .</code></pre> <h2>写调用 <code>libusb</code> 的测试代码</h2> <p>在 Visual Studio Code 编辑器中编辑 <code>src/main.rs</code></p> <pre><code class="language-rs">extern crate libusb; fn main() { let context = libusb::Context::new().unwrap(); for device in context.devices().unwrap().iter() { let device_desc = device.device_descriptor().unwrap(); println!("Bus {:03} Device {:03} ID {:04x}:{:04x}", device.bus_number(), device.address(), device_desc.vendor_id(), device_desc.product_id()); } }</code></pre> <h2>配置</h2> <ol> <li>解压 <code>libusb-1.0.26-binaries.7z</code> 得到 <code>libusb-1.0.26-binaries</code>。</li> <li>把 <code>libusb-1.0.26-binaries</code> 下的 <code>VS2015-x64</code> 复制到 <code>D:\projects\rust\libusb-demo</code> 下,并改名为 <code>libusb-x64</code></li> <li>在 <code>libusb-demo</code> 下新建 <code>.cargo/config.toml</code> <pre><code class="language-toml">// .cargo/config.toml [target.x86_64-pc-windows-msvc.'usb-1.0'] # rustc-link-search = ['D:\projects\rust\libusb-demo\libusb-x64\dll'] # 这种是动态链接方式 rustc-link-search = ['D:\projects\rust\libusb-demo\libusb-x64\lib'] # 这种是静态链接方式 rustc-link-lib = ['libusb-1.0']</code></pre></li> </ol> <h2>编译正式版</h2> <pre><code class="language-cmd">cargo build --release</code></pre> <h2>验证</h2> <h3>静态链接版</h3> <p>复制 <code>target\release\libusb-demo.exe</code> 到虚拟机里或另一台装有 Windows 的系统运行。 打开 <code>命令提示符</code>,进入 <code>C:\Users\Payton\Desktop</code></p> <pre><code class="language-cmd">C:\Users\Payton\Desktop>libusb-demo.exe Bus 003 Device 001 ID 0e0f:0003 Bus 001 Device 002 ID 0e0f:0002 Bus 003 Device 000 ID 15ad:0779 Bus 002 Device 000 ID 15ad:0770 Bus 003 Device 002 ID 0e0f:0002 Bus 003 Device 003 ID 0e0f:0002 Bus 001 Device 000 ID 15ad:0774</code></pre> <h3>动态链接版</h3> <p>复制 <code>target\release\libusb-demo.exe</code> 和 <code>libusb-x64\dll\libusb-1.0.dll</code> 到虚拟机里或另一台装有 Windows 的系统运行。 打开 <code>命令提示符</code>,进入 <code>C:\Users\Payton\Desktop</code>,</p> <ul> <li>如果没有复制 <code>libusb-1.0.dll</code></li> </ul> <pre><code class="language-cmd">C:\Users\Payton\Desktop>libusb-demo.exe</code></pre> <p>提示 <img src="//pcdeng.com/uploads/libusb-1.0.dll-not-found.jpg" alt="" /></p> <ul> <li>复制 <code>libusb-1.0.dll</code> <pre><code class="language-cmd">C:\Users\Payton\Desktop>libusb-demo.exe Bus 003 Device 001 ID 0e0f:0003 Bus 001 Device 002 ID 0e0f:0002 Bus 003 Device 000 ID 15ad:0779 Bus 002 Device 000 ID 15ad:0770 Bus 003 Device 002 ID 0e0f:0002 Bus 003 Device 003 ID 0e0f:0002 Bus 001 Device 000 ID 15ad:0774</code></pre></li> </ul> <h1>参考</h1> <p><a href="https://zhuanlan.zhihu.com/p/507532813">Windows上Rust所依赖的msvc到底怎么装?</a></p> <p><a href="https://github.com/dcuddeback/libusb-rs/issues/20">Linking on Windows #20</a></p> <h1>附</h1> <ol> <li>项目目录 <img src="//pcdeng.com/uploads/rust-libusb-demo.jpg" alt="" /></li> </ol>
详情
<p>我之前写过一篇文章<a href="http://pcdeng.com/tauri-printer.html">《基于 Tauri + React + Rust + libusb + ESC/POS 驱动打印机蜂鸣器》</a>,有网友评论说:</p> <blockquote> <p>现在又卡在了link上,无法打开输入文件usb-1.0.lib</p> </blockquote> <p>然后我在自己的电脑也复现了,但之前安装环境是稀里糊涂地装了一遍,有些地方是不清楚原理的。</p> <p>趁着今天休息的时间,在家里的电脑重新装了一遍。我只记录了一些我疑惑的点。</p> <p>具体如何安装的软件见文章底部的<code>参考</code>部分列出的链接。</p> <h1>需要的软件</h1> <h2>Rust 工具集。</h2> <ul> <li><code>rustup</code> 是 Rust 工具链的安装程序和更新程序。</li> <li><code>Cargo</code> 是 Rust 包管理工具的名称。</li> <li><code>rustc</code> 是 Rust 编译器。大多数情况下,你不会直接调用 <code>rustc</code>,而是通过 <code>Cargo</code> 间接调用它。</li> </ul> <h2>MSYS2</h2> <blockquote> <p>Mingw(Minimalist GNU for Windows)是一个开发工具集,提供了用于 Windows 环境下编译和运行 C、C++ 等程序的工具链。</p> <p>Mingw-w64 是 Mingw 的一个分支,专注于支持 64 位 Windows 系统。它提供了对 64 位 Windows 环境下各种 API 的支持。</p> <p>Msys(Minimal System)是一个轻量级的类 Unix 环境,它提供了一些基本的 Unix 工具和 shell 环境,用于在 Windows 系统上运行 Unix 程序。</p> <p>Msys2 是 Msys 的一个升级版,它提供了更丰富的软件包管理系统和更新的工具链,使得在 Windows 系统上使用Unix 程序变得更加方便。</p> <p>Cygwin 是另一个类 Unix 环境,它提供了一套完整的 GNU 工具链和一些 Unix 系统调用的实现,使得在 Windows 系统上可以运行大部分的 Unix 程序。</p> <p>这些工具之间的关系是:</p> <p>Mingw 和 Mingw-w64 提供了用于编译和运行 C、C++ 等程序的工具链;</p> <p>Msys 和 Msys2 提供了在 Windows 系统上运行 Unix 程序所需的环境和工具;</p> <p>Cygwin 则提供了一套完整的 GNU 工具链和 Unix 系统调用的实现,使得在 Windows 系统上可以运行大部分的Unix 程序。</p> <p>Msys 和 Msys2 都是基于 Cygwin 开发的,但它们的目标不同,Msys 和 Msys2 更加轻量级且专注于提供基本的Unix 环境和工具,而 Cygwin 则更加完整和强大。</p> </blockquote> <h3>pkg-config</h3> <blockquote> <p>pkg-config 是一个用于管理编译和链接时的依赖库的工具。它可以帮助开发人员在编译和链接时自动查找和配置所需的库文件和头文件。</p> <h3>libusb</h3> <p>用于访问 USB 设备的跨平台库。</p> </blockquote> <p>最后来一张图:</p> <p><img src="//pcdeng.com/uploads/rust-toolchain.jpg" alt="" /> 安装好以上的软件后。</p> <h1>检查一下软件信息</h1> <h2>rust 相关</h2> <pre><code>$ rustup --version rustup 1.26.0 (5af9b9484 2023-04-05) info: This is the version for the rustup toolchain manager, not the rustc compiler. info: The currently active `rustc` version is `rustc 1.74.1 (a28077b28 2023-12-04) $ cargo --version cargo 1.74.1 (ecb9851af 2023-10-18) $ rustup toolchain list stable-x86_64-pc-windows-gnu (default) stable-x86_64-pc-windows-msvc</code></pre> <p>可以看出 rust 我是基于 <code>GNU 工具链</code> 来编译的。安装 <code>gnu 工具链</code>,可以通过命令 <code>rustup toolchain install stable-gnu</code>;</p> <p>如果要安装 <code>msvc 工具链</code>,则 <code>rustup toolchain install stable-msvc</code></p> <p>设置默认工具链 <code>rustup default stable-gnu</code></p> <h2>MSYS2</h2> <p><code>Mingw-w64</code> 所在目录:<code>C:\msys64\mingw64</code></p> <pre><code>$ pkg-config --cflags --libs libusb-1.0 -IC:/msys64/mingw64/include/libusb-1.0 -LC:/msys64/mingw64/lib/libusb-1.0/static -lusb-1.0</code></pre> <p>请确保你电脑安装好这些软件,如果没有,请参考<code>参考</code>列出的链接。</p> <h1>配置 libusb</h1> <p>这里主要参考 <a href="https://qianchenzhumeng.github.io/posts/rust_usb_programming/">Rust USB 开发|前尘逐梦</a></p> <ol> <li><a href="https://github.com/libusb/libusb/releases?page=3">下载 libusb</a> 编译好的包。我下载的是 <code>libusb-1.0.20.7z</code></li> <li>解压后,进入 <code>libusb-1.0.20</code> 目录。</li> <li>复制 <code>include</code> 下的 <code>libusb-1.0</code> 文件夹到 <code>C:\msys64\mingw64\include</code> 下。</li> <li><code>C:\msys64\mingw64\include</code> 下新建 <code>libusb-1.0</code>。</li> <li>复制 <code>MinGW64</code> 下的 <code>dll</code> 和 <code>static</code> 文件夹到 <code>C:\msys64\mingw64\include\libusb-1.0</code>。</li> <li><code>C:\msys64\mingw64\lib\pkgconfig</code> 下新建 <code>libusb-1.0.pc</code></li> <li><code>libusb-1.0.pc</code> 内容,静态链接 libusb</li> </ol> <pre><code>prefix=c:\msys64\mingw64 exec_prefix=${prefix} includedir=${prefix}/include/libusb-1.0 libdir=${prefix}/lib/libusb-1.0/static Name: libusb Description: libusb Version: 1.0 Cflags: -I${includedir} Libs: -L${libdir} -lusb-1.0</code></pre> <h1>测试验证</h1> <h2>创建 rust 项目</h2> <pre><code class="language-bash">cargo new libusb-demo cd libusb-demo cargo add libusb code .</code></pre> <h2>调用 libusb</h2> <pre><code class="language-rs">// src/main.rs extern crate libusb; fn main() { let context = libusb::Context::new().unwrap(); for device in context.devices().unwrap().iter() { let device_desc = device.device_descriptor().unwrap(); println!( "Bus {:03} Device {:03} ID {:04x}:{:04x}", device.bus_number(), device.address(), device_desc.vendor_id(), device_desc.product_id() ); } } </code></pre> <h2>运行</h2> <pre><code class="language-bash">$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target\debug\libusb-demo.exe` Bus 001 Device 002 ID 413c:2113 Bus 001 Device 001 ID 413c:301a Bus 001 Device 002 ID 0bda:c829 Bus 001 Device 000 ID 8086:7ae0</code></pre> <h1>参考</h1> <p><a href="https://zhuanlan.zhihu.com/p/655386777">Windows 上安装 Rust 及配置</a></p> <p><a href="https://learn.microsoft.com/zh-cn/windows/dev-environment/rust/">在 Windows 上通过 Rust 进行开发</a></p> <p><a href="https://qianchenzhumeng.github.io/posts/rust_usb_programming/">Rust USB 开发|前尘逐梦</a></p> <p><a href="https://blog.csdn.net/dorlolo/article/details/131009754">windows下快速安装gcc、pkg-config的方法</a></p>
详情
<h1>需求</h1> <p>网页通过蓝牙发送 TSPL 指令到打印机。</p> <h1>前置条件</h1> <ul> <li>电脑需要有蓝牙</li> <li>低功耗蓝牙打印机(关键是低功耗)</li> </ul> <p align=center><img src="/uploads/bluetooth-device.jpg" alt="image.png" width="300" /></p> <p align=center>表1</p> <ul> <li>了解蓝牙基础概念和通讯原理</li> </ul> <h1>概念</h1> <h2>Web Bluetooth API</h2> <p>网络蓝牙 API 提供了与蓝牙低功耗外围设备连接和交互的能力。Web Bluetooth API 现在还是一项实验性的功能,用于生产环境需谨慎。</p> <p><img src="uploads/web-bluetooth-compatibility.jpg" alt="image.png" /></p> <h1>实现</h1> <h2>1. 配对</h2> <pre><code class="language-js">const pair = async () => { return navigator.bluetooth .requestDevice({ filters: [{ services: [SERVICE_UUID] }], }) .then((device) => { selectedDevice = device; return device; }) .catch((err) => { console.log(err.message); return ""; }); };</code></pre> <h2>2. 连接 GATT server 并获取 打印特性</h2> <pre><code class="language-js">const getPrintCharacteristic = async () => { if (!selectedDevice) { return Promise.reject(new Error("没有配对设备")); } if (printCharacteristic) { return Promise.resolve(printCharacteristic); } return Promise.resolve(selectedDevice) .then((device) => { console.log(`设备名称 ${device.name}`); console.log("连接到 GATT 服务器......"); return device.gatt.connect(); }) .then((server) => server.getPrimaryService(SERVICE_UUID)) .then((service) => service.getCharacteristic(CHARACTERISTIC_UUID)) .then((characteristic) => { printCharacteristic = characteristic; return characteristic; }); };</code></pre> <h2>3. 发送命令</h2> <pre><code class="language-js">const sendPrinterData = (cmd) => { if (!printCharacteristic) { console.log("无法打印:打印特性属性为空"); return; } const encoder = new TextEncoder("utf-8"); const text = encoder.encode(cmd); return printCharacteristic.writeValue(text).then(() => { console.log("发送完毕"); }); };</code></pre> <h1>完整代码(仅供参考)</h1> <pre><code class="language-html"><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Bluetooth 示例</title> </head> <body> <button id="connectBtn">连接</button> <button id="sendBtn">发送</button> <script src="./bluetooth.js"></script> </body> </html></code></pre> <pre><code class="language-js">// 以下这两个值,在标准的 UUID 文档中找不到。应该是设备制造商没有按标准执行,我是从 https://github.com/WebBluetoothCG/demos/blob/gh-pages/bluetooth-printer/index.html 获取的 const SERVICE_UUID = '000018f0-0000-1000-8000-00805f9b34fb'; // 打印服务的 UUID const CHARACTERISTIC_UUID = '00002af1-0000-1000-8000-00805f9b34fb'; // 写特性的 UUID let selectedDevice; let printCharacteristic; const sendPrinterData = (cmd) => { if (!printCharacteristic) { console.log("无法打印:打印特性属性为空"); return; } const encoder = new TextEncoder("utf-8"); const text = encoder.encode(cmd); return printCharacteristic.writeValue(text).then(() => { console.log("发送完毕"); }); }; const getPrintCharacteristic = async () => { if (!selectedDevice) { return Promise.reject(new Error("没有配对设备")); } if (printCharacteristic) { return Promise.resolve(printCharacteristic); } return Promise.resolve(selectedDevice) .then((device) => { console.log(`设备名称 ${device.name}`); console.log("连接到 GATT 服务器......"); return device.gatt.connect(); }) .then((server) => server.getPrimaryService(SERVICE_UUID)) .then((service) => service.getCharacteristic(CHARACTERISTIC_UUID)) .then((characteristic) => { printCharacteristic = characteristic; return characteristic; }); }; const handleSend = () => { getPrintCharacteristic() .then(() => { sendPrinterData("SELFTEST\r\n"); // 发送打印自检页 }) .catch((err) => { console.log("发送指令失败:", err.message); }); }; const pair = async () => { return navigator.bluetooth .requestDevice({ filters: [{ services: [SERVICE_UUID] }], }) .then((device) => { selectedDevice = device; return device; }) .catch((err) => { console.log(err.message); return ""; }); }; const handleConnect = async () => { const device = await pair(); console.log("配对设备:", device); }; const init = () => { const btn = document.getElementById("connectBtn"); btn.addEventListener("click", handleConnect); const sendBtn = document.getElementById("sendBtn"); sendBtn.addEventListener("click", handleSend); }; init();</code></pre> <h1>注意&问题</h1> <ol> <li>由于 Web Bluetooth 功能还在实验阶段,有些功能还需要打开特性开关,如 <code>Bluetooth.getDevices()</code> 接口,可以通过在浏览器地址输入 <code>chrome://flags/#enable-web-bluetooth-new-permissions-backend</code>,进入 <code>Experiments</code> 面板打开 <code>web-bluetooth-new-permissions-backend</code> 特性</li> <li><code>printCharacteristic.writeValue</code> 一次性只能最多发送 <code>512</code> 字节的数据,数据太大,比如发送图片,要分包发送,可以参考 <a href="https://github.com/WebBluetoothCG/demos/blob/gh-pages/bluetooth-printer/index.html">demo</a>,但是有些打印机一次最多发送的字节数比 <code>512</code> 小,最好是连接的打印机获取一次最多能发送的字节数。</li> <li>传输数据很慢,我测试过几台打印机,发现打印图片超级慢。因为图片的数据太大了,也可能是打印机性能不好。具体性能瓶颈卡在哪里还不知道怎样排查,如果你有这方面的经验,欢迎评论。</li> </ol> <h1>参考</h1> <p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Bluetooth">https://developer.mozilla.org/zh-CN/docs/Web/API/Bluetooth</a></p> <p><a href="https://zhuanlan.zhihu.com/p/20657057">https://zhuanlan.zhihu.com/p/20657057</a></p> <p><a href="https://blog.csdn.net/tanqth/article/details/108151033">https://blog.csdn.net/tanqth/article/details/108151033</a></p> <p><a href="https://github.com/WebBluetoothCG/demos">https://github.com/WebBluetoothCG/demos</a></p>
详情
<h1>需求</h1> <p>网页通过 USB 发送 ESC/POS 指令到打印机。</p> <h1>概念</h1> <h2>什么是 WebUSB?</h2> <blockquote> <p>WebUSB 是一个 Web API,它允许网页通过 USB 连接与本地的 USB 设备进行通信。通过 WebUSB,网页可以与各种类型的 USB 设备进行直接交互,而无需通过平台特定的驱动程序或中间件。</p> <p>使用 WebUSB,开发人员可以创建具有以下功能的网页应用程序:</p> <ul> <li>识别和连接可用的 USB 设备。</li> <li>与 USB 设备进行数据交换,包括读取和写入设备的数据端点。</li> <li>监听 USB 设备上的事件,例如设备连接和断开连接。</li> </ul> <p>由于 WebUSB 使用了 USB 设备的通用性标准,因此它可以与各种类型的设备进行通信,例如打印机、扫描仪、键盘、鼠标、游戏控制器等。这使得开发人员可以创建具有更高级别的交互和控制的 Web 应用程序,而无需依赖于特定平台或操作系统。</p> <p>需要注意的是,为了保护用户安全和隐私,WebUSB 需要用户的明确授权才能访问 USB 设备。用户将通过浏览器的权限提示决定是否允许网页应用程序与指定的 USB 设备进行通信。</p> </blockquote> <h2>什么是 ESC/POS 指令?</h2> <blockquote> <p>ESC/POS(Epson Standard Code for Printers)是一种打印机指令集,由爱普生(Epson)公司创建并广泛使用。ESC/POS 指令集包括一系列控制命令,以控制打印机的各种操作,例如打印文本、绘制条形码、切纸等等。它被应用到广泛的打印机应用程序中,例如收银系统、票据打印、咨询机等等。</p> <p>ESC/POS 指令集为打印机提供了许多有用和高级的特性。例如,它支持各种字体和字号、颜色、对齐方式、旋转、加粗、下划线、倾斜等功能,以及各种类型的条形码和二维码。此外,ESC/POS 指令集还支持自定义 logo 和图像,以及打印多个副本和自动切纸等功能。</p> <p>ESC/POS 命令是通过向打印机发送 ASCII 字符串来实现的。对于不同的打印机,其 ESC/POS 指令集可能非常相似,但也存在一些差异。因此,开发人员应该了解所使用打印机的文档以正确使用其 ESC/POS 指令集。</p> </blockquote> <h1>实战</h1> <p>流程:获取设备(配对,或者从已配对的列表中获取)-> 初始化(打开设备,选择配置,声明接口)-> 发送 ESC/POS 命令</p> <h2>配对</h2> <pre><code class="language-js">const getDevice = async () => { const device = await navigator.usb .requestDevice({ filters: [] }) .then((device) => { return device; }) .catch(() => { return null; }); return device; };</code></pre> <h2>初始化设备</h2> <pre><code class="language-js"> const initDevice = async (device) => { await device.open(); const { configurationValue, interfaces } = device.configuration; await device.selectConfiguration(configurationValue || 0); await device.claimInterface(interfaces[0].interfaceNumber || 0); return device; };</code></pre> <h2>发送 ESC/POS 指令</h2> <pre><code class="language-js">const sendCmd = async (device) => { const cmd = new Uint8Array([0x1f, 0x1b, 0x1f, 0x67, 0x00]); const { outEndpoint } = getEndpoint(device); device.transferOut(outEndpoint, cmd); };</code></pre> <h1>完整代码</h1> <pre><code class="language-html"><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebUSB 驱动打印机打印</title> </head> <body> <button id="connectBtn">连接</button> <button id="sendBtn">打印自检页</button> <script src="./usb.js"></script> </body> </html></code></pre> <pre><code class="language-js">let selectedDevice; // 当前选择的设备 const getEndpoint = (device) => { let inEndpoint = undefined; let outEndpoint = undefined; for (const { alternates } of device.configuration.interfaces) { const alternate = alternates[0]; const USB_PRINTER_CLASS = 7; if (alternate.interfaceClass !== USB_PRINTER_CLASS) { continue; } for (const endpoint of alternate.endpoints) { if (endpoint.type !== "bulk") { continue; } if (endpoint.direction === "in") { inEndpoint = endpoint.endpointNumber; } else if (endpoint.direction === "out") { outEndpoint = endpoint.endpointNumber; } } } return { inEndpoint, outEndpoint, }; }; const connect = async () => { let device = await getDevice(); if (!device) { return; } selectedDevice = device; await initDevice(device); }; const getDevice = async () => { const device = await navigator.usb .requestDevice({ filters: [] }) .then((device) => { return device; }) .catch(() => { return null; }); return device; }; const initDevice = async (device) => { await device.open(); const { configurationValue, interfaces } = device.configuration; await device.selectConfiguration(configurationValue || 0); await device.claimInterface(interfaces[0].interfaceNumber || 0); }; const sendCmd = async () => { if (!selectedDevice) { console.warn("请先配对设备"); return; } const cmd = new Uint8Array([0x1f, 0x1b, 0x1f, 0x67, 0x00]); const { outEndpoint } = getEndpoint(selectedDevice); selectedDevice.transferOut(outEndpoint, cmd); }; const init = () => { navigator.usb.addEventListener("connect", (e) => { console.log("新连上的设备", e.device); }); navigator.usb.addEventListener("disconnect", (e) => { console.log("断开的设备", e.device); }); const connectBtn = document.querySelector("#connectBtn"); connectBtn.addEventListener("click", connect); const sendBtn = document.querySelector("#sendBtn"); sendBtn.addEventListener("click", sendCmd); }; document.addEventListener("DOMContentLoaded", init);</code></pre>
详情
<p>最近项目需要生成条码,展示的条码字符需要支持自定义字体。</p> <p>实现其实好简单,翻看了一下 bwip-js 的文档就知道。我这里只想简单记录一下。</p> <p>我的项目依赖:</p> <ul> <li>React 18.2.0</li> <li>bwip-js 4.1.2</li> <li>antd 5.8.6</li> <li>fabricjs 5.3.0</li> </ul> <h1>先上效果:</h1> <p><img src="/uploads/bwip-js-load-font.png" alt="" /></p> <h2>再上代码:</h2> <p><code>App.tsx</code> (只包含关键部分)</p> <h2>1、导入</h2> <pre><code class="language-tsx">import { code128, loadFont } from "bwip-js/browser";</code></pre> <h2>2、添加条码</h2> <pre><code class="language-tsx">const board = new fabric.Canvas("board", { width: window.innerWidth - 160 - 200, height: window.innerHeight - 50 - 40 - 2 - 20, backgroundColor: "#fff", }); /** * 加载自定义字体 * * fontName 和 fontUrl 一般从字体列表获取而字体列表一般通过 API 获取。 * * @TODO 优化:记住加载过的字体,对同一个字体不需要重复下载,因为一般字体文件比较大,有些会达到 10M */ const loadCustomFont = async (fontName: string, fontUrl: string) => { const arrayBuffer = await fetch(fontUrl).then((res) => res.arrayBuffer()); const bytes = new Uint8Array(arrayBuffer); loadFont(fontName, bytes); }; /** * 添加条形码 */ const onAddBarcode = async () => { const fontName = "迷你简丫丫体"; // 这个名字不重要,重要的是下面的 ttf 文件, 4d44c61fbfb84e279b28adf1e1eb9b40.ttf 对应的字体就是“迷你简丫丫体” await loadCustomFont(fontName, "./4d44c61fbfb84e279b28adf1e1eb9b40.ttf"); const canvasEle = document.createElement("canvas"); const canvas = code128(canvasEle, { bcid: "code128", text: "遥遥领先", includetext: true, textfont: fontName, textyoffset: 3, }); const base64 = canvas.toDataURL("jpg", 1); const imgEle = await loadImg(base64); const fabricImg = new fabric.Image(imgEle); fabricImg.on("resizing", (evt) => { console.log(evt); }); fabricImg.on("scaling", (evt) => { console.log(evt); }); board.add(fabricImg); }; return <canvas id="board"></canvas></code></pre> <h1>参考</h1> <p><a href="http://bwip-js.metafloor.com/">http://bwip-js.metafloor.com/</a></p>
详情
<h1>前置</h1> <ol> <li>票据打印机(支持 ESC/POS 指令),需要内置蜂鸣器</li> <li>打印机连接到电脑</li> <li>Windows 11 系统</li> <li>使用 zadig 修改默认驱动。为什么要改默认驱动?<a href="https://github.com/libusb/libusb/wiki/Windows">看这里</a></li> <li>需要提前理解 usb 的 <code>vendor id</code>、<code>product_id</code>、<code>config</code>、<code>endpoint</code>、<code>interface number</code>、</li> <li>需要提前了解 usb 通信过程。</li> </ol> <p><img src="/uploads/tauri-printer-sys.svg" alt="image.svg" /></p> <h1>框图</h1> <p><img src="/uploads/tauri-printer-flow.png" alt="" /></p> <h1>UI</h1> <p><img src="/uploads/tauri-printer.png" alt="image.png" /></p> <h1>流程</h1> <ol> <li>前端页面加载完毕后,请求获取 usb 设备列表。</li> </ol> <pre><code class="language-tsx">useEffect(() => { getUsbDevices(); }, []); async function getUsbDevices() { invoke<UsbDevice[]>("get_usb_devices") .then((result) => { setUsbDevices(result); }) .catch((error) => { console.error("get usb list err", error); }); }</code></pre> <ol start="2"> <li>后端(Rust 端)初始化 usb 上下文并获取 usb 设备列表,然后根据 <code>vendor_id</code> 过滤出我们需要的 usb 列表,然后将 usb 设备的 <code>vendor_id</code>, <code>product_id</code>, <code>manufacturer_name</code>, <code>product_name</code> 等信息以 json 格式返回给前端。</li> </ol> <pre><code class="language-rs">#[tauri::command] async fn get_usb_devices() -> Vec<UsbDevice> { let context = libusb::Context::new().unwrap(); let mut usb_devices: Vec<UsbDevice> = Vec::new(); for device in context.devices().unwrap().iter() { if let Ok(d1) = read_usb_device(device) { usb_devices.push(d1); } } return usb_devices; } fn read_usb_device(device: Device) -> Result<UsbDevice, String> { let device_desc = device.device_descriptor().unwrap(); // 根据 vendor_id 过滤 if device_desc.vendor_id() != 0x1FC9 { return Err("not valid printer".into()); } // 构造返回给前端的 usb device 信息 let mut usb_device = UsbDevice { bus_number: device.bus_number(), address: device.address(), vendor_id: device_desc.vendor_id(), product_id: device_desc.product_id(), manufacturer_name: Some(String::from("unknow manufacturer name")), product_name: Some(String::from("unknow product name")), serial_number: Some(String::from("unknow serial number")), }; // 为了获取 usb 设备名称,或许有更好的方式获取,如果你知道,请记得给我留言。 match device.open() { Ok(handle) => { let timeout = Duration::from_secs(60); if let Ok(languages) = handle.read_languages(timeout) { if languages.len() > 0 { let language = languages[0]; usb_device.manufacturer_name = handle .read_manufacturer_string(language, &device_desc, timeout) .ok(); usb_device.product_name = handle .read_product_string(language, &device_desc, timeout) .ok(); usb_device.serial_number = handle .read_serial_number_string(language, &device_desc, timeout) .ok(); } } } Err(e) => { println!("can not open device: {:?}", e); // return Err("can not open device".into()); } } return Ok(usb_device); }</code></pre> <ol start="3"> <li>用户从 usb 设备列表下拉框中选中一个 usb 设备,点击“蜂鸣器”,此时前端会把 <code>vendor_id</code>, <code>product_id</code> 和使蜂鸣器响的指令(以<code>“27, 66, 2, 2”</code> 字符串的形式)这是 <code>ESC/POS</code> 指令,传给后端。</li> </ol> <pre><code class="language-tsx">const handleUsbBeep = async () => { const { vendor_id: vid, product_id: pid } = usbDevice || {}; try { const cmd = [27, 66, 2, 2].join(","); await invoke<string>("write_to_device", { pid, vid, cmdStr: cmd }); } catch (err: any) { message.error(err); } };</code></pre> <ol start="4"> <li>后端根据接收到 <code>vendor_id</code> 和 <code>product_id</code> 打开设备,然后设置激活的配置(调用DeviceHandle 上的 <code>set_active_configuration(1)</code>)和声明接口(调用 DeviceHandle 上的 <code>claim_interface(0)</code>)1 和 0,我这里是写死的,不同的 usb 设备,这两个值或许会不一样。</li> </ol> <pre><code class="language-rs">#[tauri::command] async fn write_to_device(pid: u16, vid: u16, cmd_str: &str) -> Result<String, String> { println!("received: {:04x}:{:04x}, {:?}", pid, vid, cmd_str); let context = libusb::Context::new().unwrap(); let usb_device = context.open_device_with_vid_pid(vid, pid); match usb_device { Some(b) => write(b, cmd_str), _ => panic!("can not open device"), } }</code></pre> <ol start="5"> <li>转换字符串命令为 <code>Vec<u8></code> 形式,然后通过 <code>write_bulk(endpoint, buf, timeout)</code> 方法发送到 usb 设备。注意 <code>endpoint</code> 我的这里写死的是 <code>3</code>,这个要根据 usb 设备来变。</li> </ol> <pre><code class="language-rs">fn str2u8(value: &str) -> u8 { if let Ok(v) = value.parse() { return v; } else { return 0; } } fn write(mut dh: DeviceHandle, cmd: &str) -> Result<String, String> { dh.set_active_configuration(1).unwrap(); if let Err(e) = dh.claim_interface(0) { println!("{}", e); return Err(e.to_string()); } else { // 字符串转 Vec<u8> let arr: Vec<&str> = cmd.split(",").collect(); let arr1: Vec<u8> = arr.iter().map(|value| str2u8(value)).collect(); if let Err(b) = dh.write_bulk(3, &arr1[..], Duration::new(60, 0)) { println!("{}", b); return Err(b.to_string()); } else { return Ok("执行成功".into()); } } }</code></pre> <h1>遇到的问题</h1> <ol> <li>一开始我是基于 <code>antd@5</code> 来做的,后来回家用 macOS 打包了,发现运行不起来,后来就把 <code>antd</code> 的版本降到 <code>4</code>。</li> <li>家里没有打印机,无法测试 macOS 版本的 app 是否正常。</li> <li>打包后,把安装包放到虚拟机里,发现缺少了 <code>libusb dll</code> 而导致程序无法正常运行,把 <code>libusb-1.0.dll</code> 拷贝到安装目录下就能正常运行。</li> <li>打包后,把安装包放到虚拟机(Windows 7)里运行,第一打开,由于系统没有安装 WebView2 运行时,安装程序会自动下载 WebView2 运行时。</li> <li>Windows <code>wix</code> 打包无法把 <code>libusb-1.0.dll</code> 打包进安装包,这个问题到现在也不知道怎样解决,如果你知道,请给我留言。</li> <li>Windows <code>nsis</code> 打包无法把 <code>Webviewloader.dll</code> 打包进安装包导致安装后无法运行。</li> </ol>
详情
<p>如果有客人到访,问你要 wifi 密码,此时你有忘记了,你该怎么办?</p> <p>本文就给你支个招,教你如何快速查看已连接过的 wifi 密码,亲测可用。</p> <p>先说答案:通过“钥匙串访问” app 查看。</p> <h1>操作步骤</h1> <p>1、打开“钥匙串访问” app。"启动台" ->"其它"->“钥匙串访问”</p> <p>2、找到已经连接过的 wifi 的名称。比如我的是“ChinaNet-3CCW_2.4G_plus”</p> <p>3、右键->将密码拷贝到剪贴板。</p> <p>4、输入当前登录用户的密码。</p> <p>5、输入当前的登录用户名和密码。</p> <p>6、打开一个文本编辑器 Command + V 复制。即可看到该 wifi 对应的密码。</p> <p><img src="/uploads/wifi.png" alt="" /></p>
详情
<h1>起因</h1> <p>最近接到一个项目,需要基于 Fabric.js 实现一个表格,百度、Bing、谷歌搜索了一番,没有找到合适的,无法直接白嫖,那只能自己撸起袖子干。</p> <h1>原理分析</h1> <p>一个表格由一个个单元格组成,如下图。</p> <p><img src="/uploads/fabric-1.jpg" alt="image1" /></p> <p>从第二列开始,单元格的左边和前一列的单元右边重合; 从第二行开始,单元格的上边和前一行的单元格的底边重合; 那么一个障眼法的表格就出来了。</p> <h1>任务拆解</h1> <h2>实现单元格</h2> <p>在 Fabric.js 内,单元格可以用一个 <code>Group</code> 包住 <code>Rect</code> 和 <code>Textbox</code> 来实现。其中单元格的边框由<code>Rect</code> 的边框实现,单元格里面文本由 <code>Textbox</code> 实现。当双击单元格时,使 <code>Textbox</code> 进入编辑模式。</p> <pre><code class="language-js">// table.js import { fabric } from 'fabric' // 定义自定义的单元格类 fabric.TableCell = fabric.util.createClass(fabric.Group, { type: 'tableCell', text: null, rect: null, initialize: function (rectOptions, textOptions, text) { this.rect = new fabric.Rect(rectOptions) this.text = new fabric.Textbox(text, { ...textOptions, selectable: false, }) this.on('mousedblclick', () => { this.selectable = false this.text.selectable = true this.canvas.setActiveObject(this.text) this.text.enterEditing() }) this.on('mousedown', () => { // this.rect.set('fill', 'green') // this.canvas.requestRenderAll() }) this.text.on('editing:exited', () => { this.text.selectable = false this.selectable = true }) this.callSuper('initialize', [this.rect, this.text], { subTargetCheck: true, objectCaching: false }) }, toObject: function () { return fabric.util.object.extend(this.callSuper('toObject'), { text: this.text.get('text') }) } })</code></pre> <h2>实现表格</h2> <p>一个表格由一个 <code>Group</code> 包裹 <code>rowHeights.length</code> * <code>columnWidths.length</code> 个 <code>TableCell</code> 组成。 由于每一列的宽度会不一样,所以要算出下一列的 <code>left</code> 值;同理每一行的高度也会不一样,同样需要算出下一行所有单元格的 <code>top</code> 值</p> <p>由于会有单元格合并的情况存在,所以用 <code>ignore</code> 记住哪些单元格是不用渲染的。</p> <pre><code class="language-js">// table.js fabric.Table = fabric.util.createClass(fabric.Group, { type: 'table', borderWidth: 4, // 单元格边框宽度 rowHeights: [], // 行高,每一行的高度可以自由配置 columnWidths: [], // 列宽,每一列的宽度可以自由配置 cells: [], // 每一个单元格。比较重要的字段有 row, col 分别表示行、列的索引,从 0 开始,rowSpan 代表单元格横跨的行数,colSpan 代表单元格横跨的列数,content 代表单元格内的文字 initialize: function (tableOptions) { const { left, top, borderWidth, rowHeights, columnWidths, cells } = tableOptions const rectOptions = { rx: 0, stroke: '#000', fill: 'transparent', shadow: 0, strokeWidth: +borderWidth, strokeUniform: true } const textOptions = { lineHeight: 1, fontSize: 20, stroke: '#000', fill: '#000', selectable: false, textAlign: 'center', editingBorderColor: '#FF9002' } const ignore = [] const cellCmps = [] let totalH = 0 for (let i = 0; i < rowHeights.length; i++) { let totalW = 0 for (let j = 0; j < columnWidths.length; j++) { let { rowSpan = 0, colSpan = 0, content } = cells.find((cell) => cell.row == i && cell.col == j) || {} const height = rowHeights[i] const width = columnWidths[j] rowSpan = Math.min(rowHeights.length - 1, rowSpan) colSpan = Math.min(columnWidths.length - 1, colSpan) let w = width let h = height let rowSpanHeight = 0 let colSpanHeight = 0 //@TODO 以下 3 个 for 有很大的优化空间。 for (let rs = 1; rs <= rowSpan; rs++) { rowSpanHeight += rowHeights[i + rs] ignore.push(`${i + rs}:${j}`) } for (let rs = 1; rs <= colSpan; rs++) { colSpanHeight += columnWidths[j + rs] ignore.push(`${i}:${j + rs}`) } if (rowSpan > 0 && colSpan > 0) { for (let r = 1; r <= rowSpan; r++) { for (let c = 1; c <= colSpan; c++) { ignore.push(`${i + r}:${j + c}`) } } } h += rowSpanHeight w += colSpanHeight if (ignore.includes(`${i}:${j}`)) { totalW += width continue } const cell = new fabric.TableCell( { ...rectOptions, left: totalW + 1, top: totalH, width: w, height: h }, { ...textOptions, left: totalW + 1 + rectOptions.strokeWidth, top: totalH + rectOptions.strokeWidth, width: w, height: h }, content || `${i}-${j}` ) cellCmps.push(cell) totalW += width } totalH += rowHeights[i] } this.borderWidth = borderWidth this.rowHeights = rowHeights this.columnWidths = columnWidths this.cells = cells this.callSuper('initialize', cellCmps, { subTargetCheck: true, objectCaching: false, left, top }) }, toObject: function () { return fabric.util.object.extend(this.callSuper('toObject'), { borderWidth: this.get('borderWidth'), rowHeights: this.get('rowHeights'), columnWidths: this.get('columnWidths'), cells: this.get('cells') }) } }) fabric.Table.fromObject = function (o, callback) { const options = { left: o.left, top: o.top, rowHeights: o.rowHeights, columnWidths: o.columnWidths, borderWidth: o.borderWidth, cells: o.cells, } const newTable = new fabric.Table(options) callback(newTable) }</code></pre> <h1>Demo</h1> <pre><code class="language-demo.js">import { fabric } from 'fabric' class PcEditor { canvas; constructor(canvasId) { this.init(canvasId); } /** * 初始化画板 * @param {string} canvasId */ init(canvasId) { const canvas = new fabric.Canvas(canvasId, { stopContextMenu: true, controlsAboveOverlay: true, preserveObjectStacking: true, altSelectionKey: "altKey", }); this.canvas = canvas; this.addTable() } addTable() { const options = { left: 20, top: 20, borderWidth: 4, rowHeights: [80, 120], columnWidths: [160, 100, 100], cells: [ { row: 0, col: 0, rowSpan: 0, colSpan: 2, content: "遥遥领先" }, { row: 0, col: 1, rowSpan: 0, colSpan: 0, content: "" }, { row: 0, col: 2, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 0, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 1, rowSpan: 0, colSpan: 0, content: "" }, { row: 1, col: 2, rowSpan: 0, colSpan: 0, content: "" } ] }; const canvas = this.canvas; const center = canvas.getCenter(); const table = new fabric.Table(options); table.set("left", center.left - table.width / 2); table.set("top", center.top - table.height / 2); canvas.add(table); } } new PcEditor("canvasBox");</code></pre> <p><img src="/uploads/fabric-js-2.jpg" alt="image2" /></p> <h1>扩展阅读</h1> <ul> <li><a href="https://juejin.cn/post/7143794070513516581">Fabric.js 自定义子类,创建属于自己的图形~</a></li> <li><a href="https://stackoverflow.com/questions/74248303/editable-table-with-fabricjs">Editable table with fabricjs</a></li> </ul>
详情
<h1>解决方案</h1> <p>先说解决方案:</p> <pre><code>import { encodingIndexes } from "@zxing/text-encoding/es2015/encoding-indexes"; import { TextEncoder } from "@zxing/text-encoding"; // @ts-ignore window.TextEncodingIndexes = { encodingIndexes: encodingIndexes }; // 如果没有引入 encodingIndexes,机会报错:Indexes missing. Did you forget to include encoding-indexes.js first? // 当然,我这里是比较粗暴的做法,可能有更好的引入 encodingIndexes 的方法 const encoder = new TextEncoder("gb2312", { NONSTANDARD_allowLegacyEncoding: true, }); const encoded = encoder.encode("中文"); console.log(encoded); // Uint8Array(2) [214, 208, buffer: ArrayBuffer(2), byteLength: 2, byteOffset: 0, length: 2, Symbol(Symbol.toStringTag): 'Uint8Array']</code></pre> <p>转为 16 进制为 D6 D0,查看 GB2312 编码表为 D6D0 中</p> <h1>问题</h1> <p>打印英文不存乱码的问题。 打印机支持 gb2312 编码,从浏览发送数据到打印机,需要编码为 gb2312,如果发送到打印的字节数组是用 TextEncoder 的 encode("中") 编码出来,打印机打出的是 “涓 ”。</p> <pre><code class="language-js">const encoder = new TextEncoder(); const encoded = encoder.encode(“中”); console.log(encoded); // Uint8Array(3) [228, 184, 173, buffer: ArrayBuffer(3), byteLength: 3, byteOffset: 0, length: 3, Symbol(Symbol.toStringTag): 'Uint8Array']</code></pre> <p>[228, 184, 173] 转为 十六进制 E4 B8 AD,查 GB2312 编码表 E4 B8 -> 涓,AD 没有编码。</p> <p>乱码的原因是:打印机支持 gb2312,但是接收到的却是 utf-8 编码的文字,所以就乱码了。</p> <p>解决方法:统一两端的编码。</p> <h1>参考</h1> <p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder">https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder</a></p> <p><a href="https://juejin.cn/post/6844903534144552967">https://juejin.cn/post/6844903534144552967</a></p> <p><a href="https://github.com/zxing-js/text-encoding">https://github.com/zxing-js/text-encoding</a></p> <p><a href="http://tools.jb51.net/table/gb2312">GB2312 编码表</a></p>
详情
<p>为了面试,搜索了一些关于 fabric.js 的资料,花了一天的时间,做了个原型,之后利用业余时间断断续续地完善了一些功能。 目前已基本可用,就想和大家分享一下成果。</p> <h1>概述</h1> <p>首先我们来看看它的全貌 <img src="/uploads/label-editor.png" alt="标签编辑器" /></p> <p>如果想体验 Live demo 可以访问直接 <a href="https://www.pcdeng.com/tag-editor/">https://www.pcdeng.com/tag-editor/</a></p> <p>界面主要分四部分。</p> <ul> <li>顶栏,是 fabric.js 画布的对象(下文统称为组件)通用属性设置栏。</li> <li>左侧是组件列表,点击之后会把组件添加到画布中;图标组件由于有分组,需要选中某个分组下的图标再点击,才会添加到画布中。</li> <li>中间灰色背景部分是画布,白色区域是可见区域,就是说,只有白色画板内的组件才会可见,打印时也只会打印白色区域。</li> <li>右侧是组件详细属性设置栏。当没有选中组件时,可以设置白色画板的大小。当有组件被选中时,展示的是选中组件特有的属性。</li> </ul> <h1>功能清单</h1> <ul> <li>支持在线字体</li> <li>离线可用(PWA)</li> <li>支持文字</li> <li>支持矩形</li> <li>支持线条</li> <li>支持条码</li> <li>支持二维码</li> <li>支持图标</li> <li>支持图片</li> <li>支持表格</li> <li>支持组合</li> <li>支持拆分组合</li> <li>支持图层</li> <li>支持单位转换(工具)</li> </ul> <h1>技术栈</h1> <p>前端技术栈:Vue3、Naive UI、Vite、fabric.js</p> <h1>参考</h1> <ul> <li><a href="https://github.com/nihaojob/vue-fabric-editor" target="_blank">vue-fabric-editor</a></li> <li><a href="https://juejin.cn/column/7050370347324932132" target="_blank">零基础入门Fabric.js,提高Canvas开发效率</a></li> </ul>
详情
<h1>概述</h1> <p>Chrome extension 又称 Chrome 插件,Chrome 扩展程序,为方便叙述,下文用“ Chrome 扩展”指代。你可以从<a href="https://chrome.google.com/webstore"> Chrome 应用商店</a>获取 <img src="/uploads/chrome-web-store.png" alt="" />,也可以离线包的方式安装,比如<a href="https://juejin.cn/extension?utm_source=jj_nav">掘金浏览器插件</a>,<a href="https://note.youdao.com/note-download">有道云网页剪报插件</a>,<a href="https://www.wetab.link/">Wetab</a></p> <h2>框图</h2> <p><img src="/uploads/chrome-extension.png" alt="框图" /></p> <p>我做的一个 <a href="https://chrome.google.com/webstore/detail/pos-printer-manager/ngmlmeaknfjiomiklcdjmoajhdompmii">插件</a> 已经上架到 Chrome 应用商店,它是一个打印机驱动程序。</p> <h2>Chrome 扩展能做什么</h2> <ul> <li>拦截网络请求,比如比较出名的 ad blocker</li> <li>调试工具,如 Vue Devtools,React Developer Tools,Angular Prober</li> <li>标签管理,如 WeTab,Bookmarks clean up</li> <li>打印机驱动,如 POS Printer manager,Zebra Printing</li> <li>截图,如 滚动屏幕截图工具和屏幕捕获</li> </ul> <h1>基本概念</h1> <p>Chrome 扩展目前最新的版本是 v3,网络上有些资料是 v2 的,阅读的时候要注意区分一下。</p> <ul> <li>manifest.json Chrome 扩展清单文件,关于 Chrome 扩展的有关配置都在这个文件内,比如名字,描述,版本,图标。其中 manifest_version 字段值为 3,如果是 2,是无法发布到 Chrome 应用商店的。</li> <li>Service workers Manifest V3 将 background pages 替换为 service worker。监听大部分浏览器事件然后做出处理,这些事件如 新打开标签页、关闭标签页、网络请求完成、删除书签、新增书签等等。</li> <li>popup html <img src="/uploads/webtab-popup-html.png" alt="" /></li> <li>content script 可以嵌入到每一个页面的脚本</li> </ul> <h1>开发一个简单扩展</h1> <p>前端开发中,和后端对接经常会遇到跨域的问题。解决方法要么在前端的 devServer 配置转发,要么要求后端配置允许跨域。这个插件实现的是修改响应头,模拟后端允许跨域。</p> <h2>创建一个 manifest.json</h2> <pre><code>// manifest.json {  "manifest_version": 3,  "name": "Proxy",  "description": "Enable CORS for each api reqeust",  "version": "1.0" }</code></pre> <h2>配置规则</h2> <pre><code>// manifest.json "declarative_net_request": { "rule_resources": [{ "id": "ruleset_1", "enabled": true, "path": "rules.json" }] }, "permissions": [ "declarativeNetRequest" ]</code></pre> <pre><code>// rules.json [ { "id": 1, "priority": 1, "condition": { "urlFilter": "*", "resourceTypes": ["xmlhttprequest", "script", "main_frame", "image"] }, "action": { "type": "modifyHeaders", "responseHeaders": [ { "header": "Access-Control-Allow-Origin", "operation": "set", "value": "*" } ] } } ]</code></pre> <h1>参考</h1> <p><a href="https://juejin.cn/post/7223315092858224695">Chrome扩展开发指南</a></p>
详情