# 请求数据与调用服务

在实际开发中，并非所有数据都需要像广播电台一样不间断地推送。

例如：
- **按需拉取**：天气卡片通常只需在用户点击“刷新”或定时触发时单次拉取最新预报；
- **插件间协作**：一个汇率转换动作，可以在执行时直接向专门的金融数据插件请求最新的货币换算结果。

这两种典型场景在飞块中分别通过**数据提供者（DataProvider）**和**插件服务（Service）**来优雅实现。

---

## 1. 从前端页面单次请求数据（requestData）

当超级块页面需要主动从后台拉取一次数据时，先在清单的 `dataProviders` 中声明该能力，并在后台入口中编写对应的响应函数。

在前端 HTML/JavaScript 页面中，直接调用页面 SDK 的 `requestData()`：

```javascript
async function refreshWeather() {
  try {
    // 向后台请求 'current' 数据源，可传入自定义查询参数
    const data = await window.FlyonDeckWidget.requestData('current', {});
    document.querySelector('#value').textContent = data.message;
  } catch (error) {
    // 优雅捕获网络异常或后台错误并提示用户
    document.querySelector('#value').textContent = `刷新失败：${error.message}`;
  }
}
```

后台的处理函数将安全接收到前端页面传递的输入参数、当前实例 ID 以及该实例保存的用户设置：



```js
const { t: tr } = require(process.env.FLYONDECK_SDK_PATH || "@flyondeck/plugin-sdk");
"use strict";
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);

definePlugin({
  dataProviders: {
    async current({ settings }, context) {
      // privateNote 只属于后台，绝不把 settings 整体返回或记录到日志。
      const todo = await context.network.getJson(
        `https://jsonplaceholder.typicode.com/todos/${settings.todoId || 1}`,
      );
      if (
        !todo ||
        typeof todo.title !== "string" ||
        typeof todo.completed !== "boolean"
      )
        throw new Error(tr("messages.index.external-service-returned-an-invalid-format"));
      return { message: todo.title, completed: todo.completed };
    },
  },
});
```



**架构设计优势：**
- **天然免除跨域（CORS）烦恼**：请求是由插件的后台 Node.js 进程发起的，前端页面无需配置复杂的代理或规避浏览器的跨域拦截。
- **保护核心敏感信息**：私密 API Key 或身份令牌可安全保存在后台内存或配置中，只将过滤后的干净数据返回给前端展示，杜绝凭证泄露。

> 📚 **深入参考**：清单字段规范见 [DataProvider 参考文档](../reference/data-providers.md)，前端 API 细节见 [requestData() 参考](../reference/page-sdk.md#requestdata)。

### 超级块首屏读取

**推荐“挂载时拉取一次权威快照，之后订阅推送”。** `onState()` 是订阅，`getState()` 只读页面已收到的缓存，两者都不会查询后台。PC 编辑器晚挂载、重新打开页面时，首份状态可能为 `null`，也可能是尚未校准的零值；实例配置已到达也不代表业务数据已同步。`mode === 'preview'` 同样不能保证已有当前快照。

在 `contributes.dataProviders` 中为对应 `widget` 声明一次性 `snapshot` 提供者（`lifetime: "request"`、`visibility: "instance"`），输入为空对象，返回值的 Schema 与状态快照一致。后台从权威记录读取数据，处理跨天等业务边界，并结合该实例的配置计算结果；需要同步其它视图时，再定向 `publish()` 同一份快照。

以下示意用于**已放置实例**：收到首份宿主上下文后请求一次，期间若有后续推送或上下文更新，就不让旧响应覆盖它。`render` 和 `showReadError` 由页面实现。

```js
const sdk = window.FlyonDeckWidget;
let requested = false, revision = 0, active = true;
const unsubscribe = sdk.onState((state, context) => {
  const atRequest = ++revision;
  render(state, context.settings);
  if (requested) return;
  requested = true; // 首份 state 为零值也需要校准
  sdk.requestData('snapshot', {}).then(snapshot => {
    if (active && revision === atRequest) render(snapshot, sdk.getSettings());
  }).catch(error => {
    if (active && revision === atRequest) showReadError(error);
  });
});
window.addEventListener('pagehide', () => {
  active = false;
  unsubscribe();
}, { once: true });
```

首次数据缺失或可信度未知时，显示加载状态或标记为待校准；不要用 `state ?? 0` 把“尚未同步”当成真实零值。失败时提供重试入口；实例或配置切换后重新校准。组件目录中**尚未放置**的卡片不能请求实例数据，应显示明确标注的示例，不要仅凭 `mode` 判断能否请求。

如果后台做了“状态不变就跳过发布”的去重，必须为新挂载页面保留首屏读取路径；无条件定时发布不能替代明确的初始同步约定。验证时先产生非零数据、再打开页面，分别覆盖初始 `null`、零值、真实非零值，以及“拉取返回前已有新推送”的情况。出现“点一下才恢复完整累计值”时，优先检查首屏同步路径。

---

## 2. 桌面预览与真实数据策略

很多开发者容易陷入一个误区：“只要处于桌面端编辑器的预览模式，就一律显示假数据”。

请牢记：**桌面端已放置在画布上的组件同样可以且应当拉取真实数据。**
- `getContext().mode === 'preview'` 仅说明当前组件处于桌面编辑环境，不表示没有真实数据，也不保证状态源已有当前快照。只要实例已经放置在画布上，请照常调用 `requestData()` 并优先渲染真实结果。
- 只有在组件选择目录卡片中（尚未拖入画布、确实没有实例 ID 和配置），或者遇到硬件能力无法在电脑端模拟时，才展示带有“示例”字样的模拟内容。

> 🎯 **详细设计指南**：请完整阅读[桌面端与移动端数据同步的最佳实践](./widgets.md#最佳实践一-桌面端与移动端数据同步)。你也可以在[网络组件示例](./examples.md)中观察完整的拉取与容灾处理范式。

---

## 3. 插件间跨界协作：提供与调用服务

飞块拥有开放繁荣的插件生态。你的插件不仅可以独立工作，还能像微服务一样对外提供接口，或是调用其它成熟插件的现成能力。

### 第一步：服务提供方声明与实现
提供方在 `manifest.json` 的 `contributes.services` 中声明服务 ID、接口版本及开放的方法，并在 `definePlugin` 中注入实现。

例如，一个工具插件开放了一个加法计算服务 `sum`：



```js
const { t: tr } = require(process.env.FLYONDECK_SDK_PATH || "@flyondeck/plugin-sdk");
"use strict";
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);

