uniapp-x 生成二维码与条形码:UTS 原生插件封装实践
2026/9/8 8:28:41 网站建设 项目流程

1. 先说结论:uniapp-x 里生成码,为什么绕开了纯 JS 方案

先交代下背景。我最近在做一个仓储管理 App,需求很直白:把货架上的货品编号生成条形码,贴在箱子上;同时生成二维码,里面存 SKU 和批次信息,方便仓管员扫一下就能看到货品详情。项目用的就是 uniapp-x,也就是 DCloud 新一代的跨平台开发框架,很多人习惯写成 uniapp-x,官方文档里叫 uni-app x。

这个需求听起来简单,但真做起来比想象中麻烦。原因在于 uniapp-x 和传统的 uni-app 不完全是一回事。旧版 uni-app 的 App 端本质上是 WebView 套壳,前端跑的是 Web 那套环境,所以网上大量 qrcode.js、bwip-js 这类纯 JS 库可以直接拿过来用,canvas 画一画就出图了。但 uniapp-x 的逻辑层和视图层都是自研或原生编译的,不再天然兼容浏览器 API。它支持 Vue3 和 TS 语法,但底层没有 DOM、没有 window,你指望拿一段“在浏览器里跑得好好的”二维码生成脚本直接塞进去,大概率会报各种稀奇古怪的错。

我一开始没意识到这个问题,图省事直接 npm 装了一个 qrcode 库,结果一编译就发现 canvas 相关的 API 拿不到。后来换思路,试过用 canvas 组件手动画码,uniapp-x 的 canvas 接口和 Web 端的 Canvas API 有很多细节不一样,画一个二维码需要把矩阵点一个个绘制出来,性能先不说,光是逐点控制坐标就很折磨人,而且条形码的编码规则比二维码复杂得多,手写 Code128 的编码表是个大工程,完全不划算。

所以如果你现在问我 uniapp-x 生成条形码和二维码到底怎么搞,我会直接建议:别在纯前端层面硬碰硬,最稳的是用 UTS 插件封装原生代码,Android 端走 Zxing,iOS 端走 CoreImage 的 CIFilter。这两套都是各自平台系统级的成熟方案,性能和兼容性都有保障。当然,我知道不是所有人都愿意碰原生代码,所以备选方案我也会在后面展开。

2. 方案选型对比:UTS 原生插件、web-view 绕路、后端出图到底怎么选

在动手之前,我把市面上常见做法列了一个对比表,这里直接贴出来,方便你做决策。

方案实现思路优点缺点适合场景
UTS 原生插件方案封装 Android Zxing / iOS CoreImage,通过 uni_modules 暴露给前端双端原生能力,性能好,支持二维码和条码多种编码,图片质量可控需要写 Kotlin / Swift 代码,对纯前端同学有门槛大多数正式项目,尤其是对图片尺寸、清晰度、批量生成有要求的场景
web-view 绕路方案在 uniapp-x 页面里嵌 web-view,加载本地 HTML,HTML 里用 JS 库生成二维码图片,再通过 postMessage 或图片回传前端代码量少,JS 库生态丰富,不用碰原生web-view 加载和通信有延迟,生成大量码时体验一般,条形码的清晰度受限于渲染尺寸临时演示、工具类小应用、不想引入原生依赖的场景
后端生成图片方案服务端用 Zxing 或 Java 库生成 Base64 图片,App 直接请求拿到图片展示前端最简单,逻辑全部在后端,方便后期统一样式和水印依赖网络,弱网环境体验差,批量场景请求量大,而且本质上没解决“App 端自主生成”的问题网络稳定、有现成后端的内部系统
Canvas 纯前端绘制解析二维码矩阵或条码编码规则,用 uniapp-x 的 canvas 组件逐格绘制不依赖原生,也不依赖网络编码规则实现复杂,Canvas 接口适配成本高,性能堪忧练手项目,或者只生成最简单的 QRCode 且能容忍性能问题

我最终选了 UTS 原生插件方案,原因有三点。

第一,这条链路是 DCloud 官方推荐的扩展方式,后续 HBuilderX 升级、uniapp-x 版本迭代,兼容性不会突然断裂。UTS 插件编译后直接打进 App 原生工程,不经过 WebView 的中间层,稳定性最强。

