iOS、安卓、鸿蒙APP如何运行同一个小程序
公司已经做好的会员服务小程序,能不能同时放进 iOS、安卓和鸿蒙 APP?对业务团队来说,页面、接口和办理流程已经有了,新增一个客户端,当然希望继续使用原来的成果。按三个平台分别重写页面,后续每次修改活动规则、增加表单字段,还要再跟着维护三遍。
借助小程序容器,可以把业务页面和逻辑保留在同一套小程序代码中,由三个 APP 分别嵌入对应的小程序 SDK,提供加载和运行环境。客户端团队完成宿主接入,业务团队继续开发小程序,管理平台负责版本上传和发布。同一项业务就能进入不同系统的 APP,后续更新也有了统一的管理入口。
以 FinClip 为例,可以让三个 APP 打开同一个测试小程序,再逐步接入登录、支付等业务能力。接入示例使用官方 SDK 接口,并补充项目侧状态判断和错误处理;尖括号中的版本、凭据和服务地址需要替换成项目配置。代码放入已有宿主工程,省略工程结构及部分导入声明。
小程序与三个客户端的运行关系
小程序代码包含页面结构、样式、业务脚本和资源文件。容器在 APP 内加载代码包,提供页面渲染、脚本执行、路由、生命周期以及设备能力调用等运行支持。用户在 APP 首页点击某项服务,打开的就是运行在宿主内部的小程序页面。
iOS APP 使用 iOS 版 SDK,安卓 APP 使用 Android 版 SDK,鸿蒙 APP 使用 HarmonyOS 版 SDK。各端运行时对接自己的系统环境,向小程序提供相应的组件和接口,业务开发者可以复用页面布局、表单校验、查询逻辑和后端请求。三端共用业务代码,各自保留原有客户端工程和原生导航。
例如,会员积分小程序里有积分明细、兑换列表和订单详情。业务团队维护一套页面,三个客户端都从各自的入口打开它。积分怎么计算、订单怎么生成仍由原有业务后台处理,小程序容器负责让页面在不同 APP 中运行。
平台配置与小程序准备
开始集成前,需要在 FinClip 管理平台创建小程序,取得平台分配的小程序 AppID,再配置宿主应用及各端应用标识,获取相应的 SDK Key 和 SDK Secret。小程序还要与允许承载它的宿主应用建立关联,客户端才能按平台配置打开服务。
接入时可以把信息分成两组:SDK 初始化使用宿主凭据和平台服务地址;打开具体业务时使用小程序 AppID、目标页面与业务参数。宿主凭据标识的是哪个 APP 在接入平台,小程序 AppID 标识的是要运行哪一项业务,两者在工程配置中分别维护。
已有微信小程序可以导入 FinClip 开发者工具,检查组件和 API 兼容情况,复用受支持的页面与逻辑。登录、支付、消息以及微信云开发等平台相关能力,需要对接自有 APP 和后端服务。为了验证 SDK 接入,先准备一个只显示入口参数的小程序页面,避免把容器接入与业务接口问题混在一起。
共用测试小程序
在小程序项目中注册 pages/index/index。项目侧测试示例的 app.json 如下;已有工程只需合并页面配置。
{
"pages": ["pages/index/index"],
"window": {
"navigationBarTitleText": "多端运行测试"
}
}
pages/index/index.js 读取启动参数,pages/index/index.wxml 将参数显示出来:
Page({
data: { source: '未传入' },
onLoad(options) {
this.setData({ source: options.source || '未传入' });
}
});
<view>
<text>小程序已打开,入口参数:{{source}}</text>
</view>
用开发者工具编译并上传代码包,完成审核、上架和宿主关联。三端示例统一使用同一个 <MINIAPP_ID>,通过 source=ios、source=android、source=harmony 区分入口。source 只是客户端传入的测试标记,不代表系统检测结果。示例按 AppID 打开正式版;体验版和开发版应使用对应的二维码打开接口,并配置体验成员。
iOS:依赖引入、初始化与打开页面
在现有 Podfile 的 APP target 中加入官方依赖,将 <IOS_SDK_VERSION> 替换为选定版本,执行 pod install,再通过生成的 .xcworkspace 打开工程。
pod 'FinApplet', '<IOS_SDK_VERSION>'
在 AppDelegate 的应用启动方法中加入初始化片段。示例使用 Objective-C;Swift 工程可以按官方集成方式导入同一 SDK,再按 Swift 调用形式接入。
#import <FinApplet/FinApplet.h>
// 放在应用启动方法内,初始化一次。
FATStoreConfig *store = [[FATStoreConfig alloc] init];
store.sdkKey = @"<IOS_SDK_KEY>";
store.sdkSecret = @"<IOS_SDK_SECRET>";
store.apiServer = @"<FINCLIP_API_SERVER>";
FATConfig *config = [FATConfig configWithStoreConfigs:@[store]];
NSError *initError = nil;
BOOL ready = [[FATClient sharedClient] initWithConfig:config
error:&initError];
if (!ready) {
NSLog(@"小程序 SDK 初始化失败:%@", initError);
}
宿主保存初始化结果,成功后启用业务入口。在当前可见的 UIViewController 中响应按钮点击,使用 FATAppletRequest 指定小程序和目标页面。代码中的 self 是当前页面控制器,打开操作在主线程执行。
FATAppletRequest *request = [[FATAppletRequest alloc] init];
request.appletId = @"<MINIAPP_ID>";
request.apiServer = @"<FINCLIP_API_SERVER>";
request.startParams = @{
@"path": @"/pages/index/index",
@"query": @"source=ios"
};
[[FATClient sharedClient] startAppletWithRequest:request
InParentViewController:self
completion:^(BOOL result, FATError *error) {
if (!result) {
NSLog(@"小程序打开失败:%@", error);
}
}
closeCompletion:^{
NSLog(@"已返回宿主页面");
}];
测试页显示 ios,即可确认代码包加载、页面跳转和参数传递已经连通。接入相机、定位等业务时,再补充相应权限描述和扩展模块。
安卓:初始化状态与业务入口
按官方集成指引配置 FinClip Maven 仓库后,在 APP 模块的 build.gradle 中加入 SDK 依赖。仓库地址与访问配置由工程维护,<ANDROID_SDK_VERSION> 替换为项目使用的固定版本。
dependencies {
implementation 'com.finogeeks.lib:finapplet:<ANDROID_SDK_VERSION>'
}
同时按该版本接入文档配置原生库打包与混淆规则,在使用代码压缩的工程中加入:
-keep class com.finogeeks.** {*;}
初始化放入宿主现有 Application。Kotlin 示例使用 FinStoreConfig、FinAppConfig、FinAppClient 和 FinCallback 的官方接口;类名 HostApplication、状态字段及提示语是项目侧示例,导入声明由 IDE 从已安装 SDK 补齐。若项目已有 Application,将字段和初始化逻辑合入原类,并确认 Manifest 指向该类。
class HostApplication : android.app.Application() {
@Volatile var finclipReady = false
private set
override fun onCreate() {
super.onCreate()
if (FinAppClient.isFinAppProcess(this)) return
val store = FinStoreConfig(
"<ANDROID_SDK_KEY>",
"<ANDROID_SDK_SECRET>",
"<FINCLIP_API_SERVER>",
"<FINCLIP_APM_SERVER>",
"/api/v1/mop/",
"",
"MD5",
false,
true
)
val config = FinAppConfig.Builder()
.setFinStoreConfigs(listOf(store))
.build()
FinAppClient.init(this, config, object : FinCallback<Any?> {
override fun onSuccess(result: Any?) {
finclipReady = true
}
override fun onError(code: Int, error: String?) {
finclipReady = false
android.util.Log.e("FinClip", "初始化失败:$code $error")
}
override fun onProgress(status: Int, info: String?) {}
})
}
}
MD5 是示例中的 SDK 通信配置值,需要与平台配置一致;末尾两个布尔值分别关闭返回数据加密、开启基础库预加载。APM 地址填写平台的数据上报地址。进程判断也应覆盖其他宿主级初始化,避免小程序进程重复启动整套业务组件。
Activity 的按钮点击处理使用真实的 startApplet 接口。只有初始化成功才继续打开,失败结果交回当前页面处理。
val app = application as HostApplication
if (!app.finclipReady) {
android.widget.Toast.makeText(
this, "小程序服务尚未就绪", android.widget.Toast.LENGTH_SHORT
).show()
} else {
val request = IFinAppletRequest.fromAppId(
"<FINCLIP_API_SERVER>", "<MINIAPP_ID>"
).setStartParams(mapOf(
"path" to "/pages/index/index",
"query" to "source=android"
))
FinAppClient.appletApiManager.startApplet(
this, request, object : FinCallback<String?> {
override fun onSuccess(result: String?) {}
override fun onProgress(status: Int, info: String?) {}
override fun onError(code: Int, error: String?) {
android.util.Log.e("FinClip", "打开失败:$code $error")
}
}
)
}
打开接口接收当前 Activity,测试页应显示 android。初始化成功却打不开时,优先核对小程序发布状态、关联关系和请求的服务地址。
鸿蒙:HAR 依赖与 Navigation 接入
鸿蒙可以采用线上依赖或本地 HAR 包。以本地包为例,将同一交付版本的 FinClipSDK.har 和 FinClipSDKCore.har 放进工程根目录的 har 文件夹,在 entry/oh-package.json5 中合并:
{
"dependencies": {
"@finclip/sdk": "file:../har/FinClipSDK.har"
}
}
在工程根目录的 oh-package.json5 中合并底层依赖覆盖配置,随后执行 ohpm install:
{
"overrides": {
"@finclip/sdk-core": "file:./har/FinClipSDKCore.har"
}
}
依照接入指引,在根目录 build-profile.json5 的目标 app.products 元素内合并配置;JSON 片段均保留工程已有字段:
{
"buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
}
}
}
entry/src/main/module.json5 的 module.requestPermissions 中至少配置网络权限:
{
"name": "ohos.permission.INTERNET"
}
启动方式采用官方 Navigation 方案,该方式文档标注最低支持 SDK 1.1.0。小程序使用宿主已挂载的 NavPathStack 打开,因此无需额外注册承载小程序的 Ability。将示例加入工程已登记的入口页面,已有 Navigation 工程则复用自己的页面栈。
import common from '@ohos.app.ability.common';
import {
EAppletStartMode, FinAppClient,
IFinAppConfig, IFinAppStartMode
} from '@finclip/sdk';
@Entry
@Component
struct Index {
@State pageInfos: NavPathStack = new NavPathStack();
@State status: string = '等待打开';
client?: FinAppClient;
async openMiniProgram() {
try {
if (!this.client) {
const config: IFinAppConfig.IFinAppConfig = {
finStoreConfigs: [{
apiServer: '<FINCLIP_API_SERVER>',
sdkKey: '<HARMONY_SDK_KEY>',
sdkSecret: '<HARMONY_SDK_SECRET>',
cryptType: 'md5'
}]
};
const mode: IFinAppStartMode = {
startMode: EAppletStartMode.Navigation,
routerState: this.pageInfos,
uiContext: this.getUIContext()
};
this.client = FinAppClient.init(
config,
getContext(this) as common.UIAbilityContext,
'',
mode
);
}
this.status = '正在打开';
await this.client.startApplet({
appId: '<MINIAPP_ID>',
apiServer: '<FINCLIP_API_SERVER>',
startParams: {
path: '/pages/index/index',
query: 'source=harmony'
}
});
this.status = '打开调用已返回,请检查小程序页面';
} catch {
this.status = '调用异常,请查看 SDK 日志后重试';
}
}
@Builder
PageMap(name: string) {
// 在已有工程中保留宿主原有的页面路由映射。
}
build() {
Navigation(this.pageInfos) {
Column({ space: 16 }) {
Text(this.status)
Button('打开测试小程序')
.onClick(() => { this.openMiniProgram(); })
}
}
.mode(NavigationMode.Stack)
.id('root')
.title('小程序接入测试')
.navDestination(this.PageMap)
}
}
openMiniProgram 是页面自定义方法,内部调用的是 SDK 的 FinAppClient.init 和 startApplet。初始化在用户点击后执行,此时页面已经创建,能够提供 UI 上下文;routerState 与页面上的 Navigation 使用同一个实例。示例将通信方式显式设为 md5,实际值按平台配置填写。已有工程统一保存客户端实例,不随页面重建重复初始化 SDK。
点击按钮后,小程序应显示 harmony。startApplet 的调用返回并不单独作为页面成功展示的证据,实际验收以测试页出现、参数正确和 SDK 日志为准。
宿主登录与设备能力对接
小程序页面能够打开后,接下来要让它接上 APP 已有的业务环境。比如用户已经登录 APP,进入会员小程序时,应该能够继续查询自己的积分和订单,无需再走一遍注册流程。
项目可以通过容器提供的扩展机制,把获取业务登录态、发起支付、打开原生页面等操作封装为统一的宿主能力。小程序调用约定接口,各端宿主分别完成原生实现,再按一致的数据结构返回结果。登录过程由企业账号服务衔接,业务后台继续校验用户身份和访问权限。
对于扫码、拍照、定位等能力,三个客户端需要分别配置系统权限、接入相关 SDK 模块。业务侧保持统一的调用约定,平台差异由运行时和宿主适配处理。后续再增加新的小程序时,已经接好的登录、支付和设备能力就能继续复用。
返回行为也要与 APP 原有体验接上:小程序内部返回上一页,退出小程序则回到宿主入口;业务完成后需要刷新原生列表的,由宿主接收办理结果并更新页面。用户感受到的是同一个 APP 内连续的服务流程。
三端联调与发布检查
三台设备使用同一个小程序 AppID,分别从宿主按钮打开测试页。先看页面能否显示,再核对参数和返回行为,比直接接入完整业务更容易定位接入问题。
| 检查项 | 操作与预期结果 | 未通过时检查 |
|---|---|---|
| 初始化 | iOS 返回成功;安卓收到成功回调;鸿蒙完成初始化调用,并结合后续打开结果验证 | 各端凭据、应用标识、平台地址和网络 |
| 页面加载 | 三端都显示“多端运行测试”页面 | 上架状态、宿主关联、AppID 和页面路径 |
| 参数传递 | 分别显示 ios、android、harmony | startParams.query 与页面 onLoad的参数读取 |
| 返回宿主 | 关闭小程序后回到原入口,入口仍能再次使用 | 页面控制器、Activity 或 Navigation 页面栈 |
| 业务更新 | 修改测试页文字并发布新版,按 SDK 更新策略重新进入后确认版本 | 发布范围、实际加载版本和本地缓存策略 |
测试页通过后,将相同的打开方式接到首页宫格、活动位或消息入口,再把目标路径换成真实业务页面。例如订单消息可以携带订单标识,直接进入详情页。随后在三个宿主中完成登录、查询、提交、返回的全流程,检查设备授权、网络中断和前后台切换。错误日志同时记录宿主版本、SDK 版本及小程序版本,方便区分客户端接入问题和业务更新问题。
统一发布与业务独立更新
三端接入完成后,小程序代码包进入同一个管理平台,经过体验验证、审核和发布,再由各端 SDK 获取适用版本。团队可以统一管理小程序资产,也可以通过灰度策略控制新版本的发布范围,结合各端测试结果逐步上线。
日常调整会员页面、增加活动专区、修改表单等业务更新,只要使用的能力已由当前宿主和运行时提供,就可以走小程序发布流程,无需等待三个主 APP 同步发版。容器 SDK 升级、新增原生能力等宿主工程变更,继续走各客户端的构建和发布流程。
业务更新与客户端更新分开后,团队的协作方式也会随之变化:业务开发人员维护共用的小程序,客户端人员维护各端运行环境和公共能力,运营人员在平台查看版本、审核和上线状态。新增业务可以继续接入现有容器,已有微信小程序资源也能在完成适配后加入自有 APP。
FinClip 将三端运行 SDK、开发者工具和管理平台连接起来,让同一套小程序拥有跨客户端运行和持续发布的能力。企业保留各端 APP 已有的原生体验,同时把频繁变化的业务交给共用小程序承载,减少重复开发,让功能上线与日常维护围绕同一份业务成果推进。