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

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

你可能要问了,打印?这不就是 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 的初始化和获取逻辑。

typescript 复制代码
// 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 中,我们看到了打印任务的"蓝图"定义。

typescript 复制代码
// 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 能理解的指令。

typescript 复制代码
// 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 打印任务中。

typescript 复制代码
// 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 函数提供。

typescript 复制代码
// 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 在实际业务中的应用。

typescript 复制代码
// 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 打印模板。
typescript 复制代码
// 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 这个"魔法咒语"吧!

相关链接

官方资源

技术社区

替代方案参考

浏览器打印相关

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax