公司已经做好的会员服务小程序,能不能同时放进 iOS、安卓和鸿蒙 APP?对业务团队来说,页面、接口和办理流程已经有了,新增一个客户端,当然希望继续使用原来的成果。按三个平台分别重写页面,后续每次修改活动规则、增加表单字段,还要再跟着维护三遍。

借助小程序容器,可以把业务页面和逻辑保留在同一套小程序代码中,由三个 APP 分别嵌入对应的小程序 SDK,提供加载和运行环境。客户端团队完成宿主接入,业务团队继续开发小程序,管理平台负责版本上传和发布。同一项业务就能进入不同系统的 APP,后续更新也有了统一的管理入口。

以 FinClip 为例,可以让三个 APP 打开同一个测试小程序,再逐步接入登录、支付等业务能力。接入示例使用官方 SDK 接口,并补充项目侧状态判断和错误处理;尖括号中的版本、凭据和服务地址需要替换成项目配置。代码放入已有宿主工程,省略工程结构及部分导入声明。

cover_16_9

小程序与三个客户端的运行关系

小程序代码包含页面结构、样式、业务脚本和资源文件。容器在 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=iossource=androidsource=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,即可确认代码包加载、页面跳转和参数传递已经连通。接入相机、定位等业务时,再补充相应权限描述和扩展模块。

inline_01_architecture

安卓:初始化状态与业务入口

按官方集成指引配置 FinClip Maven 仓库后,在 APP 模块的 build.gradle 中加入 SDK 依赖。仓库地址与访问配置由工程维护,<ANDROID_SDK_VERSION> 替换为项目使用的固定版本。

dependencies {
    implementation 'com.finogeeks.lib:finapplet:<ANDROID_SDK_VERSION>'
}

同时按该版本接入文档配置原生库打包与混淆规则,在使用代码压缩的工程中加入:

-keep class com.finogeeks.** {*;}

初始化放入宿主现有 Application。Kotlin 示例使用 FinStoreConfigFinAppConfigFinAppClientFinCallback 的官方接口;类名 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.harFinClipSDKCore.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.json5module.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.initstartApplet。初始化在用户点击后执行,此时页面已经创建,能够提供 UI 上下文;routerState 与页面上的 Navigation 使用同一个实例。示例将通信方式显式设为 md5,实际值按平台配置填写。已有工程统一保存客户端实例,不随页面重建重复初始化 SDK。

点击按钮后,小程序应显示 harmonystartApplet 的调用返回并不单独作为页面成功展示的证据,实际验收以测试页出现、参数正确和 SDK 日志为准。

inline_02_workflow

宿主登录与设备能力对接

小程序页面能够打开后,接下来要让它接上 APP 已有的业务环境。比如用户已经登录 APP,进入会员小程序时,应该能够继续查询自己的积分和订单,无需再走一遍注册流程。

项目可以通过容器提供的扩展机制,把获取业务登录态、发起支付、打开原生页面等操作封装为统一的宿主能力。小程序调用约定接口,各端宿主分别完成原生实现,再按一致的数据结构返回结果。登录过程由企业账号服务衔接,业务后台继续校验用户身份和访问权限。

对于扫码、拍照、定位等能力,三个客户端需要分别配置系统权限、接入相关 SDK 模块。业务侧保持统一的调用约定,平台差异由运行时和宿主适配处理。后续再增加新的小程序时,已经接好的登录、支付和设备能力就能继续复用。

返回行为也要与 APP 原有体验接上:小程序内部返回上一页,退出小程序则回到宿主入口;业务完成后需要刷新原生列表的,由宿主接收办理结果并更新页面。用户感受到的是同一个 APP 内连续的服务流程。

三端联调与发布检查

三台设备使用同一个小程序 AppID,分别从宿主按钮打开测试页。先看页面能否显示,再核对参数和返回行为,比直接接入完整业务更容易定位接入问题。

检查项操作与预期结果未通过时检查
初始化iOS 返回成功;安卓收到成功回调;鸿蒙完成初始化调用,并结合后续打开结果验证各端凭据、应用标识、平台地址和网络
页面加载三端都显示“多端运行测试”页面上架状态、宿主关联、AppID 和页面路径
参数传递分别显示 ios、android、harmonystartParams.query 与页面 onLoad 的参数读取
返回宿主关闭小程序后回到原入口,入口仍能再次使用页面控制器、Activity 或 Navigation 页面栈
业务更新修改测试页文字并发布新版,按 SDK 更新策略重新进入后确认版本发布范围、实际加载版本和本地缓存策略

测试页通过后,将相同的打开方式接到首页宫格、活动位或消息入口,再把目标路径换成真实业务页面。例如订单消息可以携带订单标识,直接进入详情页。随后在三个宿主中完成登录、查询、提交、返回的全流程,检查设备授权、网络中断和前后台切换。错误日志同时记录宿主版本、SDK 版本及小程序版本,方便区分客户端接入问题和业务更新问题。

统一发布与业务独立更新

三端接入完成后,小程序代码包进入同一个管理平台,经过体验验证、审核和发布,再由各端 SDK 获取适用版本。团队可以统一管理小程序资产,也可以通过灰度策略控制新版本的发布范围,结合各端测试结果逐步上线。

日常调整会员页面、增加活动专区、修改表单等业务更新,只要使用的能力已由当前宿主和运行时提供,就可以走小程序发布流程,无需等待三个主 APP 同步发版。容器 SDK 升级、新增原生能力等宿主工程变更,继续走各客户端的构建和发布流程。

业务更新与客户端更新分开后,团队的协作方式也会随之变化:业务开发人员维护共用的小程序,客户端人员维护各端运行环境和公共能力,运营人员在平台查看版本、审核和上线状态。新增业务可以继续接入现有容器,已有微信小程序资源也能在完成适配后加入自有 APP。

FinClip 将三端运行 SDK、开发者工具和管理平台连接起来,让同一套小程序拥有跨客户端运行和持续发布的能力。企业保留各端 APP 已有的原生体验,同时把频繁变化的业务交给共用小程序承载,减少重复开发,让功能上线与日常维护围绕同一份业务成果推进。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