插件项目文件结构详解

新建插件时 Eagle 只生成四个文件,但真正能上架的项目通常有十几个。这一节把每个文件的职责讲透,顺便说清楚哪些文件绝对不能进最终安装包。

官方骨架长什么样

Plugin
├─ manifest.json
├─ logo.png
├─ index.html
└─ js
   └─ plugin.js

这是最小结构。manifest.json 是唯一必需的文件,没有它 Eagle 连插件都识别不了。

逐个文件职责

manifest.json —— 插件身份证

唯一必需的文件。它同时承担两件事:告诉 Eagle「这是什么插件」,以及「该怎么加载它」。不同插件类型在这里的写法完全不同:窗口插件写 main,背景服务插件在 main 里加 serviceMode: true,格式扩展和检查器插件则完全不写 main,改写 preview。

{
    "id": "LBCZE8V6LPCKD",
    "version": "1.0.0",
    "platform": "all",
    "arch": "all",
    "name": "窗口插件",
    "logo": "/logo.png",
    "keywords": [],
    "devTools": false,
    "main": {
        "url": "index.html",
        "width": 640,
        "height": 480
    }
}

platform 和 arch 决定这个插件在哪些环境可见:platform 取 all/mac/win,arch 取 all/arm/x64。如果你用了 FFmpeg 这类带原生二进制的依赖,通常需要按平台分别打包并分别提交。

logo.png —— 插件图标

对应 manifest.json 里的 logo 字段,用于插件列表和插件中心。官方建议 128×128 像素、PNG 格式,理由是 PNG 对透明度支持最好。审核阶段还会检查图标是否用了正方形画布、主要图形与边缘是否有足够留白(经过插件中心圆角裁切后不能被切掉)。

index.html —— 窗口插件的入口

对应 main.url。插件执行时这个文件会被独立载入到独立窗口里。注意它是每次打开都重新载入,不保留任何上一次的状态。

js/plugin.js —— 业务逻辑

官方骨架把它放在 js/ 目录下,纯属约定,不是强制。这个文件里才是你真正写代码的地方。它通过 index.html 的 script 标签引入,引入位置放在 head 里,这样脚本执行时机早于 DOM 就绪,需要操作 DOM 时记得包一层 onPluginCreate。

真实项目会长什么样

一个稍微像样的插件,结构大致是这样:

my-plugin
├─ manifest.json
├─ logo.png
├─ index.html
├─ js
│  ├─ plugin.js          # 入口,只负责组装
│  ├─ renderer.js        # 界面渲染
│  └─ exporter.js        # 导出逻辑
├─ lib
│  ├─ image-size.js      # 自研或第三方工具
│  └─ format.js
├─ i18n
│  └─ index.js           # 语言映射表
├─ _locales              # 多语言 JSON(仅 i18n 方案需要)
│  ├─ en.json
│  ├─ zh_CN.json
│  └─ zh_TW.json
└─ assets
   └─ icons/

要不要 package.json 和 node_modules,取决于你是否用了第三方模块。用了就得把它们一起打包;只用了 Eagle API 和 Node 原生模块,就不要引入——多余的依赖目录既增大体积,也会在安装包内容审核里被逐项追问。

格式扩展插件的特殊结构

如果你的插件是给 Eagle 加新格式支持,入口文件不是 index.html,而是两个:

my-preview-plugin
├─ manifest.json
├─ logo.png
├─ thumbnail
│  └─ icns.js            # 缩略图生成脚本
├─ viewer
│  └─ icns.html          # 双击预览页面
└─ js
   └─ icns-util.js       # 解析库,被上面两个文件 require

manifest.json 里不写 main,改写 preview:

{
    "id": "LARSKLB8OTOC2",
    "version": "1.0.0",
    "platform": "all",
    "arch": "all",
    "name": "Preview Plugin",
    "logo": "/logo.png",
    "keywords": ["icns"],
    "devTools": false,
    "preview": {
        "icns,ico": {
            "thumbnail": {
                "path": "thumbnail/icns.js",
                "size": 400,
                "allowZoom": false
            },
            "viewer": {
                "path": "viewer/icns.html"
            }
        }
    }
}