第二,Zxing 和 CoreImage 对二维码和条形码的支持都非常完整。Zxing 支持 QRCode、Code128、Code39、EAN-13、UPC-A 等几乎所有常见码制;iOS 的 CIFilter 虽然只原生支持 CIQRCodeGenerator(二维码)和 CICode128BarcodeGenerator(Code128 条码),但已经覆盖了大多数业务场景。如果你在某个平台遇到不支持的码制,UTS 插件里也方便单独扩展。

第三,团队里虽然也有人不擅长原生,但 UTS 的语法跟 TypeScript 很像,Kotlin 和 Swift 的代码量也就几十行,花一晚上看基础语法完全能看懂。封装完之后前端调用就是一个简单函数,后面谁都能接手维护。

我在做之前也下载过几个插件市场的现成二维码插件,但发现很多要么只支持 Android,要么生成的图片带水印,要么需要付费解锁高清尺寸,还不如自己封装省心。这个你们根据自己的实际情况取舍。

3. UTS 插件封装 Zxing 生成二维码与条形码(Android 端)

3.1 创建 UTS 插件:目录结构和接口定义

先说项目环境。我用的是 HBuilderX 4.x 版本,创建的项目类型是“uni-app x”空模板,编译目标包含 Android 和 iOS。UTS 插件有两种存在方式:一种是在uni_modules目录下建共享插件,方便多项目复用;另一种是放在项目根目录utssdk下,只能当前项目用。我建议直接放uni_modules,因为生成的码工具类太常用了,以后别的项目也能直接导入。

插件目录结构长这样:

uni_modules/ └── uts-codegen/ ├── package.json ├── index.uts // 前端引用的接口定义 ├── utssdk/ │ ├── app-android/ │ │ ├── build.gradle // Android 依赖声明 │ │ └── CodeGenerator.kt │ ├── app-ios/ │ │ ├── CodeGenerator.swift │ │ └── Info.plist // 如果不需要权限可以省略 │ └── app-harmony/ // 如果暂时只做双端,这里可以先留着不写 └── common/ // 公共类型

index.uts是前端唯一需要关心的接口文件,我在里面定义了两个方法:

/** * 生成二维码 * @param content 二维码内容,可以是文本或 URL * @param size 图片尺寸,单位 px * @returns Base64 字符串(不含 data:image/png;base64, 前缀) */ export function generateQRCode(content: string, size: number): string { return Platform.OS === 'android' ? androidGenerateQRCode(content, size) : iosGenerateQRCode(content, size); } /** * 生成条形码(Code128) * @param content 条码内容 * @param width 图片宽度 * @param height 图片高度 * @returns Base64 字符串 */ export function generateBarcode(content: string, width: number, height: number): string { return Platform.OS === 'android' ? androidGenerateBarcode(content, width, height) : iosGenerateBarcode(content, width, height); }

Android 端的实际实现挂在androidGenerateQRCodeandroidGenerateBarcode这两个函数上,iOS 端挂到对应的 iOS 函数上。这里我用了Platform.OS做运行时判断,逻辑很简单,但方便以后扩展到鸿蒙等平台时只改这一个文件。

3.2 Android 端 Kotlin 实现:Zxing 的矩阵转 Bitmap

Android 端我选 Zxing 3.5.2 版本,这个版本比较稳定,在 Maven Central 上可以直接拉取。先在build.gradle里加上依赖:

dependencies { implementation 'com.google.zxing:core:3.5.2' }

这里有个很关键的细节:Zxing 本身分core包和javase包。javase里才有MatrixToImageWriter这类把矩阵转成图片的快捷工具类,但它在 Android 上用不了,因为它依赖 Java 标准库里的ImageIO。Android 端需要自己写一个矩阵转 Bitmap 的逻辑。代码如下:

