浏览器中的打印魔法:Lodop与系统打印机

0 阅读18分钟

作为一名身经百战的前端工程师,我见过各种各样的“奇葩”需求。但要说哪个需求最能让人体验到“浏览器与现实世界”的次元壁,那非“浏览器直接连接系统打印机”莫属了。这不,最近我们团队就接到了一个“甜蜜的负担”:为某电商系统开发一个高效、稳定的“面单打印”功能。

你可能要问了,打印?这不就是 window.print() 的事儿吗?Too young, too simple, sometimes naive! 如果只是简单的网页内容打印,window.print() 确实是"傻瓜式"的首选。但当需求涉及到:

  • 指定打印机: 我要打到仓库的那个热敏打印机上,不是你办公室的激光打印机!
  • 精确控制纸张和内容: 面单尺寸固定,内容排版一丝不苟,不能有半点偏差!
  • 静默打印: 用户点一下就打,别给我弹什么打印预览框!
  • 打印条码、二维码: 这些特殊元素可不是简单的图片,需要打印机原生支持才能保证清晰度!

这时,window.print() 就显得力不从心了。它就像一个只会喊“打印”的指挥官,至于具体怎么打、打到哪、打成啥样,它一概不知。

破壁者:Lodop横空出世

正当我们一籌莫展,准备祭出“后端生成 PDF 再下载打印”这种曲线救国方案时,一位“老司机”轻描淡写地抛出了一个名字——Lodop

“Lodop?”我心里嘀咕,这名字听起来有点像某个古老的魔法咒语。然而,正是这个“咒语”,为我们打开了浏览器直连系统打印机的“魔法大门”。

Lodop 是什么?

简单来说,Lodop 是一款专业的 Web 打印控件/服务。它不是浏览器内置的功能,而是一个需要在客户端(用户的电脑上)安装的“小助手”。这个小助手扮演着“翻译官”的角色:

  1. 浏览器端: 你的前端 JavaScript 代码通过 Lodop 提供的 API,将打印指令(比如“在某个位置打印一段文字”、“打印一个条码”)发送给 Lodop。
  2. 客户端: 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) // 错误处理  
  }  
}

代码解读:

  1. 多端口加载策略: loadCLodop 函数通过 URL_WS1 和 URL_WS2 两个 WebSocket 端口,以及 URL_HTTP1、URL_HTTP2、URL_HTTP3 三个 HTTP 端口来加载 CLodopfuncs.js。这种"多管齐下"的策略是为了提高加载成功率,避免某个端口被占用导致服务无法连接。
  2. 动态加载 JS: 当 WebSocket 连接成功时,eval(ev.data) 会执行 C-Lodop 返回的 JavaScript 代码,其中包含了 window.getCLodop 方法。
  3. getLodop() 的职责: 这个函数是整个打印流程的"守门员"。它不仅负责获取 Lodop 实例(无论是 C-Lodop 还是旧版插件),还会智能地判断 Lodop/C-Lodop 是否已安装、版本是否需要升级,并给出相应的提示信息和下载链接。这大大简化了前端对打印环境的判断逻辑。
  4. 注册码: 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'  
}

代码解读:

  1. ITemplate: 定义了整个打印任务的宏观配置,包括纸张尺寸 (pageWidth, pageHeight)、打印机 (device)、以及最重要的打印项集合 tempItems。
  2. CommonTemplate: 这是所有具体打印项的基础接口,包含了位置 (top, left)、尺寸 (width, height)、数据绑定 (name, value, defaultValue) 和样式 (style) 等通用属性。
  3. TemplateItem 与 ETemplateItem: TemplateItem 是一个联合类型,它定义了所有支持的打印项类型,例如普通文本 (Txt)、条码 (Code)、HTML 片段 (Html)、表格 (Table)、图片 (Image) 等。ETemplateItem 是一个枚举,用于标识这些类型。
  4. 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  
}

代码解读:

  1. 样式转换: _createLodopStyle 是一个"翻译官"中的"翻译官"。Lodop 的 API 对样式属性有特定的值要求(例如,布尔值用 0/1 表示,对齐方式用 1/2/3 表示),这个函数负责将我们友好的 TypeScript 接口定义转换成 Lodop 能理解的格式。
  2. 模板解析与数据绑定: _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 的模板机制来构建复杂的打印内容。

  1. 静态与动态结合: 它定义了小票的固定部分(如门店名称、订单信息),并通过 goods.forEach 循环动态生成商品明细部分。
  2. between-text 的妙用: 在商品明细和底部汇总部分,大量使用了 type: 'between-text' 来实现左右对齐的排版,这在小票打印中非常常见。
  3. 动态纸张高度: pageHeight = mmAdds(90, mmTimes(goods.length, 22)) 这行代码根据商品数量动态计算打印纸张的高度,确保所有商品都能完整打印,这对于热敏小票打印尤为重要。
  4. 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 的功能封装成了一个可复用的前端逻辑。

  1. 打印机列表: getPrintDevices 函数通过 LODOP.GET_PRINTER_COUNT() 和 LODOP.GET_PRINTER_NAME(index) 获取当前系统所有已安装的打印机列表,并将其存储在 prints 状态中,供用户选择。
  2. 安装检查与提示: checkIsInstall 函数在每次打印或预览前都会被调用,它通过 getLodopInfo() 来检查 Lodop/C-Lodop 的安装状态。如果未安装,会弹出一个 Ant Design 的 Modal 提示用户下载安装包,并提供下载链接。这种用户友好的提示机制非常重要。
  3. 集成与调用: preview 和 print 函数则简单地调用了 index.ts 中导出的 PREVIEW 和 PRINT 函数,并将当前选中的打印机设备 (printSnap.device) 传递给模板。

挑战与思考:打印魔法的代价

尽管 Lodop 解决了浏览器打印的诸多痛点,但它并非没有“代价”:

  1. 客户端安装: 这是最大的“门槛”。用户首次使用时需要下载并安装 Lodop/C-Lodop 服务。虽然 getLodop() 提供了友好的提示和下载链接,但对于一些不熟悉电脑操作的用户来说,这仍然可能是一个挑战。
  2. 安全性考量: Lodop 能够直接访问本地打印机,这意味着它拥有较高的权限。因此,确保 Lodop 服务的安全性至关重要,应从官方渠道下载,并关注其版本更新。
  3. 维护与兼容性: 随着浏览器和操作系统的不断更新,Lodop 也需要持续维护以保证兼容性。虽然它已经做得很好,但作为开发者,我们仍需关注其官方动态。
  4. 模板设计: 尽管代码中提供了强大的模板解析能力,但设计复杂的打印模板本身就是一项细致的工作,需要精确计算每个打印项的位置和尺寸。
  5. 商用付费: 虽然 Lodop 提供了免费的 C-Lodop 云打印服务,但商业应用需要购买注册码以获得更多功能(去除测试版水印等)和支持。

结语:当魔法照进现实

通过 Lodop,我们成功地为电商系统实现了高效、精确的面单打印功能。它就像一座桥梁,将看似封闭的浏览器与现实世界的物理打印机连接起来,让前端开发者也能施展“打印魔法”。

虽然它需要客户端安装,并且在模板设计上需要一些细致的投入,但对于那些对打印有高要求、需要精确控制打印内容的 Web 应用来说,Lodop 无疑是一个强大且成熟的解决方案。它让那些曾经“不可能”的需求,变成了触手可及的“可能”。所以,下次再遇到浏览器打印的“硬骨头”,不妨试试 Lodop 这个“魔法咒语”吧!

相关链接

官方资源

技术社区

替代方案参考

浏览器打印相关