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

@lark-base-open/base-site-channel

v0.1.1

Published

用于宿主页面与 iframe 页面之间的双向调用。

Readme

@lark-base-open/base-site-channel

用于宿主页面与 iframe 页面之间的双向调用。

适用场景

  • 宿主页面需要嵌入一个 iframe 页面
  • 宿主页面需要主动调用 iframe 内提供的能力
  • iframe 页面也需要反向调用宿主页面提供的能力
  • 希望通过统一的调用方式管理双方通信

对外能力

  • Host 侧创建 iframe 容器并加载目标页面
  • Host 侧监听页面加载状态
  • Host 侧注册可被 Client 调用的方法
  • Host 侧主动调用 Client 提供的方法
  • Host 侧订阅 Client 派发的事件
  • Host 侧主动向 Client 派发事件
  • Client 侧注册可被 Host 调用的方法
  • Client 侧主动调用 Host 提供的方法
  • Client 侧订阅 Host 派发的事件
  • Client 侧主动向 Host 派发事件
  • Client 侧直接使用内置批量二进制拉取能力,从 Host 内部 worker 拉取 URLList

导出内容

import { Host, LoadStatus, client } from "@lark-base-open/base-site-channel";

快速开始

1. 在 Host 页面创建通信实例

Host 负责:

  • 创建 iframe
  • 加载 client 页面地址
  • 监听加载状态
  • 注册给 client 使用的方法
  • 调用 client 方法
  • 订阅 client 事件
  • 派发事件给 client
import { Host, LoadStatus } from "@lark-base-open/base-site-channel";

const container = document.getElementById("app");

if (!container) {
  throw new Error("container not found");
}

const host = new Host(
  container,
  (status, errorInfo) => {
    if (status === LoadStatus.ERROR) {
      console.error("页面加载失败", errorInfo);
    }
  },
  "demo-block",
);

host.setClientUrl("https://example.com/view");

2. 在 Host 页面注册可被 Client 调用的方法

host.registerApi<{ userId: string }, { name: string }>(
  "getUserInfo",
  async (params) => {
    return {
      name: `user-${params.userId}`,
    };
  },
);

3. 在 Host 页面主动调用 Client 方法

const result = await host.invokeApi<{ keyword: string }, { total: number }>(
  "searchData",
  {
    keyword: "hello",
  },
);

4. 在 Host 页面订阅 Client 事件

const unsubscribe = host.subscribeEvent<{ count: number }>(
  "dataChange",
  (payload) => {
    // 监听 client 派发的事件
    console.log("host receive dataChange", payload.count);
  },
);

// 不再需要时取消订阅
unsubscribe();

5. 在 Host 页面主动向 Client 派发事件

await host.emitEvent("themeChange", {
  theme: "dark",
});

Client 页面使用方式

Client 侧直接使用库导出的单例 client,适合在 iframe 页面中直接接入。

1. 在 Client 页面注册可被 Host 调用的方法

import { client } from "@lark-base-open/base-site-channel";

client.registerApi<{ keyword: string }, { total: number }>(
  "searchData",
  async (params) => {
    return {
      total: params.keyword.length,
    };
  },
);

2. 在 Client 页面主动调用 Host 方法

const userInfo = await client.invokeApi<{ userId: string }, { name: string }>(
  "getUserInfo",
  {
    userId: "1001",
  },
);

3. 在 Client 页面订阅 Host 事件

const unsubscribe = client.subscribeEvent<{ theme: string }>(
  "themeChange",
  (payload) => {
    // 监听 host 派发的事件
    console.log("client receive themeChange", payload.theme);
  },
);

// 不再需要时取消订阅
unsubscribe();

4. 在 Client 页面主动向 Host 派发事件

await client.emitEvent("dataChange", {
  count: 1,
});

推荐使用流程

Host 侧流程

  1. 准备一个用于挂载 iframe 的容器元素
  2. 创建 Host 实例,建议传入唯一的 instanceId
  3. 先通过 registerApi 暴露宿主能力
  4. 再调用 setClientUrl 加载 iframe 页面
  5. 在需要监听 client 状态变化时通过 subscribeEvent 订阅事件
  6. 在业务需要时通过 invokeApi 调用 client 能力,或通过 emitEvent 派发事件
  7. 页面卸载时调用 destroy

