作为一名身经百战的前端工程师,我见过各种各样的“奇葩”需求。但要说哪个需求最能让人体验到“浏览器与现实世界”的次元壁,那非“浏览器直接连接系统打印机”莫属了。这不,最近我们团队就接到了一个“甜蜜的负担”:为某电商系统开发一个高效、稳定的“面单打印”功能。
你可能要问了,打印?这不就是 window.print()
的事儿吗?Too young, too simple, sometimes naive! 如果只是简单的网页内容打印,window.print()
确实是"傻瓜式"的首选。但当需求涉及到:
- 指定打印机: 我要打到仓库的那个热敏打印机上,不是你办公室的激光打印机!
- 精确控制纸张和内容: 面单尺寸固定,内容排版一丝不苟,不能有半点偏差!
- 静默打印: 用户点一下就打,别给我弹什么打印预览框!
- 打印条码、二维码: 这些特殊元素可不是简单的图片,需要打印机原生支持才能保证清晰度!
这时,window.print()
就显得力不从心了。它就像一个只会喊“打印”的指挥官,至于具体怎么打、打到哪、打成啥样,它一概不知。
破壁者:Lodop横空出世
正当我们一籌莫展,准备祭出“后端生成 PDF 再下载打印”这种曲线救国方案时,一位“老司机”轻描淡写地抛出了一个名字——Lodop。
“Lodop?”我心里嘀咕,这名字听起来有点像某个古老的魔法咒语。然而,正是这个“咒语”,为我们打开了浏览器直连系统打印机的“魔法大门”。
Lodop 是什么?
简单来说,Lodop 是一款专业的 Web 打印控件/服务。它不是浏览器内置的功能,而是一个需要在客户端(用户的电脑上)安装的“小助手”。这个小助手扮演着“翻译官”的角色:
- 浏览器端: 你的前端 JavaScript 代码通过 Lodop 提供的 API,将打印指令(比如“在某个位置打印一段文字”、“打印一个条码”)发送给 Lodop。
- 客户端: Lodop 接收到这些指令后,将其“翻译”成操作系统和打印机能理解的语言,然后调用本地打印机进行打印。
这样一来,浏览器就绕过了 window.print()
的限制,获得了对本地打印机的“直接控制权”。
硬核解析:Lodop 的技术内幕
Lodop 的核心在于其客户端的组件。它提供了两种主要的工作模式:
- 传统 ActiveX 控件/NPAPI 插件模式: 早期浏览器(如 IE)通过 ActiveX 控件,其他浏览器通过 NPAPI 插件来加载 Lodop。这种方式现在已经被淘汰,因为现代浏览器出于安全和性能考虑,已经完全禁用了这类插件。
- C-Lodop Web 打印服务模式: 这是当前主流且推荐的使用方式。C-Lodop 是一个独立的本地服务(以后台进程形式运行),它通过 WebSocket 或 HTTP 与浏览器进行通信。无论使用 Chrome、Firefox、Edge 还是 Safari,只要 C-Lodop 服务在后台运行,浏览器就能通过标准的 Web 技术(WebSocket/HTTP)与其交互,从而实现打印功能。
我们项目中的 Lodop 集成,正是基于 C-Lodop 模式。接下来,我们结合实战代码,来一场 Lodop 的“源码探秘”。
(lodop封装库)
大量实战源码警告
1. Lodop 的“召唤术”:loadCLodop() 与 getLodop()
在 index.ts 中,我们看到了 Lodop 的初始化和获取逻辑。
// index.ts
// 用双端口加载主JS文件Lodop.js(或CLodopfuncs.js兼容老版本)以防其中某端口被占:
const MainJS = 'CLodopfuncs.js'
const URL_WS1 = 'ws://localhost:8000/' + MainJS // ws用8000/18000
const URL_WS2 = 'ws://localhost:18000/' + MainJS
const URL_HTTP1 = 'http://localhost:8000/' + MainJS // http用8000/18000
const URL_HTTP2 = 'http://localhost:18000/' + MainJS
const URL_HTTP3 = 'https://localhost.lodop.net:8443/' + MainJS // https用8000/8443
const LICENSE = '35561D0C4B35C5370C21686E788A9388AB3' // 你的 Lodop 注册码
// ... (其他变量和函数)
/**
* 加载Lodop对象主程函数。
* lodop主程方法。建议在项目初始化时就执行一次。
* 后续调用getLodop方法即可
* 此函数负责初始化和尝试连接到Lodop服务端的WebSocket,以加载Lodop打印服务。
* 如果环境不支持WebSocket或连接失败,则会尝试通过HTTP方式加载。
*/
export function loadCLodop() {
if (!needCLodop()) return // 判断是否需要 CLodop (IE 浏览器通常不需要)
if (checkCLodop()) return // 检查是否已加载
CLodopIsLocal = !!(URL_WS1 + URL_WS2).match(///localho|//127.0.0./i)
LoadJsState = 'loadingA'
if (!window.WebSocket && window.MozWebSocket) window.WebSocket = window.MozWebSocket // 兼容旧版 Firefox
// 尝试通过 WebSocket 连接 C-Lodop 服务
try {
const WSK1 = new WebSocket(URL_WS1)
WSK1.onopen = function () {
setTimeout(checkOrTryHttp, 200) // 200ms 后检查或尝试 HTTP
}
WSK1.onmessage = function (ev) {
if (!window.getCLodop) eval(ev.data) // 接收到 C-Lodop 的 JS 代码并执行
}
WSK1.onerror = function () {
// 第一个 WebSocket 端口失败,尝试第二个
const WSK2 = new WebSocket(URL_WS2)
WSK2.onopen = function () {
setTimeout(checkOrTryHttp, 200)
}
WSK2.onmessage = function (ev) {
if (!window.getCLodop) eval(ev.data)
}
WSK2.onerror = function () {
checkOrTryHttp() // 第二个也失败,直接尝试 HTTP
}
}
} catch (ev: any) {
checkOrTryHttp() // WebSocket 异常,直接尝试 HTTP
}
}
/**
* 获取LODOP对象主过程,判断是否安装、需否升级:
*/
export function getLodop(option: ILODOPOPtion = {}): ICLODOP | undefined {
// ... (各种提示信息字符串)
let LODOP
try {
// ... (判断浏览器和操作系统类型)
if (needCLodop() || isLinuxX86 || isLinuxARM) {
// 如果需要 CLodop (非 IE 或 Linux)
try {
LODOP = window.getCLodop?.() // 尝试获取 C-Lodop 实例
} catch (err) {}
// ... (检查加载状态,提示安装或升级)
if (!LODOP) {
// 如果 C-Lodop 未安装或未启动,则提示用户下载安装包
// ... (省略具体 DOM 操作,但会向页面插入提示信息和下载链接)
return
}
// ... (检查 C-Lodop 版本,提示升级)
} else {
// 如果不需要 CLodop (IE 浏览器)
// ... (通过 ActiveX 或 EMBED 标签创建 Lodop 实例)
// ... (检查 Lodop 插件版本,提示安装或升级)
}
// 设置软件产品注册信息
if (!LICENSESet) {
console.log('SET_LICENSES')
LICENSESet = true
;(LODOP as ICLODOP).SET_LICENSES(option.strCompanyName ?? '', LICENSE, option.strLicenseA || '', option.strLicenseB || '')
}
return LODOP as ICLODOP
} catch (err) {
alert('getLodop出错:' + err) // 错误处理
}
}
代码解读:
- 多端口加载策略: loadCLodop 函数通过 URL_WS1 和 URL_WS2 两个 WebSocket 端口,以及 URL_HTTP1、URL_HTTP2、URL_HTTP3 三个 HTTP 端口来加载 CLodopfuncs.js。这种"多管齐下"的策略是为了提高加载成功率,避免某个端口被占用导致服务无法连接。
- 动态加载 JS: 当 WebSocket 连接成功时,eval(ev.data) 会执行 C-Lodop 返回的 JavaScript 代码,其中包含了 window.getCLodop 方法。
- getLodop() 的职责: 这个函数是整个打印流程的"守门员"。它不仅负责获取 Lodop 实例(无论是 C-Lodop 还是旧版插件),还会智能地判断 Lodop/C-Lodop 是否已安装、版本是否需要升级,并给出相应的提示信息和下载链接。这大大简化了前端对打印环境的判断逻辑。
- 注册码: SET_LICENSES 方法用于设置 Lodop 的注册信息。这是一个商业软件,需要注册码才能去除水印或解锁全部功能。
2. 打印任务的“蓝图”:ITemplate 与 TemplateItem
在 type.ts 中,我们看到了打印任务的“蓝图”定义。
// type.ts
export interface ITemplate<T extends Record<string, any> = any> {
/** 模板名称 */
title: string
/** 绘图宽度尺寸 */
width: number
/** 绘图高度尺寸 */
height: number
/** 指定设备打印机 */
device?: string | number
/** 打印机纸张宽度 @unit 单位为 mm 毫米 */
pageWidth: number
/** 打印机纸张高度 @unit 单位为 mm 毫米 */
pageHeight: number
/** 模板数据 */
tempItems: TemplateItem<T>[]
// ... (其他属性)
}
export interface CommonTemplate<N = string> {
/** 数据字段名称 */
name: N
/** 数据字段值.可以插入{T]实现动态模板替换.需要注意的是。目前只支持单参数替换 */
value: string
/** 数据值. */
defaultValue?: string
/** 绘图宽度 */
width: number
/** 绘图高度 */
height: number
/** 绘图左偏移 */
left: number
/** 绘图上偏移 */
top: number
/** 样式 */
style: TempItemStyle
/** lodop样式 */
lodopStyle?: InnerTempItemStyle
// ... (其他属性)
}
export type TemplateItem<T extends Record<string, any> = any> =
| BraidTxtTemplate
| BetweenTxtTemplate
| CodeTemplate
| HtmlTemplate
| TableTemplate
| ImageTemplate
| ListTemplate<T>
export enum ETemplateItem {
/** 文本 */
Txt = 'text',
/** 两端对齐文本 */
BetweenText = 'between-text',
/** 码 */
Code = 'code',
/** html文本 */
Html = 'html',
/** 表格 */
Table = 'table',
/** 图片 */
Image = 'image',
/** 列表 */
List = 'list'
}
代码解读:
- ITemplate: 定义了整个打印任务的宏观配置,包括纸张尺寸 (pageWidth, pageHeight)、打印机 (device)、以及最重要的打印项集合 tempItems。
- CommonTemplate: 这是所有具体打印项的基础接口,包含了位置 (top, left)、尺寸 (width, height)、数据绑定 (name, value, defaultValue) 和样式 (style) 等通用属性。
- TemplateItem 与 ETemplateItem: TemplateItem 是一个联合类型,它定义了所有支持的打印项类型,例如普通文本 (Txt)、条码 (Code)、HTML 片段 (Html)、表格 (Table)、图片 (Image) 等。ETemplateItem 是一个枚举,用于标识这些类型。
- TempItemStyle: 详细定义了每个打印项的样式,例如字体大小 (FontSize)、颜色 (FontColor)、对齐方式 (Alignment),甚至还有 Lodop 特有的 AutoHeight(自动高度)和 LinkedItem(关联项)等属性,这些属性在处理动态内容(如列表、表格)时非常有用。
这套接口设计非常精妙,它将复杂的打印需求抽象成结构化的数据,使得前端可以像搭积木一样组合出各种复杂的打印内容。
3. 模板的“变形金刚”:_createLodopStyle() 与 _TempParser()
在 template.ts 和 index.ts 中,我们看到了如何将我们定义的“蓝图”转换为 Lodop 能理解的指令。
// template.ts
/**
* 将模板设计样式转换为lodop样式
* @param style 模板样式
* @returns lodop样式对象
*/
export function _createLodopStyle(style: TempItemStyle) {
const lodopStyle = {
zIndex: style.zIndex
}
for (const key in style) {
if (['Bold', 'Italic', 'Underline', 'ShowBarText'].indexOf(key) > -1) {
lodopStyle[key] = style[key] ? 1 : 0 // 布尔值转换为 0 或 1
} else if (key === 'Alignment') {
lodopStyle[key] = style[key] === 'left' ? 1 : style[key] === 'center' ? 2 : 3 // 字符串对齐方式转换为数字
} else {
lodopStyle[key] = style[key]
}
}
return lodopStyle as unknown as InnerTempItemStyle
}
// index.ts
/**
* 解析模板和数据生成打印项
* @param {*Array} tempItem 模板打赢项
* @param {Array} data 打印数据,
* @return {Array} 若data为null则返回处理后的模板
*/
function _TempParser(tempItem: TemplateItem[], data: any[] = []): TemplateItem[][] {
let temp = cloneDeep(tempItem) // 深度克隆,避免修改原始模板
// 处理对齐文本 (BetweenText)
temp = temp.reduce((result, item) => {
if (item.type === ETemplateItem.BetweenText) {
// 将 BetweenText 类型拆分为两个 Text 类型,一个左对齐,一个右对齐
const { type, data, style = {}, ...rest } = item
const splitsItems: any[] = item.data.map((da, index) => {
const info = {
...rest,
...da,
type: 'text',
style: index === 1 ? { ...style, Alignment: 'right' } : { ...style }
}
return info
})
return result.concat(splitsItems)
}
result.push(item)
return result
}, [] as TemplateItem[])
// 修改模板打印项顺序:将自适应高度的打印项放在第一项,并处理下方关联项
const flag = temp.findIndex((item) => item.style.AutoHeight)
if (flag !== -1) {
const autoItem = temp[flag]
temp.splice(flag, 1)
temp.unshift(autoItem)
// 处理位于自适应打印项下方的打印项,调整其 top/left 并添加 LinkedItem
temp.forEach((item) => {
if (item.top > autoItem.top && item.style.ItemType === 0) {
item.top = item.top - autoItem.top - autoItem.height + (autoItem.style.AutoHeightBottomMargin ?? 0)
item.left = item.left - autoItem.left + (autoItem.style.AutoHeightLeftMargin ?? 0)
item.style.LinkedItem = 1 // 关联到第一个打印项(自适应高度项)
}
})
}
if (!data.length) {
return [temp] // 如果没有数据,只返回处理后的模板
}
// 解析打印模板和数据,生成打印内容(数据绑定)
const tempContent: any[] = []
data.forEach((dataItem) => {
const conItem = temp.map((tempItem) => {
const item = cloneDeep(tempItem)
if (item.name) {
item.defaultValue = dataItem[item.name]
if (item.type === ETemplateItem.List) {
item.value = item.renderList(dataItem) // 列表类型调用 renderList
} else {
item.value = strTempToValue(item.value, item.defaultValue) // 字符串模板替换
}
}
return item
})
tempContent.push(conItem)
})
return tempContent
}
代码解读:
- 样式转换: _createLodopStyle 是一个"翻译官"中的"翻译官"。Lodop 的 API 对样式属性有特定的值要求(例如,布尔值用 0/1 表示,对齐方式用 1/2/3 表示),这个函数负责将我们友好的 TypeScript 接口定义转换成 Lodop 能理解的格式。
- 模板解析与数据绑定: _TempParser 是整个打印流程的核心逻辑之一。
- BetweenText 处理: 它巧妙地将 BetweenText(两端对齐文本,例如“商品金额: {amount}”)这种自定义类型,拆分成两个普通的 Text 类型,一个左对齐,一个右对齐,从而实现两端对齐的效果。
- AutoHeight 优先: 针对 AutoHeight(自动高度)的打印项(通常是列表或表格),它会将其调整到打印项数组的第一位,并调整其下方关联项的 top 和 left 属性,并设置 LinkedItem。这是 Lodop 实现动态高度和内容关联的关键。
- 数据填充: 最重要的是,它会遍历传入的数据 (data),将模板项中的 {} 占位符替换为实际的数据值,从而生成最终要打印的内容。对于 List 类型,它会调用 renderList 函数来生成复杂的 HTML 列表。
4. 打印项的“画笔”:_AddPrintItem()
在 index.ts 中,_AddPrintItem 负责将解析后的打印项添加到 Lodop 打印任务中。
// index.ts
/**
* 添加打印项
* @param {lodop} LODOP 打印实例
* @param {Object} printItem 打印项内容
* @param {Number} pageIndex 当前打印页的开始序号
*/
function _AddPrintItem(LODOP: ICLODOP, tempItem: TemplateItem, pageIndex = 0) {
const printItem = cloneDeep(tempItem)
const lodopStyle = _createLodopStyle(printItem.style) // 转换样式
// 批量打印时,修改关联打印项的关联序号
if (lodopStyle.LinkedItem === 1) {
lodopStyle.LinkedItem = 1 + pageIndex
}
const height = _calcPrintHeight(lodopStyle, printItem.type as any, printItem.height) // 计算高度
// 根据打印项类型调用不同的 Lodop API
switch (printItem.type) {
case ETemplateItem.Txt:
LODOP.ADD_PRINT_TEXT(printItem.top, printItem.left, printItem.width, height, printItem.value)
break
case ETemplateItem.Code:
LODOP.ADD_PRINT_BARCODE(printItem.top, printItem.left, printItem.width, height, lodopStyle.codeType, printItem.value)
break
case ETemplateItem.List:
case ETemplateItem.Html:
// List 和 Html 类型都通过 ADD_PRINT_HTM 添加 HTML 内容
{
const html = htmlTempTohtml(printItem.value ?? printItem.defaultValue ?? '', printItem.style)
LODOP.ADD_PRINT_HTM(printItem.top, printItem.left, printItem.width, height, html)
}
break
case ETemplateItem.Table:
// Table 类型通过 ADD_PRINT_TABLE 添加 HTML 表格
{
const html = tableTempTohtml(printItem.columns ? printItem.columns : [], printItem.defaultValue, printItem.style, printItem.tableHeadRender ?? true)
LODOP.ADD_PRINT_TABLE(printItem.top, printItem.left, printItem.width, height, html)
}
break
case ETemplateItem.Image:
// Image 类型通过 ADD_PRINT_IMAGE 添加图片
{
const html = imageTempTohtml(printItem.value)
LODOP.ADD_PRINT_IMAGE(printItem.top, printItem.left, printItem.width, height, html)
}
break
default:
}
// 设置打印项样式
Object.keys(lodopStyle).forEach((key) => {
LODOP.SET_PRINT_STYLEA(0, key, lodopStyle[key]) // SET_PRINT_STYLEA 用于设置当前添加的打印项的样式
})
// ... (设置默认 LodopStyle)
}
代码解读:
这个函数是 Lodop API 的"调用者"。它根据 printItem.type 的不同,调用 Lodop 实例 (LODOP) 对应的 ADD_PRINT_XXX 方法来添加打印内容:
- ADD_PRINT_TEXT:添加纯文本。
- ADD_PRINT_BARCODE:添加条码或二维码。
- ADD_PRINT_HTM:添加 HTML 内容。这对于打印复杂布局、富文本或自定义列表非常有用。
- ADD_PRINT_TABLE:专门用于添加 HTML 表格。
- ADD_PRINT_IMAGE:添加图片。
SET_PRINT_STYLEA(0, key, lodopStyle[key]) 则是用来设置刚刚添加的打印项的各种样式属性,例如字体、颜色、粗细等。
5. 打印任务的“执行官”:print() 与 preview()
在 index.ts 中,最终的打印和预览功能由 print 和 preview 函数提供。
// index.ts
function handlePrintOrPreview(temp: ITemplate, data) {
const LODOP = _CreateLodop(temp) // 初始化打印任务
if (!LODOP) return
const tempItems = cloneDeep(temp.tempItems)
const printContent = _TempParser(tempItems, data) // 解析模板和数据
if (printContent.length > 1) {
// 打印多份
printContent.forEach((aPrint, index) => {
LODOP.NewPageA() // 新建页面
aPrint.forEach((printItem) => {
_AddPrintItem(LODOP, printItem, index) // 添加打印项
})
})
} else {
// 单份
printContent[0].forEach((printItem) => {
_AddPrintItem(LODOP, printItem)
})
}
return LODOP
}
/**
* 打印功能
*/
export function print(temp: ITemplate, data) {
const LODOP = handlePrintOrPreview(temp, data)
if (!LODOP) return
return LODOP.PRINT() // 调用 Lodop 的 PRINT 方法
}
/**
* 打印预览功能
*/
export function preview(temp: ITemplate, data) {
const LODOP = handlePrintOrPreview(temp, data)
if (!LODOP) return
return LODOP.PREVIEW() // 调用 Lodop 的 PREVIEW 方法
}
代码解读:
handlePrintOrPreview 函数封装了创建 Lodop 实例、初始化打印任务 (_CreateLodop)、解析模板和数据 (_TempParser) 以及添加所有打印项 (_AddPrintItem) 的通用逻辑。
- _CreateLodop: 这个函数会调用 LODOP.PRINT_INITA() 来初始化一个打印任务,并设置打印区域的尺寸和名称。它还会通过 LODOP.SET_PRINT_PAGESIZE() 设置纸张的物理尺寸(毫米),并通过 LODOP.SET_PRINTER_INDEX() 来指定要使用的打印机。
- LODOP.NewPageA(): 如果需要打印多份(printContent.length > 1),则会为每份内容调用 NewPageA() 来创建新的打印页面。
- 最后,print() 函数调用 LODOP.PRINT() 来直接启动打印,而 preview() 函数调用 LODOP.PREVIEW() 来显示打印预览界面。
6. 实用工具集:utils.ts
utils.ts 提供了一些辅助函数:
- needCLodop(): 这个函数通过判断 navigator.userAgent 来决定当前浏览器是否需要加载 C-Lodop。例如,IE 浏览器通常可以直接使用 Lodop 插件,而 Chrome、Firefox 等现代浏览器则需要 C-Lodop 服务。
- cloneDeep(): 深度克隆函数。在处理复杂的模板对象和数据时,为了避免直接修改原始数据导致意外副作用,深度克隆是必不可少的。
7. 业务场景实战:cartTemplates.ts 与 usePrint.tsx
这两个文件展示了 Lodop 在实际业务中的应用。
// cartTemplates.ts
export const cartTemplates = (goods: ResPreViewOrderGoodsByPosDTO[] = []) => {
const tempItems: TempItems = [
{
height: 40,
title: '门店名称',
name: 'shop',
value: '{shop}',
style: { FontSize: 9 }
},
// ... (其他固定文本项,如手机号、下单时间、订单号等)
]
// 加入商品打印列 (动态生成商品明细)
goods.forEach((good) => {
const { goodsName = '', skuNo = '', specNames = '', goodsSalePrice = 0, quantity = 0, goodsAmount = 0 } = good
tempItems.push(
{
height: 40,
name: '',
title: goodsName,
value: goodsName,
style: { FontSize: 9 }
},
{
height: 20,
name: '',
title: skuNo,
value: skuNo,
style: { FontSize: 7 }
},
{
type: 'between-text', // 使用两端对齐文本
height: 20,
name: '',
data: [
{ title: '', name: 'amount', value: `${specNames} *${quantity ?? 0} ${mmCurrenty(goodsSalePrice, { precision: 2 })}` },
{ title: '', name: 'amount', value: `${mmCurrenty(goodsAmount, { precision: 2 })}` }
],
style: { FontSize: 7 }
}
)
})
const pageHeight = mmAdds(90, mmTimes(goods.length, 22)) // 根据商品数量动态计算纸张高度
const tpl: Temp = {
title: '购物车小票',
width: 180,
height: 1600, // 初始高度可以大一点,实际会根据内容调整
pageWidth: 48, // 毫米
pageHeight, // 毫米
tempItems: tempItems.concat(
// ... (底部汇总信息,如商品金额、优惠金额、实收款等,同样使用 between-text)
)
}
return generateLodopTemplate<ICartData>(tpl as any) as ITemplate<ICartData>
}
代码解读:
cartTemplates.ts 完美展示了如何利用 Lodop 的模板机制来构建复杂的打印内容。
- 静态与动态结合: 它定义了小票的固定部分(如门店名称、订单信息),并通过 goods.forEach 循环动态生成商品明细部分。
- between-text 的妙用: 在商品明细和底部汇总部分,大量使用了 type: 'between-text' 来实现左右对齐的排版,这在小票打印中非常常见。
- 动态纸张高度: pageHeight = mmAdds(90, mmTimes(goods.length, 22)) 这行代码根据商品数量动态计算打印纸张的高度,确保所有商品都能完整打印,这对于热敏小票打印尤为重要。
- generateLodopTemplate: 最后,通过 generateLodopTemplate 函数将所有模板项和整体配置组合成一个完整的 Lodop 打印模板。
// usePrint.tsx (React Hook)
export function usePrint() {
const [prints, setPrints] = useState<Prints[]>([]) // 存储已安装的打印机列表
// ... (printSnap 来自 Valtio 状态管理,用于获取当前选中的打印机)
/**
* 获取已安装的打印设备列表。
*/
function getPrintDevices() {
if (!checkIsInstall(true)) {
return
}
const LODOP = getLodop()
const counts = LODOP!.GET_PRINTER_COUNT() ?? 0 // 获取打印机数量
const prs: Prints[] = []
for (let index = 0; index < counts; index++) {
const value = LODOP!.GET_PRINTER_NAME(index) // 获取打印机名称
prs.push({ label: value, value })
}
setPrints(prs)
return prs
}
/**
* 检查lodops是否安装
*/
function checkIsInstall(silent = false) {
const info = getLodopInfo() // 获取 Lodop 信息
if (!info) {
Modal.warning({
title: '提示',
content: <span>lodop未安装。请先安装并启动Lodop服务。</span>,
okText: '下载安装',
closable: true,
onOk() {
window.open('https://shop/CLodop_Setup_for_Win32NT.exe') // 提供下载链接
}
})
return false
}
if (!silent && info.MESSAGE) {
Modal.success({ title: '提示', content: info.MESSAGE })
}
return true
}
/**
* 预览
*/
const preview = (temp: ITemplate, data: any) => {
if (checkIsInstall(true)) {
PREVIEW({ ...temp, device: printSnap.device }, data) // 调用 PREVIEW 函数,并传入当前选中的打印机
}
}
/**
* 打印
*/
const print = (temp: ITemplate, data: any) => {
if (checkIsInstall(true)) {
PRINT({ ...temp, device: printSnap.device }, data) // 调用 PRINT 函数,并传入当前选中的打印机
}
}
return {
prints,
getPrintDevices,
print,
preview,
checkIsInstall
}
}
代码解读:
usePrint.tsx 是一个 React Hook,它将 Lodop 的功能封装成了一个可复用的前端逻辑。
- 打印机列表: getPrintDevices 函数通过 LODOP.GET_PRINTER_COUNT() 和 LODOP.GET_PRINTER_NAME(index) 获取当前系统所有已安装的打印机列表,并将其存储在 prints 状态中,供用户选择。
- 安装检查与提示: checkIsInstall 函数在每次打印或预览前都会被调用,它通过 getLodopInfo() 来检查 Lodop/C-Lodop 的安装状态。如果未安装,会弹出一个 Ant Design 的 Modal 提示用户下载安装包,并提供下载链接。这种用户友好的提示机制非常重要。
- 集成与调用: preview 和 print 函数则简单地调用了 index.ts 中导出的 PREVIEW 和 PRINT 函数,并将当前选中的打印机设备 (printSnap.device) 传递给模板。
挑战与思考:打印魔法的代价
尽管 Lodop 解决了浏览器打印的诸多痛点,但它并非没有“代价”:
- 客户端安装: 这是最大的“门槛”。用户首次使用时需要下载并安装 Lodop/C-Lodop 服务。虽然 getLodop() 提供了友好的提示和下载链接,但对于一些不熟悉电脑操作的用户来说,这仍然可能是一个挑战。
- 安全性考量: Lodop 能够直接访问本地打印机,这意味着它拥有较高的权限。因此,确保 Lodop 服务的安全性至关重要,应从官方渠道下载,并关注其版本更新。
- 维护与兼容性: 随着浏览器和操作系统的不断更新,Lodop 也需要持续维护以保证兼容性。虽然它已经做得很好,但作为开发者,我们仍需关注其官方动态。
- 模板设计: 尽管代码中提供了强大的模板解析能力,但设计复杂的打印模板本身就是一项细致的工作,需要精确计算每个打印项的位置和尺寸。
- 商用付费: 虽然 Lodop 提供了免费的 C-Lodop 云打印服务,但商业应用需要购买注册码以获得更多功能(去除测试版水印等)和支持。
结语:当魔法照进现实
通过 Lodop,我们成功地为电商系统实现了高效、精确的面单打印功能。它就像一座桥梁,将看似封闭的浏览器与现实世界的物理打印机连接起来,让前端开发者也能施展“打印魔法”。
虽然它需要客户端安装,并且在模板设计上需要一些细致的投入,但对于那些对打印有高要求、需要精确控制打印内容的 Web 应用来说,Lodop 无疑是一个强大且成熟的解决方案。它让那些曾经“不可能”的需求,变成了触手可及的“可能”。所以,下次再遇到浏览器打印的“硬骨头”,不妨试试 Lodop 这个“魔法咒语”吧!
相关链接
官方资源
- Lodop 官方网站 - Lodop 打印控件官方主页
- C-Lodop 云打印服务 - C-Lodop 下载和文档
- Lodop 开发文档 - 官方开发文档和示例
技术社区
- GitHub 相关项目 - 开源项目和示例代码
替代方案参考
- Print.js - 轻量级前端打印库
- jsPDF - 客户端 PDF 生成库
- Puppeteer - 无头浏览器打印方案
- html2canvas - HTML 转图片打印方案
浏览器打印相关
- MDN Print API - 浏览器原生打印 API
- CSS Print Media Queries - CSS 打印样式指南
- Page Break Properties - CSS 分页控制