lzc-mobile-bridge
v0.1.1
Published
A typed JavaScript bridge contract for Lazycat mobile applications.
Maintainers
Readme
lzc-mobile-bridge
面向懒猫移动端独立应用的纯 JavaScript Bridge 契约。npm 包先定义稳定的前端 API,iOS 和 Android 宿主随后实现对应的 Native handler。
安装
npm install lzc-mobile-bridge使用
import {
closeInAppBrowser,
closeKeyboard,
getCurrentLanguage,
getCurrentTheme,
hideConfigurationPageThemeBar,
isAndroidClient,
isIndependentClient,
openConfigurationPage,
openInAppBrowser,
openKeyboard,
openSystemBrowser,
setCurrentLanguage,
setCurrentTheme,
} from "lzc-mobile-bridge"
const independent = await isIndependentClient()
const android = isAndroidClient()
// 推荐:打开配置页时一次性声明需要隐藏的栏目。
await openConfigurationPage({ hidden: ["theme", "language"] })
await openSystemBrowser("https://example.com/oauth")
await openInAppBrowser("https://example.com/help")
await closeInAppBrowser()
await closeKeyboard()
await openKeyboard()
await openKeyboard({ mode: "dark" })
const theme = await getCurrentTheme()
await setCurrentTheme("dark")
const language = await getCurrentLanguage()
await setCurrentLanguage("zh-Hans")旧宿主兼容调用:
await hideConfigurationPageThemeBar()
await openConfigurationPage()主题值:
lightdarksystem
语言值:
autozh-Hansen
Native 契约
宿主必须在页面加载前提供 Bridge。iOS 提供 globalThis.LzcClientBridge,Android 提供 globalThis.LzcClientBridgeNative,两端实现同一组能力:
| JavaScript API | Native method | 参数 | 返回值 |
| --- | --- | --- | --- |
| isIndependentClient() | IsIndependentClient | [] | boolean |
| hideConfigurationPageThemeBar() | HideConfigurationPageThemeBar | [] | null |
| openConfigurationPage(options?) | OpenConfigurationPage | [] 或 [{ hidden: ["theme" | "language", ...] }] | null |
| openSystemBrowser(url) | OpenSystemBrowser | [{ url }] | boolean |
| openInAppBrowser(url) | OpenInAppBrowser | [{ url }] | boolean |
| closeInAppBrowser() | CloseInAppBrowser | [] | boolean |
| closeKeyboard() | CloseKeyboard | [] | boolean |
| openKeyboard(options?) | OpenKeyboard | []、[{}] 或 [{ mode }] | boolean |
| getCurrentTheme() | GetCurrentTheme | [] | light \| dark \| system |
| setCurrentTheme(theme) | SetCurrentTheme | [theme] | null |
| getCurrentLanguage() | GetCurrentLanguage | [] | auto \| zh-Hans \| en |
| setCurrentLanguage(language) | SetCurrentLanguage | [language] | null |
浏览器打开方式
openSystemBrowser(url):交给系统浏览器打开;iOS 使用系统 URL 打开能力,Android 使用浏览器 Intent。openInAppBrowser(url):使用宿主集成的应用内浏览器;iOS 可使用SFSafariViewController,Android 使用产品约定的应用内浏览器页面。closeInAppBrowser():只关闭由当前宿主会话通过openInAppBrowser打开的页面,便于授权完成后自动返回原页面。- 两者只接受带显式协议的绝对
http:或https:URL,统一传递[{ url }]并返回Promise<boolean>。 true表示宿主已接受打开请求,不表示目标网页已经加载完成;合法请求因缺少页面容器或系统拒绝时返回false。- 关闭返回
true表示已接受关闭请求而非动画已经完成;没有当前会话拥有的应用内浏览器时返回false。
旧版 client_OpenSafariVC(url) 和 openExternal(url) 仅保留兼容,不再作为新业务入口。
键盘控制
export type KeyboardMode = "default" | "dark"
export interface OpenKeyboardOptions {
readonly mode?: KeyboardMode
}closeKeyboard():关闭当前获得焦点的输入控件所使用的软键盘。openKeyboard():为当前已聚焦、可编辑的输入控件请求打开软键盘;没有有效焦点时返回false。openKeyboard({ mode: "default" }):请求常规键盘外观。openKeyboard({ mode: "dark" }):请求暗黑键盘外观。- 不传
mode时 Native 不覆盖当前外观,由 UI 层自行决定。 - Android 的键盘外观由输入法实现控制,
mode只作为偏好提示,不能保证第三方输入法一定采用指定外观。
配置页参数方案
openConfigurationPage() 的业务方 API 是无参或传入一个 options 对象,不需要手动传递 []:
await openConfigurationPage()
await openConfigurationPage({ hidden: ["theme"] })
await openConfigurationPage({ hidden: ["theme", "language"] })类型定义:
export type ConfigurationPageHiddenSection = "theme" | "language"
export interface OpenConfigurationPageOptions {
readonly hidden?: readonly ConfigurationPageHiddenSection[]
}参数语义:
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| hidden | ("theme" | "language")[] | 要隐藏的配置栏目;不传表示使用 Native 默认显示策略 |
| hidden: ["theme"] | - | 隐藏主题设置栏 |
| hidden: ["language"] | - | 隐藏语言设置栏 |
| hidden: ["theme", "language"] | - | 同时隐藏主题和语言设置栏 |
Bridge 内部协议如下:
| JS 调用 | Native 参数 |
| --- | --- |
| openConfigurationPage() | [] |
| openConfigurationPage({ hidden: ["theme"] }) | [{ "hidden": ["theme"] }] |
| openConfigurationPage({ hidden: ["theme", "language"] }) | [{ "hidden": ["theme", "language"] }] |
这里的 [] 是 Bridge 内部协议,不是业务方调用格式。
相比 Hidden[theme,language] 字符串,结构化参数更适合长期维护:有 TypeScript 类型提示、运行时校验、跨平台 JSON 解析,也可以在未来扩展 readonly、默认页签或其他配置,而不需要重新设计字符串语法。
运行时会拒绝未知栏目:
await openConfigurationPage({ hidden: ["theme", "invalid"] })
// throws TypeErrorNative 实现要求
Native 宿主需要将 OpenConfigurationPage 从“只接受空参数”升级为“接受空参数或一个 options 对象”。Native 应先解析并校验参数,再进入配置页;参数非法时返回 INVALID_ARGUMENT,不要静默忽略。
推荐处理顺序:
- 解析
parameters;空数组表示使用默认显示策略。 - 如果存在参数,要求第一个参数是对象,且
hidden只能包含theme、language。 - 将隐藏配置写入本次配置页路由/状态。
- 打开配置页并返回
null。
配置页的显示策略应绑定在本次打开请求上,不建议通过全局可变状态传递,否则连续打开不同配置页时容易发生状态串扰。
iOS 接入
BridgeKit 的注册层需要让 OpenConfigurationPage 读取请求参数。Provider 可采用如下形式(具体类型名以宿主使用的 BridgeKit 版本为准):
struct ConfigurationPageOptions: Decodable, Sendable {
let hidden: [String]
}
@MainActor
final class AppClientConfiguration: LzcClientConfigurationProvider {
let store: HPortalShellStore
init(store: HPortalShellStore) {
self.store = store
}
func openConfigurationPage(options: ConfigurationPageOptions?) async throws {
let hidden = Set(options?.hidden ?? [])
guard hidden.isSubset(of: ["theme", "language"]) else {
throw LzcBridgeError.invalidPayload("Invalid configuration page hidden sections")
}
store.state.configurationHiddenSections = hidden
store.state.screen = .configuration
}
}如果旧版 BridgeKit 的 Provider 仍只有 openConfigurationPage() 无参方法,则旧宿主只能支持 openConfigurationPage(),不能支持新的 hidden options;此时应升级宿主 BridgeKit/注册层,而不是在 Web 页面自行拼接字符串。
Android 接入
Android 的 addJavascriptInterface 适合接收基础类型,不适合直接接收 JavaScript 对象。因此:
openConfigurationPage()无参数时,直接调用OpenConfigurationPage()。openConfigurationPage({ hidden: [...] })有结构化参数时,通过call(callId, "OpenConfigurationPage", parametersJson)传输。
class LzcClientBridgeInterface(private val webView: WebView) {
@JavascriptInterface
fun OpenConfigurationPage() {
openConfigurationPage(hidden = emptySet())
}
@JavascriptInterface
fun call(callId: String, method: String, parametersJson: String) {
try {
when (method) {
"OpenConfigurationPage" -> {
val request = parseConfigurationPageRequest(parametersJson)
openConfigurationPage(request.hidden)
sendSuccess(callId, null)
}
else -> sendUnsupported(callId, method)
}
} catch (error: IllegalArgumentException) {
sendError(callId, "INVALID_ARGUMENT", error.message ?: "Invalid arguments")
}
}
}Android 成功事件仍为 { callId, result: null },失败事件使用 { callId, error: { code, message } }。
浏览器与键盘能力通过同一个 call 入口接入:
OpenSystemBrowser、OpenInAppBrowser:严格解析[{ url }],只允许绝对 HTTP/HTTPS URL。CloseKeyboard:只接受[]。OpenKeyboard:接受[]、[{}]或[{ mode: "default" | "dark" }]。- 四个方法成功时都返回
boolean。OpenKeyboard只有当前 WebView 存在已聚焦、可编辑目标时才请求显示输入法。 - Android 输入法主题由具体 IME 控制;
mode只能作为偏好提示,省略时不得改写 UI 当前选择。
兼容策略
| 宿主能力 | openConfigurationPage() | openConfigurationPage({ hidden }) |
| --- | --- | --- |
| 旧版宿主,只支持无参 OpenConfigurationPage | 支持 | 返回不支持/参数错误 |
| 新版 iOS/Android 宿主 | 支持 | 支持 |
| 已有 hideConfigurationPageThemeBar() 宿主 | 支持 | 建议继续使用旧接口,或升级 Native |
hideConfigurationPageThemeBar() 会继续保留,保证已接入旧版本的应用不需要立即改造。新接入优先使用 openConfigurationPage({ hidden: ["theme"] }),因为打开页面和页面显示配置在一次请求中完成,行为更可预测。
调用 hideConfigurationPageThemeBar() 后,Native 宿主应隐藏通用配置页中的主题栏。该操作应支持重复调用。
Transport 接口:
interface BridgeTransport {
readonly ready?: Promise<boolean>
call(method: string, parameters?: BridgeValue[]): Promise<BridgeValue>
has?(method: string): boolean
}Android 传输细节
lzc-mobile-bridge 对外统一返回 Promise,但 Android Native 内部按方法是否能立即完成选择两条路径:
- Native 暴露对应直接方法时,基础类型/无参数调用可以直接执行并由 JavaScript 包装为 Promise。
- 结构化参数,或未暴露直接方法的调用,生成唯一
callId并调用LzcClientBridgeNative.call(callId, method, parametersJson)。Native 完成后向window发送lzc-mobile-bridge-response事件;这样结构化配置不会依赖 Android JavaScript Interface 对对象参数的转换行为。 - JavaScript 使用 pending Map 保存
callId对应的resolve/reject。收到事件后匹配调用,成功返回result,失败返回error,超过 15 秒则清理并拒绝 Promise。 - Native 可提供同步
has(method);存在时 transport 以它为能力真值,避免通用call()让尚未实现的方法被误判为支持。
成功事件:
window.dispatchEvent(new CustomEvent("lzc-mobile-bridge-response", {
detail: { callId, result: null },
}))失败事件:
window.dispatchEvent(new CustomEvent("lzc-mobile-bridge-response", {
detail: {
callId,
error: { code: "INVALID_ARGUMENT", message: "Invalid theme" },
},
}))设置类接口必须先写入 Native 状态,再发送成功事件。主题引发的 Activity 重建等后续动作应在事件送达后执行,保证 await setCurrentTheme() 完成后立即读取时能得到新值。
isAndroidClient() 不访问 Native,而是同步检查 Android 专用的 LzcClientBridgeNative 是否存在。
isIndependentClient() 会缓存成功结果,重复调用不会再次访问 Native;调用失败时会清除缓存并允许重试。
发布
npm run check
npm publish每次发布前必须更新 package.json 中的版本号。