Client 侧流程

  1. 在 iframe 页面中引入 client
  2. 通过 registerApi 暴露 iframe 页面能力
  3. 在需要监听 host 状态变化时通过 subscribeEvent 订阅事件
  4. 在业务需要时通过 invokeApi 调用 host 能力,或通过 emitEvent 派发事件

加载状态说明

LoadStatus 包含以下几种状态:

  • pending:初始状态
  • loading:正在加载 iframe 页面
  • loaded:页面已可正常通信
  • error:加载失败或通信不可用

常见处理方式:

  • loading 时展示加载中状态
  • loaded 后再执行依赖 iframe 的业务操作
  • error 时展示错误提示,并根据需要允许重试

构造参数说明

new Host(containerEl, onLoadStatusChange, instanceId, allowedOrigin, extraAttributes, options)

  • containerEl:iframe 挂载容器
  • onLoadStatusChange:页面状态变化回调
  • instanceId:当前通信实例标识,建议保持唯一
  • allowedOrigin:允许通信的绝对 URL 或 origin 列表;非法配置会被拒绝,不支持正则或通配符
  • extraAttributes:附加到 iframe 上的属性
  • options:Host 可选配置对象
  • options.enableBinaryFetch:是否允许 Client 调用内置二进制 fetch,默认 false;Site Runtime 接入必须保持关闭

通信日志

Host 与 Client 的握手、invokeApi/registerApiemitEvent/subscribeEvent、回调和销毁日志统一在 Channel API 内部记录。运行环境存在 window.BearWebSlardarWeb 时通过 sendLog 上报,统一使用 event_name=Runtime_SiteChannel_Message,并通过 channel_sidechannel_messagekey 等脱敏字段区分阶段;没有 Slardar 实例或实例调用失败时,降级到对应级别的 console.info/warn/error

Client 调用 invokeApi 时生成 logId 并记录 api request sent;Host 收到后记录 api request received,返回时记录 api response sent 并复用同一个 logId;Client 收到回调后记录 api response received。这些日志只记录操作阶段、方法 key、状态和错误类型等必要元数据,不记录请求参数或响应内容;标识符只记录是否存在。业务方不需要在每个 Channel 调用点重复打印通信日志。

invokecallback 已由上述 API 语义日志覆盖,不再额外记录 send message to host/clientreceive host/client message;通用收发日志仅用于握手、事件等其他消息类型。

Site Runtime

本包同时提供 Site iframe 专用 Runtime 门面,业务方不需要直接操作底层 Channel:

import {
  getCurrentUser,
  invokeHttp,
  logout,
  requestEdit,
  siteRuntime,
} from "@lark-base-open/base-site-channel/runtime";

const response = await invokeHttp({ url: "/base-site/api/example" });
const currentUser = await getCurrentUser();
await requestEdit();
await logout();
await siteRuntime.ready();
  • ./runtime:子页面使用的 HTTP、Function、生命周期和 Chatbot stream API
  • ./runtime-contract:父子页面共享的 site.v1 key、payload、类型和稳定错误码
  • requestEdit():通知父页面申请进入编辑态
  • getCurrentUser():读取父页面登录用户信息,稳定包含 nameavatarUrl
  • logout():请求父页面退出当前登录态
  • 上述三个方法统一通过 site.v1.function.invoke 调用内置 Runtime Function,不新增平级 Channel API

常用方法说明

Host

  • setClientUrl(url):设置并加载 client 页面地址
  • registerApi(key, handler):注册给 client 使用的方法
  • invokeApi(key, params):调用 client 方法
  • subscribeEvent(key, handler):订阅 client 派发的事件,返回取消订阅函数
  • unsubscribeEvent(key, handler?):取消指定事件订阅,不传 handler 时移除该事件全部订阅
  • emitEvent(key, params):主动向 client 派发事件
  • destroy():销毁实例并释放资源

Client

  • registerApi(key, handler):注册给 host 使用的方法
  • invokeApi(key, params):调用 host 方法
  • subscribeEvent(key, handler):订阅 host 派发的事件,返回取消订阅函数
  • unsubscribeEvent(key, handler?):取消指定事件订阅,不传 handler 时移除该事件全部订阅
  • emitEvent(key, params):主动向 host 派发事件
  • fetchBinaryList(params):发起内置批量二进制拉取任务,返回 { jobId }
  • subscribeBinaryFetchProgress(handler):订阅内置批量二进制拉取进度
  • cancelBinaryFetch(jobId):取消指定的二进制拉取任务