package uts.sdk.modules.codegen import android.graphics.Bitmap import android.graphics.Color import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.common.BitMatrix import com.google.zxing.oned.Code128Writer import com.google.zxing.oned.Code39Writer import com.google.zxing.oned.EAN13Writer import com.google.zxing.oned.ITFWriter import java.io.ByteArrayOutputStream import java.util.Base64 import kotlin.math.roundToInt object CodeGenerator { private fun bitMatrixToBitmap(matrix: BitMatrix, width: Int, height: Int): Bitmap { val pixels = IntArray(width * height) val stepX = matrix.width / width.toFloat() val stepY = matrix.height / height.toFloat() for (y in 0 until height) { val matrixY = (y * stepY).roundToInt().coerceIn(0, matrix.height - 1) for (x in 0 until width) { val matrixX = (x * stepX).roundToInt().coerceIn(0, matrix.width - 1) pixels[y * width + x] = if (matrix.get(matrixX, matrixY)) Color.BLACK else Color.WHITE } } return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565).apply { setPixels(pixels, 0, width, 0, 0, width, height) } } private fun bitmapToBase64(bitmap: Bitmap): String { val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) return Base64.getEncoder().encodeToString(stream.toByteArray()) } fun generateQRCode(content: String, size: Int): String { val hints = mapOf( EncodeHintType.CHARACTER_SET to "UTF-8", EncodeHintType.MARGIN to 1, EncodeHintType.ERROR_CORRECTION to "H" ) val matrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size, hints) val bitmap = bitMatrixToBitmap(matrix, size, size) return bitmapToBase64(bitmap) } fun generateBarcode(content: String, width: Int, height: Int): String { val hints = mapOf( EncodeHintType.CHARACTER_SET to "UTF-8", EncodeHintType.MARGIN to 2 ) val writer: Code128Writer = Code128Writer() val matrix = writer.encode(content, BarcodeFormat.CODE_128, width, height, hints) val bitmap = bitMatrixToBitmap(matrix, width, height) return bitmapToBase64(bitmap) } }

这里我踩过一个坑,需要单独说一下:bitMatrixToBitmap里为什么有stepX/stepY这种缩放逻辑。Zxing 的encode方法传入了目标尺寸,但它生成的 BitMatrix 内部实际尺寸并非严格等于传入值,尤其是条形码,高度方向会做修正,宽度方向会根据内容动态计算。如果你直接拿 BitMatrix 的 getWidth 和 getHeight 去遍历,再创建对应尺寸的 Bitmap,经常会出现图片比例不对或者边缘留白很多的问题。所以正确做法是:固定按你想要的width * height像素数组去遍历,把矩阵坐标映射过去。这样输出的图片尺寸是可控的。

另外Base64我用的java.util.Base64,这是 Android API 26(Android 8.0)之后才有的。如果你的 App 还要兼容更低版本,记得换成android.util.Base64,用法是android.util.Base64.encodeToString(byteArray, android.util.Base64.NO_WRAP)。我的项目 minSdk 设置在当前已经普遍高于 26,所以直接用了java.util版本。

3.3 Android 端 UTS 封装:怎么把 Kotlin 对象暴露给前端

Kotlin 代码写完之后,需要在utssdk/app-android目录下写一个 UTS 的 Android 实现入口文件,一般名字叫UTSAndroid.kt或者直接用index.uts里声明的函数名一一对应。具体写法是这样的:

package uts.sdk.modules.codegen import uts.sdk.modules.codegen.CodeGenerator // UTS 插件要求暴露一个 UTSAndroid 对象,前端 index.uts 里的函数对接这里的同名方法 @Suppress("unused") object UTSAndroid { fun androidGenerateQRCode(content: String, size: Number): String { return CodeGenerator.generateQRCode(content, size.toInt()) } fun androidGenerateBarcode(content: String, width: Number, height: Number): String { return CodeGenerator.generateBarcode(content, width.toInt(), height.toInt()) } }

注意 UTS 里前端传过来的size在 Kotlin 侧会表现为Number类型,不是Int,所以这里要做一次toInt()转换。我最早没转,直接拿 Number 传给 Zxing 的encode方法,编译报错报得人一头雾水,后来查文档才明白 UTS 对跨语言类型映射有自己的一套规则,这个细节非常容易踩。

写完之后,在 HBuilderX 里对 uni_modules 目录右键“重新编译”,或者直接运行到 Android 真机,HBuilderX 会自动把 UTS 插件编进 App 原生工程。如果语法或类型有问题,编译期会直接报错,不会拖到运行时。

4. iOS 端使用 CoreImage 原生生成,不改一行前端代码

4.1 iOS 端 Swift 实现:CIFilter 的两行核心代码

iOS 端其实比 Android 简单很多,因为系统自带的 CoreImage 框架就内置了二维码和条形码的生成器,不需要引入任何第三方依赖。

