上个月帮朋友公司做员工门禁考勤App,技术栈定的uni-app。需求清单里躺着一条“人脸识别打卡”。我当时心里清楚,uni-app自带的那套API里,根本没有给人脸识别留位置;插件市场里搜了一圈,要么是商业SDK套壳按终端数收费,要么只有Android没有iOS。问了两家服务商,授权费直接把项目预算打穿。最后只能走自己写插件的路:用uni-app官方的UTS能力,把原生人脸识别引擎封装成一个API插件,业务层继续写Vue,原生层去调系统框架和离线SDK。
这条路走下来,最大的感受是:UTS插件机制比很多人想象中能打,但坑也比想象中多。这篇就把整个制作过程、接口设计思路、双端实现要点和上线前躲不开的那些坑拆开写清楚,适合正在做uni-app人脸识别需求、又不想被插件市场绑架的团队参考。
1. 人脸识别为什么必须走原生插件:UTS补的是能力缺口
1.1 人脸识别的完整链路,哪一环是前端做不了的
人脸识别看起来是一个功能,背后其实是一条完整链路:图像采集、人脸检测、关键点定位、特征提取、特征比对,有的场景还要加上活体检测。翻遍前端的能力边界,能做的只有“把相机打开”和“把图片传到服务器”,后面那一串计算,无论是在本机做还是在云端做,基本都够不到原生层。
之前有人提过用纯前端方案,比如face-api.js配合WebGL跑模型。在iPhone和旗舰安卓机上,实时检测大约能跑到每秒几帧,看起来能用,但放到门禁打卡场景就不太现实了:低端机掉帧会掉到没法看,而且前端模型包动辄几MB,加载时间很难看。最麻烦的是精度和活体能力,一旦遇到照片、视频翻拍这类攻击,纯前端方案基本挡不住。
所以结论很直接:要做可靠的人脸识别,必须落到原生能力上。而uni-app项目要调用原生能力,除了写原生插件,就是走UTS。
1.2 UTS的运行机制:写TypeScript,编译成Kotlin和Swift
UTS全称是uni-app Universal TypeScript,语法接近TypeScript,但它不是跑在JS引擎里,而是在编译阶段被转换成对应的原生代码:Android侧编译成Kotlin,iOS侧编译成Swift。这就意味着你可以在UTS代码里直接import原生类,直接调用Android的CameraX、ML Kit,或者iOS的Vision框架,不需要像传统插件那样写一套Java、一套Objective-C,再搞一套JSBridge。
这里面有个关键点需要理解:UTS不是一门新语言,而是一层“语法糖编译桥”。你写的UTS代码最终会变成原生代码参与编译打包,所以它能访问的系统API和原生第三方SDK几乎没有上限。这也是UTS插件能做的人脸识别插件、而普通JS插件做不了的根本原因——普通JS插件只能通过renderjs或者内置API间接调用能力,能力边界被框得很死。
需要补充的是,UTS插件不解决算法问题。人脸检测、特征比对这些算法,还是要靠系统框架或第三方SDK来提供。UTS解决的是“把这些原生库接入uni-app工程”的工程化问题。想清楚这一层,后面设计接口时就不会跑偏。
1.3 先想清楚:用系统/离线SDK还是在线API
动手写插件前,先要确定算法来源。目前主流是三条路:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 系统框架(Android ML Kit / iOS Vision) | 免费、包体小、接入快 | 只能做人脸检测和关键点,不做特征比对;部分能力依赖Google服务 | 检测、区域遮挡判断、活体辅助 |
| 第三方离线SDK(虹软/百度离线/商汤离线等) | 精度高,自带特征提取和1:1比对 | 收费,SDK包体大,厂商申请流程麻烦 | 门禁打卡、考勤、实名认证 |
| 在线API(云厂商人脸识别接口) | 精度最高,活体方案成熟 | 依赖网络,有按次收费,隐私合规要求高 | 需要后台统一管理的场景 |
我做门禁打卡时的取舍是:人脸检测用Android的ML Kit和iOS的Vision,先把“画面里有没有人脸、人脸在哪个位置”搞定;特征提取和1:1比对用离线SDK,因为考勤场景经常在闸机、门禁这种网络不稳定的地方,不能把每一次打卡都压在云上。UTS插件本身对这三条路都是开放的,但接口层必须预留好扩展位,不然换算法SDK时插件要大改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭一个UTS插件工程:目录、配置与最小可运行示例
2.1 用HBuilderX创建UTS插件,目录结构长什么样
创建UTS插件有两种方式:一种是在uni-app项目里手工创建uni_modules插件目录,另一种是通过HBuilderX的“新建UTS插件”向导。推荐用向导,它会自动生成规范的目录骨架。
一个最小可用的UTS插件目录大概是这样的:
code复制uni_modules/
└── uts-face-detect/
├── package.json
├── index.uts
├── android/
│ ├── src/
│ └── build.gradle
└── ios/
└── FaceDetectHelper.swift
这里每个目录的作用:
package.json:插件声明文件,定义插件id、版本号,比如"id": "uts-face-detect"。这个id就是后面业务层import时用的模块名。index.uts:插件门的入口文件,对外暴露方法、类、常量。业务层只能import到index.uts里export出去的东西。android/:放Kotlin代码和Android依赖配置,这里面的代码最终会参与Android打包。ios/:放Swift代码,参与iOS打包。
创建完之后,要在manifest.json的App模块配置里确认勾选了需要的原生模块,比如Camera相关能力。UTS插件本身不用在manifest里显式注册,但插件里依赖的原生模块必须勾选,否则真机调试时会报找不到类。
2.2 插件如何被业务层引用
把插件目录放进uni_modules后,业务层直接通过相对路径或别名import:
ts复制import { startDetect, stopDetect } from '@/uni_modules/uts-face-detect';
这里有个很容易踩的坑:UTS插件的文件路径如果带特殊字符或者层级太深,Android编译时可能报资源路径错误。我习惯把所有UTS插件放在uni_modules根目录下,不要在插件目录里再套多层无关目录。
