npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lzc-mobile-bridge

v0.1.1

Published

A typed JavaScript bridge contract for Lazycat mobile applications.

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()

主题值:

  • light
  • dark
  • system

语言值:

  • auto
  • zh-Hans
  • en

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 TypeError

Native 实现要求

Native 宿主需要将 OpenConfigurationPage 从“只接受空参数”升级为“接受空参数或一个 options 对象”。Native 应先解析并校验参数,再进入配置页;参数非法时返回 INVALID_ARGUMENT,不要静默忽略。

推荐处理顺序:

  1. 解析 parameters;空数组表示使用默认显示策略。
  2. 如果存在参数,要求第一个参数是对象,且 hidden 只能包含 themelanguage
  3. 将隐藏配置写入本次配置页路由/状态。
  4. 打开配置页并返回 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 入口接入:

  • OpenSystemBrowserOpenInAppBrowser:严格解析 [{ url }],只允许绝对 HTTP/HTTPS URL。
  • CloseKeyboard:只接受 []
  • OpenKeyboard:接受 [][{}][{ mode: "default" | "dark" }]
  • 四个方法成功时都返回 booleanOpenKeyboard 只有当前 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 内部按方法是否能立即完成选择两条路径:

  1. Native 暴露对应直接方法时,基础类型/无参数调用可以直接执行并由 JavaScript 包装为 Promise。
  2. 结构化参数,或未暴露直接方法的调用,生成唯一 callId 并调用 LzcClientBridgeNative.call(callId, method, parametersJson)。Native 完成后向 window 发送 lzc-mobile-bridge-response 事件;这样结构化配置不会依赖 Android JavaScript Interface 对对象参数的转换行为。
  3. JavaScript 使用 pending Map 保存 callId 对应的 resolve/reject。收到事件后匹配调用,成功返回 result,失败返回 error,超过 15 秒则清理并拒绝 Promise。
  4. 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 中的版本号。