先看二维码。CIQRCodeGenerator这个滤镜在 iOS 7 之后就一直存在,用法很简单:

import CoreImage import UIKit func generateQRCode(content: String, size: CGFloat) -> String? { let data = Data(content.utf8) let filter = CIFilter(name: "CIQRCodeGenerator")! filter.setValue(data, forKey: "inputMessage") filter.setValue("H", forKey: "inputCorrectionLevel") guard let output = filter.outputImage else { return nil } // CIFilter 默认生成的是一个 23x23 的小图,需要放大 let scale = size / output.extent.width let scaledImage = output.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) let context = CIContext() guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil } let uiImage = UIImage(cgImage: cgImage) guard let pngData = uiImage.pngData() else { return nil } return pngData.base64EncodedString() }

注意output.transformed(by:)这一步不能省。CIQRCodeGenerator 默认输出的 CIImage 尺寸非常小,只有 23x23 个像素点,如果直接转图片,放到 App 里会模糊得没法扫。很多人第一次用这个滤镜都会漏掉缩放步骤,然后跑来问为什么生成的二维码这么糊。

条形码用的是CICode128BarcodeGenerator,它生成 Code128 格式的条码。这个滤镜默认输出的尺寸也是一小块,只是长宽比例跟二维码不同,同样需要按目标尺寸缩放:

func generateBarcode(content: String, width: CGFloat, height: CGFloat) -> String? { let data = Data(content.utf8) let filter = CIFilter(name: "CICode128BarcodeGenerator")! filter.setValue(data, forKey: "inputMessage") filter.setValue(0.5, forKey: "inputQuietSpace") // 左右留白宽度 guard let output = filter.outputImage else { return nil } let scaleX = width / output.extent.width let scaleY = height / output.extent.height let scaledImage = output.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) let context = CIContext() guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil } let uiImage = UIImage(cgImage: cgImage) guard let pngData = uiImage.pngData() else { return nil } return pngData.base64EncodedString() }

条形码这边有个参数非常有用,就是inputQuietSpace。它控制条码左右两侧的空白区域宽度。如果你生成的条形码是给扫描枪扫的,这个静区一定要留够,否则很多扫码设备会识别不了。我测试下来,取值 0.5 到 1.0 之间是安全的,太小容易识别失败,太大又浪费纸面空间。你们可以根据实际打印效果微调。

4.2 iOS 端 UTS 封装:Swift 文件的接入方式

iOS 端的 UTS 封装逻辑和 Android 端类似,也是建一个UTSiOS.swift文件,放在utssdk/app-ios目录里:

import Foundation import CoreImage import UIKit @objc(UTSiOS) public class UTSiOS: NSObject { @objc public static func iosGenerateQRCode(content: String, size: Double) -> String { return generateQRCode(content: content, size: CGFloat(size)) ?? "" } @objc public static func iosGenerateBarcode(content: String, width: Double, height: Double) -> String { return generateBarcode(content: content, width: CGFloat(width), height: CGFloat(height)) ?? "" } }

注意几个点:Swift 里暴露给 UTS 调用的类必须是@objc暴露,方法必须是@objc且是类方法(用static)。返回值如果可能为 nil,需要在 UTS 侧给一个兜底空字符串,不然前端拿到 nil 处理起来很麻烦。

还有一点,CIContext()的创建是有开销的。如果你在列表页一次性生成几十个二维码,每次调用都创建新的 CIContext 会有明显卡顿。我的做法是在类里定义一个静态的懒加载 context,复用它:

private static let ciContext: CIContext = CIContext()

然后在方法里直接用Self.ciContext.createCGImage(...)。这个优化虽然小,但在批量生成场景下体感差异很大,建议保留。

4.3 双端接口保持一致的好处

你可能会问,Android 和 iOS 实现完全不一样,前端怎么统一调用?这就是 UTS 插件设计的好处了。前端index.uts里我已经通过Platform.OS做了分发,所以 Vue 页面里的业务代码不需要关心当前跑在什么平台,只需要调用generateQRCodegenerateBarcode。这就是前面说的“不改一行前端代码”。

而且 Base64 返回的格式在双端是一致的:一个不含前缀的纯 Base64 字符串。前端展示图片的时候,根据自己的需求拼不拼data:image/png;base64,前缀都可以;如果要保存到相册或者发给后端,直接用纯 Base64 传输更干净。

5. 前端页面集成:组件化封装、图片保存和常见坑

5.1 把插件包一层前端工具类

UTS 插件写好后,我不建议业务页面直接去调index.uts里的函数,因为你还可能要做尺寸单位转换、错误处理、缓存等逻辑。我在项目的utils目录下包了一层codeUtils.ts

import { generateQRCode, generateBarcode } from '@/uni_modules/uts-codegen'; const MARGIN = 8; // 给图片留一点边距 /** * 生成二维码图片临时路径 * 返回的是本地文件路径,可以直接用于 image 组件展示 */ export async function createQRCodeTempFile(content: string, sizePx: number): Promise<string> { if (!content) { return ''; } const base64 = generateQRCode(content, sizePx); if (!base64) { return ''; } // 将 base64 写入临时文件(具体函数看下面) const filePath = await base64ToTempFile(base64, `qr_${Date.now()}.png`); return filePath; } /** * 生成条形码图片临时路径 */ export async function createBarcodeTempFile(content: string, widthPx: number, heightPx: number): Promise<string> { if (!content) { return ''; } const base64 = generateBarcode(content, widthPx, heightPx); if (!base64) { return ''; } const filePath = await base64ToTempFile(base64, `bar_${Date.now()}.png`); return filePath; } /** * Base64 字符串写入临时文件 * 不同平台的文件系统 API 略有区别,这里封装一层 */ async function base64ToTempFile(base64: string, fileName: string): Promise<string> { // 使用 uni-app x 提供的文件写入能力 // 具体 API 以你的项目依赖为准,核心思路是: // 1. 拿到临时目录路径 // 2. 把 base64 转成 ArrayBuffer // 3. 写入文件 // 4. 返回文件路径 // 我这里按项目里封装好的 writeBase64ToTempFile 处理 return writeBase64ToTempFile(base64, fileName); }

这里多说一句,为什么要把 Base64 转成临时文件再给 image 组件展示,而不是直接用 Data URI 塞给<image>的 src。我测试下来,uniapp-x 的 image 组件在 App 端对超长 Data URI 的支持不太稳定,尤其是 Android 端,偶尔会出现图片加载不出来的情况。转成本地临时文件路径是最稳妥的,而且后续如果要保存到相册,也直接有文件可操作。

5.2 页面里的使用示例

在业务页面里,用起来就很简单了。比如一个典型的出库单页面,需要展示一个二维码和一个条形码:

<template> <view class="container"> <text class="label">出库单号:{{ orderId }}</text> <image v-if="qrCodePath" :src="qrCodePath" class="code-image" mode="widthFix" /> <image v-if="barcodePath" :src="barcodePath" class="code-image bar-height" mode="widthFix" /> <button @click="saveToAlbum">保存到相册</button> </view> </template> <script setup> import { ref } from 'vue'; import { onLoad } from '@dcloudio/uni-app'; import { createQRCodeTempFile, createBarcodeTempFile } from '@/utils/codeUtils'; const orderId = ref('PO20250500123'); const qrCodePath = ref(''); const barcodePath = ref(''); const tempFilePath = ref(''); onLoad(async () => { // 生成二维码,内容里带中文也支持,因为底层指定了 UTF-8 qrCodePath.value = await createQRCodeTempFile( JSON.stringify({ orderId: orderId.value, ts: Date.now() }), 300 ); // 生成条形码,宽度 600,高度 200 barcodePath.value = await createBarcodeTempFile(orderId.value, 600, 200); }); async function saveToAlbum() { // 把当前展示的图片保存到系统相册,用临时文件路径即可 // 需要在 App 配置相册权限 if (!tempFilePath.value) { uni.showToast({ title: '图片还没有生成', icon: 'none' }); return; } uni.saveImageToPhotosAlbum({ filePath: tempFilePath.value, success: () => uni.showToast({ title: '已保存', icon: 'success' }), fail: (err) => { console.error('保存失败', err); uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' }); } }); } </script>

这里有两个细节想单独提一下。

第一个是二维码内容编码。仓储场景里,很多订单号是纯数字,但也不排除有字母、中文字符甚至 JSON 字符串。Zxing 和 CoreImage 都支持 UTF-8 编码,前端不用做特殊的 encodeURIComponent 处理,直接把原始字符串传给生成函数就行。但要注意,如果二维码内容太长(比如超过 1000 个字符),二维码的密度会急剧上升,扫描识别的成功率会下降。所以我的建议是内容能精简就精简,太长的数据走后端存取,二维码里只放一个索引 ID。

第二个是生成时机。上面的代码是onLoad里生成一次,这是最简单的方案。但如果是长列表页面,每一项都要生成一个码,我强烈建议做懒加载,滚动到可视区域再去生成,不然一次性几十个原生生成任务同时跑,卡顿还是小事,内存占用可能会把低端机压垮。我在列表页里用的是 IntersectionObserver 或者滚动事件 + 可视区判断的简易懒加载,生成完的图片路径缓存在 Map 里,避免重复生成。

5.3 无法绕开的几个坑:尺寸、留白、单位和缓存

在这个环节,我把实际项目里踩过的几个比较隐蔽的坑集中列一下,你们做的时候能少走很多弯路。

第一,单位的坑。UTS 插件里接收的sizewidthheight我按 px 处理。但 uniapp-x 里给用户设置界面尺寸时,通常用的是 rpx。如果前端直接把 rpx 数值传进来,生成出来的图片实际显示尺寸和期望尺寸会差很多。解决办法很简单:在调用工具函数之前,把 rpx 转成 px。uniapp-x 里可以用uni.upx2px()来做转换,或者根据uni.getSystemInfoSync()拿到窗口宽度后自己换算。

第二,二维码的留白问题。Zxing 生成时我设置了EncodeHintType.MARGIN为 1,CoreImage 生成时默认会带一圈静区。这里有个反直觉的现象:留白太小,扫码机器反而识别不了。二维码最外圈必须有至少 4 个模块宽度的白色区域,这是 QRCode 规范里明确规定的。所以我在测试时特意对比了 MARGIN=0、MARGIN=1、MARGIN=4 三种情况,用扫码枪实测,MARGIN=0 时部分扫码枪确实识别失败。最后我统一用 1,因为如果前端展示时外面还要套一层圆角卡片,卡片背景本身就是白色,等于额外加了静区,识别完全没问题。如果你要把生成的图片直接贴在非白色背景上,记得把 MARGIN 调大一些。

第三,图片缓存。Base64 转临时文件,如果每次都生成新文件,临时目录会越积越多,最终触发系统清理或者磁盘满。我封装工具类时,生成文件名里带了时间戳,就是为了避免覆盖,但同时我也加了一个“按内容哈希命名”的逻辑,同一内容的码只生成一次,下次直接复用已有文件。这样既避免了重复生成的开销,也不会无限堆积文件。如果你们有清理需求,可以在 App 启动时把临时目录里超过 7 天的文件删掉。

第四,条形码的内容限制。Code128 虽然支持全部 ASCII 字符,但某些特殊字符(比如中文)是编不了的。Zxing 的 Code128Writer 遇到中文字符会直接抛异常,CoreImage 的 CICode128BarcodeGenerator 遇到不支持的内容也会返回空。所以条形码内容最好限制为数字、字母、连字符、空格这类纯 ASCII 字符。如果业务上一定要用中文存条码,那就只能改用二维码,或者先把中文映射成拼音/编号。

6. 补充方案:web-view 本地 HTML 生成二维码的绕路实现

6.1 什么时候才会想起这个方案

很多同学可能不想碰 UTS 插件,或者项目只是做个内部小工具,连“原生工程”的概念都不太想了解。这种时候,web-view 绕路方案反而是一个性价比很高的选择。

我最初也是先试了这个方案,虽然最后因为性能和图片传输的繁琐程度换成了原生方案,但它的实现思路还是有参考价值的,尤其是对于那些只需要在某个页面偶尔生成一下、不追求极致性能的场景。

这个方案的核心思路是:uniapp-x 页面里嵌一个 web-view,web-view 加载本地 HTML 文件,HTML 里用前端 JS 库(比如 qrcodejs)生成二维码图片,然后把图片的 Base64 数据通过 postMessage 传回 uniapp-x 侧。

6.2 实现步骤和关键代码

第一步,在项目static/html目录下建一个qrgen.html

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>二维码生成器</title> <script src="./qrcode.min.js"></script> <script src="./bwip-js.js"></script> </head> <body> <script> // 接收 uniapp-x 传来的参数 document.addEventListener('message', function (e) { var payload = JSON.parse(e.data || '{}'); if (payload.type === 'qr') { var qr = new QRCode(document.getElementById('code'), { text: payload.content, width: payload.size || 256, height: payload.size || 256, correctLevel: QRCode.CorrectLevel.H }); var img = document.querySelector('#code canvas'); var base64 = img.toDataURL('image/png'); window.postMessage(base64); } else if (payload.type === 'barcode') { try { var canvas = document.createElement('canvas'); bwipjs.toCanvas(canvas, { bcid: 'code128', text: payload.content, scale: 3, height: Math.round(payload.height / 2), includetext: true, textxalign: 'center' }); window.postMessage(canvas.toDataURL('image/png')); } catch (err) { window.postMessage('ERROR:' + err.message); } } }); </script> <div id="code"></div> </body> </html>

注意,web-view 里加载本地 HTML,qrcode.min.js 和 bwip-js 这些 JS 库也要一起放进static/html目录下。不要在 HTML 里用 CDN 链接,因为本地 web-view 可能没有网络权限,或者网络加载有延迟。

第二步,uniapp-x 页面里放一个 web-view 组件,并处理通信:

<template> <web-view :src="htmlPath" @message="onWebViewMessage" ></web-view> </template> <script setup> import { ref } from 'vue'; import { onLoad } from '@dcloudio/uni-app'; const htmlPath = ref('/static/html/qrgen.html'); onLoad(() => { // 页面加载后给 web-view 发送消息 setTimeout(() => { // 这里通过 web-view 的 context 发消息,不同版本 API 名称可能不同 uni.postMessageToWebView({ type: 'qr', content: 'PO20250500123', size: 256 }); }, 500); }); function onWebViewMessage(e) { const data = e.detail.data; if (data && data[0]) { const base64 = data[0]; // 拿到 base64 后,转成临时文件展示 console.log('web-view 返回的二维码图片', base64); } } </script>

6.3 这个方案的真实痛点

我必须说实话,这个方案我最终没有采用,主要因为它有几个绕不开的痛点。

第一个是加载时序问题。web-view 加载本地 HTML 也是异步的,你不能保证onLoad一触发 HTML 里的 JS 就已经准备好了。我上面用了 500ms 的 setTimeout,这是很糙的做法,实际项目里应该用更可靠的“就绪握手”,比如 HTML 加载完后主动向 uniapp-x 发一条 ready 消息,uniapp-x 收到后再下发生成请求。我图省事用了延时,在低端安卓机上就出现过发消息太早、HTML 还没监听 message 的情况。

第二个是图片传回效率。HTML 侧生成的图片 Base64 通过 postMessage 传回,数据量动辄几十 KB 到几百 KB,在 web-view 和原生层之间走这么一大段字符串,近实时展示还行,批量生成几十个码的时候就会明显感到卡顿和内存飙升。

第三个是可维护性。web-view 里的代码是独立的 HTML/JS 世界,调试要靠 H5 那套 devtools,报错信息也不容易冒泡到 uniapp-x 侧。有一次用户反馈说某个条码生成失败,我 debug 了很久才发现是 bwip-js 不支持那串特殊字符。如果走原生方案,异常会在 UTS 层直接抛出,问题定位会快很多。

所以我的建议是:web-view 方案只适合“临时顶上”或者“快速验证”的场景。如果你在做正式的仓储、物流、零售类 App,还是老老实实走原生插件路线,一次封装,长期受益。

从我个人角度看,这次 uniapp-x 项目里最值回票价的决策就是把码图生成从“前端 JS 库思路”切换到“原生能力封装思路”。虽然前期多花了一天写 Kotlin 和 Swift,但后面所有页面调用都变得极其干净,而且打印出来的条码用工业扫码枪实测识别率非常高。后面如果你们要扩展别的码制,比如 EAN-13 商品码或 PDF417 堆叠码,也只需要在 UTS 插件里对应平台各加一个方法即可,前端接口完全不受影响。最后再分享一个小技巧:如果你需要在多端做灰度切换(比如 iOS 先走 CoreImage,Android 临时走 web-view),UTS 的Platform.OS判断可以让两端随时各走各的路,调试起来特别方便。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询