省级融媒APP如何引入本地生活服务,构建自有小程序开发平台实践

32 阅读8分钟

一、当融媒APP也要转型~

2024年初,领导要求重新审视了APP的定位,过去是广播电视台的官方客户端,日活在本地媒体类APP里处于本地媒体类APP前列,核心还是以新闻为主,基本上只考虑对外传播,不需要考虑更多其他的内容。

后来在大的方针要求下,需要聚焦在本地,基于APP能够提供更多的服务,不仅仅是本地资讯,还需要引入一些本地生活相关的内容,像缴费、租车、社区服务、相亲、二手回收这些本地化特色的服务。

但作为一个规模并不大的团队,完全不可能自己开发几十种生活服务。团队规模有限,不仅时间周期长,而且技术实力也有限,虽然AI能够帮忙解决一些开发问题,但服务完全没办法去全部由一个团队来提供。

后面开始尝试,是不是可以把第三方服务商的小程序嵌入我们的APP,用户在我们这里一站式解决问题。其中比较重要的考虑就是——小程序的生态比较完善。大部分商家都已经有了自己的小程序,有开发经验,也知道小程序能做什么。以小程序形式入驻,不需要从零教商家这是什么,商家接受度远高于其他方案, 接下来的重点就是:如何让自己的APP拥有运行小程序的能力和如何构建一个后台进行管理这些小程序。

二、技术架构——SDK完整接入方案

cover-image.png

2.1 整体架构

技术架构方面还是直接引入第三方SDK之后,通过引入SDK的方式,让自己的APP拥有运行小程序的能力:

原生APP
  ├── 原生模块(登录、设置、支付等核心流程)
  ├── 小程序运行时(SDK)
  │     ├── 小程序容器(第三方服务商开发)
  │     └── WebView容器(支持原有H5页面接入)
  └── 小程序管理后台(统一管理所有小程序包、版本、灰度规则)

2.2 SDK初始化——双端代码对比

iOS和Android端的SDK初始化代码量都在40行以内,接口协议一致,这是我们选择这个方案的技术前提之一。

iOS侧在AppDelegate中初始化:

// iOS SDK 初始化
NSMutableArray *storeArrayM = [NSMutableArray array];
FATStoreConfig *storeConfig = [[FATStoreConfig alloc] init];
storeConfig.sdkKey = @"您的sdkKey信息";
storeConfig.sdkSecret = @"您的sdkSecret信息";
storeConfig.apiServer = @"服务端地址";
[storeArrayM addObject:storeConfig];
[[FATClient sharedClient] initWithConfig:storeArrayM];

Android侧的逻辑完全一致:

// Android SDK 初始化
if (FinAppClient.INSTANCE.isFinAppProcess(this)) {
    // 小程序进程不执行任何初始化操作
    return;
}
List<FinStoreConfig> storeConfigs = new ArrayList<>();
FinStoreConfig config = new FinStoreConfig();
config.setSdkKey("您的sdkKey信息");
config.setSdkSecret("您的sdkSecret信息");
config.setApiServer("服务端地址");
storeConfigs.add(config);
FinAppClient.init(this, storeConfigs);
FinAppClient.start();

两端初始化代码放在一起对比,接口命名规范一致,上手没有理解成本。

2.3 小程序启动与参数传递

SDK初始化完成之后,启动一个小程序只需要调用startApplet接口,并传入目标小程序的ID和可选的启动参数。启动参数支持指定小程序的启动页面路径和URL查询参数,与微信小程序的navigateTo行为一致。

Flutter端的调用方式:

// Flutter 启动小程序——触发热更新检查
Future<Map> startApplet(RemoteAppletRequest request)

RemoteAppletRequest request = new RemoteAppletRequest(
  apiServer: 'https://api.finclip.com',
  appletId: appId
);
request.startParams = {
  'path':'/pages/index/index',
  'query':'key1=value2&key2=value2'
};
Mop.instance.startApplet(request);

如果小程序包已预置在APP内(离线包),可以传入本地路径加快首次启动速度:

// Android 离线包启动——首次启动免下载
FinAppClient.appletApiManager.startApplet(
    this,
    IFinAppletRequest.Companion.fromAppId("https://api.finclip.com","5f17f457297b540001e06ebb")
        .setOfflineParams("$filesDir/framework-3.2.3.zip", "$filesDir/5f17f457297b540001e06ebb-1.0.44.zip")
)

离线包参数需要同时传入小程序包路径和框架包路径,缺一不可。离线包在APP打包时植入,属于APP本体的一部分,用户首次打开时无需等待网络下载。

其实也支持鸿蒙生态,但目前鸿蒙APP还没有提上日程,后续逐步引入。

2.4 原生与H5的双向通信桥接

我们平台上有部分原有业务以H5形式存在,需要在接入小程序容器后继续保留。SDK提供了原生与H5之间的双向通信桥接能力,H5页面可以调用宿主APP的原生方法,宿主APP也可以主动向H5页面推送数据。

