> ## Documentation Index
> Fetch the complete documentation index at: https://dev.marginnote.com.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 面板之外

除了使用 Web 技术栈进行标准的场景插件开发外，也可在插件根目录增加 `main.js`，以融合场景插件和经典插件的方式进行插件开发。

使用该方式进行开发的场景包括但不限于：

* 需要访问 MN 的原生对象
* 注册生命周期方法
* 调整面板属性
* 持久化存储（如插件设置）
* 绕开 Web 安全限制

`main.js` 运行在插件的 JavaScriptCore 环境里，通过 JSB 调用 MN 暴露的原生对象。

# 文件结构

最小结构如下：

<Tree>
  <Tree.Folder name="project" defaultOpen>
    <Tree.File name="mnaddon.json" />

    <Tree.File name="index.html" />

    <Tree.File name="main.js" />
  </Tree.Folder>
</Tree>

`mnaddon.json` 仍然用 `panel` 指定 HTML 面板入口：

```json mnaddon.json highlight={8} theme={null}
{
  "addonid": "mn.hello",
  "title": "示例插件",
  "version": "0.1.0",
  "author": "插件作者",
  "marginnote_version_min": "4.5.0",
  "immersiveOnly": true,
  "panel": "index.html"
}
```

`main.js` 需要放在插件根目录。MN 加载插件时会**立即**执行该文件，并调用其中的 `JSB.newAddon(mainPath)`。该函数需要返回一个继承自 `JSExtension` 的类。

# 插件入口

加入 `main.js` 后，插件入口将变为 `main.js`。下面是一种经典写法：

```javascript main.js theme={null}
JSB.newAddon = function (mainPath) { // 插件入口。mainPath 表示插件包根目录
  var AddonClass = JSB.defineClass(
    "HelloAddon : JSExtension", // 插件名 : JSExtension
    {
      sceneWillConnect: function () {}, // 在插件即将启用时触发
      notebookWillOpen: function (topicid) {}, // 在学习集打开时触发
      notebookWillClose: function (topicid) {} // 在学习集关闭时触发
      // 添加更多实例成员...
    },
    {
      addonDidConnect: function () {},
      addonWillDisconnect: function () {},
      // 添加更多类成员...
    }
  );

  return AddonClass;
};
```

<Note>
  在`JSB.defineClass`定义的实例成员中，使用`self`而非`this`来引用 JSB 定义的类（如`JSExtension`）的实例。关于 JSB 定义的类的可用成员详见[使用 MN 原生对象](#使用-mn-原生对象)
</Note>

# 手动控制面板

加入 `main.js` 后，点击插件按钮将**不再**自动打开面板，面板的创建、加载、显示和清理都要由插件自己完成。

例如下面定义了一个打开面板并设置最大高度的函数。

```javascript main.js theme={null}
const openPanel = (context) => {
  const panel = AddonWebPanel.panel();
  panel.maxContentHeight = 760;

  panel.loadPanel();
  panel.showFloatingIn(Application.sharedInstance().studyController(context.window));

  return panel;
};
```

注意，由于显示面板需要传入`studyController`实例，而`studyController`实例又需要通过插件实例获取，因此该函数需要为 `context` 参数传入在 JSB 定义的类的实例成员方法中的 `self` 。

# 双向通信

## 从面板调用 main.js

面板调用 `mn.call(name, payload)` 时会触发 `main.js`的 `panel.onMessage(name, payload)`。

`onMessage` 的返回值会进行序列化作为 `mn.call()` 的结果返回页面。

下面是具体例子：

```javascript main.js theme={null}
const bindPanelMessage = (panel, context) => {
  panel.onMessage = (name, payload) => {
    if (name === "showMessage") {
      Application.sharedInstance().showHUD(payload.text, context.window, 2);
      return { ok: true };
    }

    return {};
  };
};
```

面板写法如下：

```html index.html theme={null}
<button id="send">发送到 main.js</button>
<pre id="out"></pre>

<script>
  const out = document.getElementById("out");

  document.getElementById("send").onclick = async function () {
    const result = await mn.call("showMessage", { text: "来自 HTML 面板" });
    out.textContent = JSON.stringify(result, null, 2);
  };
</script>
```

`mn.call`的 Promise 返回值在包括但不限于下列情形时会进入失败分支：

* 未注册`onMessage`
* 插件被禁用或暂停
* 回调抛出异常
* 返回值无法序列化

## 从 main.js 调用面板

`main.js` 可以通过调用 `panel.post(event, data)` 向已经加载的面板发送事件。面板通过 `window.addEventListener("mn:<event>", handler)` 接收事件，并从 `event.detail` 读取数据。

下面是具体例子：

```javascript main.js theme={null}
const sendStatusToPanel = (panel, message) => {
  panel.post("status", {
    message: `main.js发来消息：${message}`
  });
};
```

面板写法如下：

```html index.html theme={null}
<pre id="status"></pre>

<script>
  window.addEventListener("mn:status", function (event) {
    document.getElementById("status").textContent =
      JSON.stringify(event.detail, null, 2);
  });
</script>
```

<Tip>
  `post()` 在页面加载完成前调用时，事件会先被缓存，并在页面就绪后投递。
</Tip>

# 使用 MN 原生对象

`main.js` 可访问 JSB 暴露的原生对象，例如 `Application`、`Database`、`NSFileManager`、`NSUserDefaults`、`NSTimer`、`UIColor`、`UIViewController` 等。

例如：

```javascript theme={null}
// 在 JSB 定义的类的实例成员方法里
const app = Application.sharedInstance();
const studyController = app.studyController(self.window);
const database = Database.sharedInstance();
```

完整对象、属性与方法请参考 [MarginNote 插件开发文档](https://mn-docs.museday.top)。

# 面板对象常用方法

## 创建和加载

### `AddonWebPanel.panel()`

创建一个新的面板对象。

### `panel.loadPanel()`

按 `mnaddon.json` 的 `panel` 字段读取包内 HTML 文件。文件不存在、未声明 `panel` 或路径越界时返回 `false`。这是加载面板的推荐方式。

### `panel.loadHTML(html)`

直接加载一段 HTML 字符串作为面板的源代码。

## 双向通信

### `panel.onMessage = function (name, payload) { ... }`

接收面板侧 `mn.call(name, payload)` 的入口。

### `panel.post(event, data)`

从 `main.js` 向面板页面以 `mn:<event>` 的命名格式发送事件。

## 媒体数据

### `panel.putMedia(key, data, type)`

将二进制图片数据交给面板。页面通过 `/media/<key>` 引用该数据。`data` 为空时，宿主会删除对应 key。

### `panel.clearMedia()`

清空当前面板的媒体缓存。较大的图片数据建议通过媒体缓存传递，避免放入 JSON 或 `post()` 数据。

## 显示和隐藏

### `panel.showFloatingIn(studyController)`

将面板作为常驻浮层显示到学习集控制器中。面板已经显示时，再次调用会将面板置于前台并刷新布局。

### `panel.showFloatingInMode(studyController, mode)`

按指定形态显示面板。`mode` 可使用 `"mini"` 或 `"workspace"`。

### `panel.hideFloating()`

收起浮层，并触发面板清理。插件主动关闭面板时，优先调用该方法。

## 资源清理

### `panel.teardown()`

显式断开面板、WebView、回调和未完成请求之间的引用关系，避免旧面板继续占用资源。`hideFloating()` 会一并执行清理。插件通过其他展示方式关闭面板时，应主动调用 `teardown()`。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.