definePlugin({
  services: {
    numbers: {
      sum({ values }) {
        if (
          !Array.isArray(values) ||
          values.length > 100 ||
          values.some((v) => typeof v !== "number" || !Number.isFinite(v))
        )
          throw new Error(tr("messages.index.values-must-be-a-list-of-up-to-100-finite-numbers"));
        const total = values.reduce((sum, v) => sum + v, 0);
        if (!Number.isFinite(total))
          throw new Error(tr("messages.index.sum-exceeds-the-finite-number-range"));
        return { total };
      },
    },
  },
});
```



### 第二步：服务消费方声明与调用
想要使用该服务的插件，首先在 `package.json` 或 `manifest.json` 的 `dependencies` 中指定提供方插件的包版本范围；然后在 `manifest.json` 的 `uses` 列表中精准声明所需调用的服务标识与接口大版本：



```js
const { t: tr } = require(process.env.FLYONDECK_SDK_PATH || "@flyondeck/plugin-sdk");
"use strict";
const { definePlugin } = require(process.env.FLYONDECK_SDK_PATH);

definePlugin({
  actions: {
    async sum({ values }, context) {
      try {
        return await context.plugins.call(
          "example.service-provider",
          "numbers",
          "sum",
          { values },
        );
      } catch (error) {
        throw new Error(
          tr("messages.index.number-service-call-failed-check-that-the-provider-is-installed-and-runn", [error.message]),
        );
      }
    },
  },
});
```



在业务逻辑中，通过 `context.plugins.call(providerId, serviceId, method, args)` 即可实现高内聚、低耦合的进程间函数调用！

> 💡 **健康检查与优雅提示**：跨插件调用要求提供方插件已处于安装且激活运行状态。如果提供方尚未启动，调用会抛出明确的未就绪错误；消费方可据此向用户提示“请先在飞块中启用 XX 辅助插件”。详细字段见 [Service 参考](../reference/services.md) 与 [context.plugins 参考](../reference/plugins.md)。

---

## 4. 触发一次性系统事件（context.events）

如果插件后台在完成某个长耗时任务后（如批量压缩图片完成、下载大文件成功），需要向飞块宿主汇报一条瞬时通知，可以通过事件通道发送：

```javascript
await context.events.emit('downloadComplete', { taskId: '123', status: 'success' });
```

事件会被飞块宿主捕获并分发，它与“状态”不同，**不会在宿主内存中长期保留快照**。如果你的目的只是把执行结果交由自动化工作流的下一步骤读取，直接在动作（Action）的处理函数中通过 `return` 返回结果才是最地道、最便捷的做法。详细 API 见 [context.events.emit() 参考](../reference/events.md)。