键名 "icns,ico" 是要接管的文件扩展名,多个扩展名用逗号隔开即可。thumbnail.size 是生成缩略图的目标尺寸,allowZoom 控制是否允许放大。

为什么要拆目录

骨架里的四个文件只适合演示。真实插件一旦超过几百行,单文件就会变成灾难——原因是 Eagle 插件有个不太一样的约束:没有热重载。改了代码必须关掉窗口重新打开,没有任何增量编译机制。

这带来两个直接后果:

第一,模块划分要靠 CommonJS 手动组织。把功能拆到独立文件,每个文件自己 require 需要的依赖,这样定位问题时的搜索范围会小很多:

// js/plugin.js —— 只做装配
const { renderCards } = require('./renderer.js');
const { exportFiles } = require('./exporter.js');

eagle.onPluginCreate(async (plugin) => {
    const items = await eagle.item.getSelected();
    renderCards(document.querySelector('#detail'), items);
    console.log('导出模块就绪:', typeof exportFiles);
});

// js/renderer.js
function renderCards(container, items) {
    container.innerHTML = '';
    items.forEach((item) => {
        const el = document.createElement('div');
        el.className = 'card';
        el.textContent = `${item.name} (${item.width}×${item.height})`;
        container.appendChild(el);
    });
}
module.exports = { renderCards };

// js/exporter.js
const fs = require('fs');
const path = require('path');

async function exportFiles(items, targetDir) {
    for (const item of items) {
        const dest = path.join(targetDir, path.basename(item.filePath));
        fs.copyFileSync(item.filePath, dest);
    }
}
module.exports = { exportFiles };

第二,路径拼接必须用 plugin.path 作为基准。eagle.onPluginCreate 回调拿到的 plugin.path 就是插件在磁盘上的根目录,把它拼上相对路径就能拿到自己的资源文件,而不依赖用户从哪儿装了插件:

eagle.onPluginCreate(async (plugin) => {
    const fs = require('fs');
    const path = require('path');

    // 读取插件自带的默认配置
    const configPath = path.join(plugin.path, 'assets', 'defaults.json');
    const defaults = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    console.log('默认阈值:', defaults.threshold);
});

这段代码在用户换机器、换目录、换账号之后依然能跑。反之,任何形如 C:\\Users\\me\\Desktop\\xxx 的硬编码路径,换台电脑就崩。

目录整理的操作流程

  1. 先用骨架跑通最小可运行版本,确认能在 Eagle 里打开。
  2. 把逻辑按职责拆到 js/ 下的多个文件,plugin.js 只留装配代码。
  3. 把第三方依赖统一放 node_modules/,在 plugin.js 里用相对路径 require('./node_modules/xxx') 或直接 require('xxx')。
  4. 加入 _locales/ 与语言文件,实现多语言(做法见第四章)。
  5. 打包前从干净目录导出,确保里面没有 .git/、.vscode/、.DS_Store、node_modules 里的开发依赖。

常见坑

把 .git、.idea、.vscode、__pycache__ 一起打包。 安装包内容审核会明确检查这些目录。正确做法是每次打包前从一个干净的发布目录导出,而不是直接在开发目录上压缩。

logo 路径写错。 写成 "logo": "logo.png"(没有前导斜杠)和 "/logo.png" 在某些版本下行为不同,照官方示例写前导斜杠最保险。格式扩展插件的 thumbnail.path 和 viewer.path 是相对于插件根目录的相对路径,不带前导斜杠,两种写法规则不一样,别混用。

在插件根目录放 README.md 以为没关系。 放 README 本身不违规——审核还鼓励你在根目录放 README 说明审核步骤。但 README 里不能写真实账号密码、私钥或测试凭证,这些要通过投稿页面提供。

整理人:WorkBuddy