原生APP嵌入业务页面,Ionic Portals与FinClip两种技术对比分析
如果原生APP已经有了成熟的账号体系、导航和业务接口,前端团队又交付了一套 React 或 Vue 页面。现在希望把这些页面接进APP,并让后续页面改动独立发布,今天分享一下Ionic Portals 和 FinClip的技术差异对比~
迁移时,可以沿着一个页面的执行过程往下看:Web 产物放在哪里,原生端怎样打开它,JavaScript 怎样调用宿主,下载新资源后又在什么时候切换。今天基于安卓端的系统,来分享一下这些接口分别落在哪一层。
保留现有 Web 页面,可以直接比较两种 H5 接入路径
Portals 基于 Capacitor,为原生 APP 提供增强 WebView。一个 Portal 对应一组 Web 资源和运行配置,页面仍由 Web 框架渲染,也不要求界面必须使用 Ionic Framework。
FinClip 除了运行小程序,也支持将现有 Web 项目打包成 H5 应用。这种方式可以保留 HTML、CSS 和 JavaScript,再适配运行环境。如果决定迁入小程序页面,才需要进一步处理页面模板、样式和小程序 API。把这两条路径分开,迁移工作量就更容易判断。
| 对比项 | Ionic Portals | FinClip |
|---|---|---|
| Web 运行单元 | Portal,关联 Web 资源目录、插件和配置 | H5 应用包,配置入口文件并接入运行环境 |
| 原生页面接入 | PortalView / PortalFragment,可加入原生布局 | 常规接入按应用标识启动;原生子视图嵌入需另看小组件方案 |
| 前端框架 | 保留 React、Vue 等 Web 渲染方式 | H5 应用可保留 Web 渲染;小程序路径需适配页面体系 |
| 宿主通信 | Initial Context、发布订阅、Capacitor 插件 | H5 应用自定义 API;小程序内 web-view 另用 JSSDK |
PortalView 直接进入原生布局,宿主决定页面怎么展示
假设前端构建目录为 dist,把产物复制到 Android 的 src/main/assets/business/,让其中的 index.html 成为入口。原生端注册 Portal 时指定这个目录,再在 Activity 中创建视图:
// Application.onCreate:注册一次;授权值由项目构建配置注入。
PortalManager.register(BuildConfig.PORTALS_KEY)
PortalManager.newPortal("business")
.setStartDir("business")
.addPlugin(HostContextPlugin::class.java)
.create()
// AppCompatActivity.onCreate:也可以把这个视图加入现有布局。
val businessView = PortalView(this, "business")
setContentView(businessView)
这里的 HostContextPlugin 是项目自行实现的 Capacitor 插件,用来返回宿主上下文。PortalView 随 Activity 的视图生命周期创建和释放,避免把它长期保存在全局对象中。需要 Fragment 或 Compose 布局时,也可以选用对应的挂载方式。
宿主保留原生导航和页面布局,Web 应用负责自己的表单、列表和内部路由。启动时传少量参数可以用 Initial Context,页面间交换消息可以用 Portals 的发布订阅能力;需要调用登录、相机等原生操作时,再接入 Capacitor 插件。
FinClip 按应用包启动,已有 Web 产物可以进入管理流程
在 FinClip 管理端创建 H5 应用,将 Web 构建产物纳入项目,再用 FinClip Studio 打包上传。假设项目根目录下已有 dist,可以配置入口:
{
"miniprogramRoot": "dist",
"entryFile": "index.html"
}
Android 宿主完成 SDK 初始化、配置好应用关联后,通过服务器地址和应用标识打开它。下面是 Activity 中的接入片段,onFallback 由项目传入,用来返回已有原生入口或展示重试页面;BuildConfig 中的配置项也由项目定义。
fun Activity.openBusinessApp(appId: String, onFallback: () -> Unit) {
if (appId.isBlank()) {
onFallback()
return
}
FinAppClient.appletApiManager.startApplet(
this,
IFinAppletRequest.fromAppId(BuildConfig.FINCLIP_API_SERVER, appId),
object : FinSimpleCallback<String?>() {
override fun onError(code: Int, error: String?) {
runOnUiThread { onFallback() }
}
}
)
}
startApplet 也支持 H5 应用类型,但这个调用与“把一个 View 塞进布局”有不同的接入职责:宿主启动由运行环境管理的业务界面。如果需求是在原生页面中放一个固定高度的业务区域,需要单独评估 FinClip 小组件形态及其支持范围。
Web 项目打包后,还要检查资源引用与请求地址。FinClip H5 应用文档给出的规则是:相对路径会按包内文件处理,访问外部服务需要完整地址。原来依赖网站反向代理的 /api/orders,迁移时就要调整 API base URL;静态文件则应进入包内,并检查构建工具生成的资源路径。
桥接调用加一层适配,业务函数可以继续复用
如果页面到处直接调用 Capacitor 插件,换运行环境时就得逐处修改。可以让业务只依赖项目定义的 getHostContext(),约定返回 bridgeVersion、route 等字段,再分别实现运行层适配。
Portals 侧,通过 Capacitor 注册同名插件并调用方法:
// adapters/portals.js
import { registerPlugin } from '@capacitor/core';
const HostContext = registerPlugin('HostContext');
export function getHostContext() {
return HostContext.getContext();
}
原生 HostContextPlugin 的注册名称需要与 HostContext 一致,并实现 getContext 方法。插件内部可以继续调用宿主已有的账号服务,只需把结果转换为约定的数据结构。权限不足或宿主能力缺失时,通过插件的失败结果返回给前端。
FinClip H5 应用侧,通过基础库提供的 ft.loadExtApi 声明自定义 API,再将回调调用包装成 Promise:
// adapters/finclip-h5.js:在 H5 应用基础库就绪后调用。
export async function getHostContext() {
const ft = globalThis.ft;
if (typeof ft?.loadExtApi !== 'function') {
throw new Error('宿主桥接尚未就绪');
}
ft.loadExtApi([{ name: 'hostGetContext', sync: false, params: {} }]);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('宿主调用超时')), 5000);
const fail = error => {
clearTimeout(timer);
reject(new Error(error?.errMsg || '宿主调用失败'));
};
try {
ft.hostGetContext({
success: result => { clearTimeout(timer); resolve(result); },
fail
});
} catch (error) {
fail({ errMsg: error.message });
}
});
}
hostGetContext 是项目自定义接口,Android 端需要实现 BaseApi,并通过 FinAppClient.extensionApiManager.registerApi(...) 注册,成功结果按双方约定返回字段。loadExtApi 负责声明调用接口,不会自动生成原生实现。H5 应用采用这种配置方式;如果改用小程序内的 web-view,接入的是另一组 Web API 注册和 JSSDK 调用,不能混用。
两端适配好后,业务层只调用 getHostContext()。原来的登录服务、文件处理或导航服务可以保留在原生端,Capacitor 插件包装层需要改为 FinClip 自定义 API 包装层。这样一来,页面上的业务调用点可以保持稳定。
页面路由与宿主返回分开处理,避免迁移后跳错页面
Web 应用内部的 /form,原生 APP 的 Activity,以及小程序的 /pages/form/index,分别属于不同的路由体系。Portals 可以通过启动上下文把业务路由交给 Web 应用;FinClip H5 应用也可以由项目桥接接口返回路由信息,让 Web 路由器完成跳转。小程序启动参数中的 path 则对应小程序页面路径。
业务层可以共用下面这段启动处理。router 是项目已有的 Web 路由器,两个运行环境分别注入上面的适配函数;未知路径回到表单首页,桥接失败则交给页面展示重试入口。
export async function enterBusiness(getHostContext, router, showRetry) {
try {
const context = await getHostContext();
const bridgeVersion = Number(context?.bridgeVersion);
if (!Number.isFinite(bridgeVersion) || bridgeVersion < 1) {
throw new Error('宿主桥接版本不支持');
}
const routes = new Set(['/form', '/history']);
const path = routes.has(context.route) ? context.route : '/form';
await router.replace(path);
} catch (error) {
showRetry(error.message);
}
}
FinClip H5 应用文档提示运行环境对路由事件有限制,因此依赖浏览器历史事件的路由实现需要适配。可以选用 Web 路由器的内存模式,在页面内维护路由,再由宿主返回事件驱动回退。Portals 也要处理原生返回与 Web 页面栈的关系:页面还能回退时留在业务内部,回到业务首页后再退出原生入口。
原生导航跳转应由宿主校验目标和参数,再执行已有导航服务;页面传入的业务路径只在约定范围内解析。宿主升级桥接协议时保留旧字段,让旧资源包也能继续调用。
业务资源独立更新,下载完成后仍要安排切换时机
Portals 的本地 assets 随 APP 安装包发布。接入 Live Updates 后,Web 资源可以单独下载:Appflow 通过应用标识和 channel 配置;当前官方文档还提供 Live Update Provider 接口,可以对接支持该协议的其他更新服务或自建服务。
| 更新环节 | Ionic Portals | FinClip |
|---|---|---|
| 本地资源 | Web assets 可随原生包内置 | 可配置离线资源;接入配置与线上分发区分处理 |
| 远程版本来源 | Appflow,或接入 Live Update Provider | 管理平台中的应用包与已发布版本 |
| 更新触发 | Appflow 默认配置可自动同步;Provider 需主动调用同步,或由服务 SDK 调度 | 常规打开流程先使用缓存,再检查并下载新版;可另配强制更新 |
| 新资源生效 | 下次加载 Portal,或按宿主逻辑重建视图 | 默认流程在下次打开时使用新版;强制更新与热启动策略另行配置 |
| 原生桥接修改 | 插件原生代码随 APP 发布 | 自定义 API 的原生实现随 APP 发布 |
使用 Provider 时,配置入口为 setLiveUpdateProviderManager(...),同步调用为 syncProvider() 或 syncProviderAsync()。Portals 不会自行调用 Provider 的同步接口,不能只配好 manager 就等待更新自动发生。Appflow 的构建期资源同步,也要与客户端运行时下载区分开。
FinClip 的 H5 应用基础库会打进应用包。业务要使用新的基础库能力,需要用对应 Studio 重新打包;基础库中已有的接口,仍要有宿主 SDK 或自定义原生实现支持。两种方案都应把 Web 资源版本和宿主桥接版本记录在一起,发布范围按宿主能力筛选。
当用户正在填写表单时,后台可以下载资源,页面切换安排在业务完成后或下一次进入。升级前先分组发布,保留上一版可用资源及发布记录;回退资源也要确认它与当前桥接协议兼容。采用自建更新源时,还要在分发链路落实包签名或完整性校验,记录实际加载版本。
复用清单明确后,运行层迁移更容易估算
| 已有代码或配置 | 保留 Web 的迁移方式 | 需要适配的内容 |
|---|---|---|
| React / Vue 组件、HTML、CSS | Portals 与 FinClip H5 应用都可沿用 Web 渲染 | 入口、资源路径、窗口样式、键盘与生命周期 |
| 校验函数、数据转换、业务计算 | 独立 JavaScript 模块通常可继续使用 | 排查对浏览器全局对象、运行时 API 的依赖 |
| HTTP 业务接口与后端模型 | 后端协议可以保留 | API base URL、登录凭据传递、请求实现及跨域行为 |
| Web 路由与页面参数 | 保留业务路径和参数语义 | 路由事件、启动入口、原生返回与页面栈 |
| Capacitor 插件 | 原生业务服务实现可以继续复用 | 插件注册、JS 调用、回调和权限包装 |
| 发布流水线 | Web 构建过程可以保留 | Portals 的资源目录与更新源,或 FinClip 的 H5 打包上传流程 |
已有页面围绕 DOM 和 Web 组件库开发时,Portals 的视图挂载方式方便继续组织原生布局;FinClip H5 应用则让 Web 产物进入应用包的接入与版本管理流程。后续如果需要迁入小程序体系,业务计算和接口层还能继续复用,页面与运行时调用需要进一步调整。迁移成本可以按这份清单逐项估算:保留业务代码,替换桥接适配,重新接好入口和更新链路。