内置批量二进制拉取能力

这套能力用于 Host <-> iframe 场景下的大文件、图片密文或附件密文传输。

为什么 iframe 里直接拉 URL 会被拦

在很多接入场景里:

  • iframe 页面运行在外网沙箱域名
  • 文件下载地址位于内网域名或受限 CDN 域名
  • iframe 内直接执行 fetch(url).arrayBuffer() 时会受到浏览器 CORS 限制

即使业务已经拿到下载地址,只要浏览器判定当前 iframe 页面来源无权访问该资源,请求仍然会失败。

为什么把拉取动作放到 Host

Host 页面通常运行在具备访问权限的环境中,可以正常访问这些内网或受限域名。因此更合理的方案是:

  1. iframe 只负责描述“要拉哪些 URL”
  2. Host 在内部 worker 中真正执行批量下载
  3. 每个文件下载完成后立刻把完整 ArrayBuffer 渐进式回传给 iframe
  4. iframe 拿到密文 bytes 后再自行解密、生成 BlobblobUrl

业务层不需要自己额外封装 host worker,也不需要自己设计底层 postMessage 协议。

为什么回传要使用 Transferable

二进制文件可能很大,如果直接走普通结构化拷贝,主线程和 iframe 之间会产生额外复制成本。内置能力会使用:

postMessage(message, "*", [arrayBuffer]);

ArrayBuffer 作为 Transferable 传输,避免大块二进制在通信层重复拷贝,更适合图片密文、附件密文这类场景。

对外 API

Client / iframe 侧直接使用以下内置 API:

type BinaryFetchItem = {
  itemId: string;
  url: string;
};

type BinaryFetchParams = {
  items: BinaryFetchItem[];
  concurrency?: number;
  credentials?: 'omit' | 'same-origin' | 'include';
};

type BinaryFetchProgress =
  | {
      type: 'job-start';
      jobId: string;
      total: number;
    }
  | {
      type: 'item-complete';
      jobId: string;
      itemId: string;
      totalBytes?: number;
      buffer: ArrayBuffer;
    }
  | {
      type: 'item-error';
      jobId: string;
      itemId: string;
      error: string;
    }
  | {
      type: 'job-complete';
      jobId: string;
      successCount: number;
      failCount: number;
    }
  | {
      type: 'job-cancelled';
      jobId: string;
    };

client.fetchBinaryList(params: BinaryFetchParams): Promise<{ jobId: string }>;
client.subscribeBinaryFetchProgress(handler: (progress: BinaryFetchProgress) => void): () => void;
client.cancelBinaryFetch(jobId: string): Promise<void>;

说明:

  • 不需要业务层手动 registerApi
  • Host 会在内部自动启用专用 worker
  • 默认支持并发控制
  • 单个 job 默认并发为 4
  • 单 worker 下的全局总并发上限默认是 6
  • credentials 可选,默认使用 same-origin
  • 每个文件下载完成后会一次性回传完整 ArrayBuffer
  • 如果握手未完成,fetchBinaryListcancelBinaryFetch 会像 invokeApi 一样直接 reject

如何发起 URLList 拉取

import { client } from "@lark-base-open/base-site-channel";

const { jobId } = await client.fetchBinaryList({
  items: [
    {
      itemId: "file-token-1",
      url: "https://internal-cdn.example.com/file-1",
    },
    {
      itemId: "file-token-2",
      url: "https://internal-cdn.example.com/file-2",
    },
  ],
  concurrency: 4,
  credentials: "include",
});

如何订阅 progress

const unsubscribe = client.subscribeBinaryFetchProgress((progress) => {
  switch (progress.type) {
    case "job-start":
      console.log("binary fetch job start", progress.jobId, progress.total);
      break;
    case "item-complete":
      console.log(
        "binary fetch item complete",
        progress.jobId,
        progress.itemId,
        progress.totalBytes,
      );
      break;
    case "item-error":
      console.error(
        "binary fetch item error",
        progress.jobId,
        progress.itemId,
        progress.error,
      );
      break;
    case "job-complete":
      console.log(
        "binary fetch job complete",
        progress.jobId,
        progress.successCount,
        progress.failCount,
      );
      break;
    case "job-cancelled":
      console.warn("binary fetch job cancelled", progress.jobId);
      break;
  }
});