H5调用宿主原生方法:在Flutter侧注册一个API扩展,H5引用FinClip桥接文件后即可通过window.ft.miniProgram.callNativeAPI调用:

// Flutter 注册 webview 拓展 API——使 H5 可调用宿主原生能力
void addWebExtentionApi(String name, ExtensionApiHandler handler)

Mop.instance.addWebExtentionApi('js2AppFunction', (params) async {
  print("js2AppFunction:$params");
  return {};
});

H5侧引用桥接文件后调用注册的方法:

// H5 调用宿主原生方法
window.ft.miniProgram.callNativeAPI('js2AppFunction', {name:'getLocation'}, (result) => {
    console.log(result)
});

宿主APP主动调用H5方法:原生端通过callJS接口向小程序内的H5页面推送数据,H5侧接收后执行相应逻辑:

// Flutter 原生调用 H5 页面的 JS 方法
Future<void> callJS(String appId, String eventName, String nativeViewId,
  Map<String, dynamic> eventData)

// 原生向小程序内的 H5 推送数据
Mop.instance.callJS('小程序id', 'app2jsFunction', 'webviewId', eventData);

这套双向通信机制让我们在接入新服务的同时保留了对存量H5页面的控制能力,不需要为了迁移到小程序而一次性重写所有业务。

2.5 多租户隔离

每个入驻的服务商在平台端对应独立的租户,数据完全隔离。平台方可以在后台统一配置每个小程序的可见范围:全员可见、区域可见、或者指定用户群可见,各服务商的运营数据互不可见。

三、热更新机制——后台发布,用户无感知

4168eb75e058c8fa579589d085a48e16.png

小程序包通过FinClip后台管理完整的生命周期:开发上传→审核→发布→用户打开时SDK自动检测并拉取最新包。整个过程与APP发版完全独立,小程序更新不需要APP重新提交应用市场审核。

热更新的链路是:用户在APP中打开某小程序,SDK向后台发起版本检测请求,后台返回最新版本号。如果检测到本地版本低于后台版本,SDK在后台静默下载最新包,用户下次进入时自动加载新版本。整个过程对用户无感知,不打断当前使用。

Android SDK 2.35.1版本开始支持配置离线小程序包路径,打开时优先使用离线包,后台更新包仍可正常拉取。首次打开无等待,后续版本迭代通过热更新完成。

后台会自动计算与用户当前版本的差异包,用户只下载变更部分,而非完整包。差量更新的包体大小通常只有完整包的10%-30%。[待核实:具体差量包体积压缩比,建议实测后补充]

SDK支持配置更新策略:

更新策略行为
冷启动更新APP启动时检测并下载,用户下次进入看到新版
热启动更新小程序在前台时后台静默下载,用户退出后重新进入时切换

对于生活服务类小程序,建议采用热启动更新策略,避免打断用户正在进行的操作。

四、灰度发布——后台配置,无需改代码

在管理后台创建小程序发布时,可以配置灰度规则,支持按百分比、按地区、按用户群三种维度。新版先在小范围验证,确认无问题后切换为全量。整个过程在后台操作,不需要重新发版。

灰度维度适用场景
百分比灰度新功能上线,先让10%用户试用,观察数据再扩量
地区灰度本地化服务,按城市或区域下发
用户群灰度精准匹配特定用户群体,按ID列表精确控制

灰度规则可以在发布后随时调整,SDK会自动感知并应用新的下发策略。

五、上线实战

scene-layers.png

第一个上线的"美食"小程序功能很简单:查看附近餐厅、领取优惠券、预约订位。技术上没有门槛,运营上容易管理,商家也愿意配合——聚焦到地市级别,还是有一些本地资源的。

上线第一个月,这个小程序的日均使用次数超过8000次,超过了APP本身资讯板块的阅读量。

第一个月跑通之后,第二个月上线了"商城"小程序,接入本地商超。第三个月上线了水电燃气在线缴费、公交查询、本地交友三个刚需功能。

六、几个心得分享

兼容微信生态很方便。 大部分小程序都是已经开发好的,很多第三方服务团队已经没有了二次开发的能力,好在现在的方案能够支持微信的生态,直接把微信小程序迁移过来,整体的成本降低了很多;

内容和服务之间需要刻意管理边界。 我们一度在资讯流里插入了生活服务的内容,以为这样可以引导用户发现新功能。结果用户反馈"APP越来越乱"。后来我们在产品上明确区分了"资讯"和"服务"两个Tab,用户按需进入对应模块,投诉率随之下降。

下一步还是需要引入更多本地的服务,虽然互联网巨头在全国是绝对性的领先,但具体到一些二三线城市,本地生活服务还是有一定的机会的~


需要的话可以在Gitee中了解一下:Gitee Finclip