如何取消任务

await client.cancelBinaryFetch(jobId);
unsubscribe();

完整示例

下面的示例演示:

  • client 发起 fetchBinaryList
  • client 监听 item-complete
  • 业务层按 itemId 接收完整 ArrayBuffer
  • 最终在 iframe 中自行解密并生成 blobUrl
import { client } from "@lark-base-open/base-site-channel";

const decryptedUrlMap = new Map<string, string>();
let currentJobId = "";

const unsubscribe = client.subscribeBinaryFetchProgress(async (progress) => {
  if (progress.type === "job-start") {
    currentJobId = progress.jobId;
    console.log("job started", progress.jobId, progress.total);
    return;
  }

  if (progress.type === "item-error") {
    console.error("item fetch failed", progress.itemId, progress.error);
    return;
  }

  if (progress.type === "item-complete") {
    const encryptedBuffer = progress.buffer;
    const decryptedBuffer = await decryptFileBytes(
      encryptedBuffer,
      progress.itemId,
    );
    const blob = new Blob([decryptedBuffer]);
    const blobUrl = URL.createObjectURL(blob);
    decryptedUrlMap.set(progress.itemId, blobUrl);
    console.log("blob url ready", progress.itemId, blobUrl);
    return;
  }

  if (progress.type === "job-complete") {
    console.log(
      "all items finished",
      progress.successCount,
      progress.failCount,
    );
    return;
  }

  if (progress.type === "job-cancelled") {
    console.warn("job cancelled", progress.jobId);
  }
});

const { jobId } = await client.fetchBinaryList({
  items: [
    { itemId: "image-a", url: "https://internal-cdn.example.com/image-a" },
    { itemId: "image-b", url: "https://internal-cdn.example.com/image-b" },
  ],
  concurrency: 4,
});

function decryptFileBytes(
  buffer: ArrayBuffer,
  itemId: string,
): Promise<ArrayBuffer> {
  console.log("decrypt file bytes", itemId);
  return Promise.resolve(buffer);
}

// 业务需要时可以取消
if (shouldCancelJob(jobId)) {
  await client.cancelBinaryFetch(jobId);
}

function shouldCancelJob(targetJobId: string) {
  return currentJobId === targetJobId && false;
}

进度消息结构

内置 progress 消息包含以下几种类型:

type BinaryFetchProgress =
  | {
      type: "job-start";
      jobId: string;
      total: number;
    }
  | {
      type: "item-complete";
      jobId: string;
      itemId: string;
      totalBytes?: number;
      buffer: ArrayBuffer;
    }
  | {
      type: "item-error";
      jobId: string;
      itemId: string;
      error: string;
    }
  | {
      type: "job-complete";
      jobId: string;
      successCount: number;
      failCount: number;
    }
  | {
      type: "job-cancelled";
      jobId: string;
    };

Event 使用说明

事件通信适合处理“状态变化通知”这类不需要返回值的场景,比如:

  • 宿主页面主题切换后通知 iframe 页面更新样式
  • iframe 页面数据变更后通知宿主页面刷新外层状态
  • 双方同步选中态、表单状态、可见性等轻量级状态

invokeApi 的区别:

  • invokeApi 适合请求-响应场景,调用方需要拿到返回值
  • emitEvent 适合单向通知场景,发送后不等待业务返回值
  • subscribeEvent 用于订阅对端派发的事件

Host 订阅 Client 事件

const stopListenDataChange = host.subscribeEvent<{ count: number }>(
  "dataChange",
  (payload) => {
    // 当 iframe 页面发生数据变化时,宿主页面会收到通知
    console.log("host receive dataChange", payload.count);
  },
);

Client 向 Host 派发事件

client.emitEvent("dataChange", {
  // 通知 host 当前数据已变化
  count: 1,
});

Client 订阅 Host 事件

const stopListenThemeChange = client.subscribeEvent<{
  theme: "light" | "dark";
}>("themeChange", (payload) => {
  // 监听 host 的主题变化事件
  document.body.dataset.theme = payload.theme;
});

Host 向 Client 派发事件

client.emitEvent("themeChange", {
  // 通知 client 同步主题
  theme: "dark",
});

取消订阅

推荐优先使用 subscribeEvent 返回的取消函数:

const unsubscribe = client.subscribeEvent<{ visible: boolean }>(
  "visibleChange",
  (payload) => {
    // 处理可见性变化
    console.log(payload.visible);
  },
);

unsubscribe();

也可以通过 unsubscribeEvent 主动移除:

const onVisibleChange = (payload: { visible: boolean }) => {
  // 处理可见性变化
  console.log(payload.visible);
};

client.subscribeEvent("visibleChange", onVisibleChange);
client.unsubscribeEvent("visibleChange", onVisibleChange);

使用建议:

  • 事件 key 建议使用明确的业务语义名称,例如 themeChangedataChange
  • 事件更适合通知类场景,不建议替代有返回值的接口调用
  • 建议在页面销毁、组件卸载时及时取消订阅,避免重复监听
  • 事件参数建议保持轻量、稳定,避免传递难以序列化的数据

invokeApi 报错说明

invokeApi 在调用失败时会返回一个被拒绝的 Promise,错误对象格式如下:

type InvokeApiError = {
  code: number;
  msg: string;
};

建议统一使用 try/catch 处理:

try {
  const result = await host.invokeApi<{ id: string }, { success: boolean }>(
    "saveDetail",
    {
      id: "1",
    },
  );
  console.log(result);
} catch (error) {
  const invokeError = error;
  console.error(
    "调用失败",
    invokeError?.code,
    invokeError?.msg || invokeError?.message,
  );
}

常见报错场景:

  • 业务接口报错
  • iframe 页面加载失败
  • 当前加载地址不在允许通信范围内
  • 页面未完成初始化,导致调用超时
  • 对端方法内部执行失败,并返回业务错误

常见错误码:

  • 业务接口错误码
  • -1:未知错误
  • 10001:iframe 加载失败
  • 10002:当前页面地址不允许通信
  • 10003:页面初始化超时
  • 10004:对端未注册该 API
  • 10005:调用超时
  • 10006:通道已销毁
  • 10007:内置二进制下载能力未开启
  • 10008:同一请求 id 被新请求顶替
  • 10009:当前环境不具备建联条件(非 iframe 环境、拿不到 host origin、Host 尚未 setClientUrl

处理建议:

  • loaded 状态后再调用 invokeApi
  • Host 侧先完成 registerApi,再执行 setClientUrl
  • 将业务异常转换为明确的 codemsg 返回,便于调用方统一处理

完整示例

Host 页面

import { Host, LoadStatus } from "@lark-base-open/base-site-channel";

const container = document.getElementById("app");

if (!container) {
  throw new Error("container not found");
}

const host = new Host(
  container,
  (status) => {
    if (status === LoadStatus.LOADED) {
      console.log("client 页面已就绪");
    }
  },
  "demo-block",
);

host.registerApi<{ id: string }, { title: string }>(
  "getDetail",
  async (params) => {
    return {
      title: `detail-${params.id}`,
    };
  },
);

const stopListenDataChange = host.subscribeEvent<{ count: number }>(
  "dataChange",
  (payload) => {
    console.log("host receive dataChange", payload.count);
  },
);

host.setClientUrl("https://example.com/client");

const response = await host.invokeApi<{ id: string }, { success: boolean }>(
  "saveDetail",
  {
    id: "1",
  },
);

host.emitEvent("themeChange", {
  theme: "dark",
});

stopListenDataChange();

Client 页面

import { client } from "@lark-base-open/base-site-channel";

client.registerApi<{ id: string }, { success: boolean }>(
  "saveDetail",
  async (params) => {
    return {
      success: params.id.length > 0,
    };
  },
);

const stopListenThemeChange = client.subscribeEvent<{ theme: string }>(
  "themeChange",
  (payload) => {
    document.body.dataset.theme = payload.theme;
  },
);

const detail = await client.invokeApi<{ id: string }, { title: string }>(
  "getDetail",
  {
    id: "1",
  },
);

client.emitEvent("dataChange", {
  count: 1,
});

stopListenThemeChange();

使用建议

  • key 建议按业务语义命名,避免重复
  • instanceId 建议为每个 iframe 实例保持唯一
  • 在 Host 页面销毁、切页或重新挂载时主动调用 destroy
  • 只有在 iframe 页面已经可用时再发起业务调用