📘 插件开发与使用教程 ← 返回编辑器

一、插件结构

一个插件是一个 JS 文件,导出包含以下字段的对象:

字段必填说明
id是唯一标识,字母数字与短横线,如 my-addon
name是插件显示名称
description是一句话功能说明
category否分类标签,如「自定义」「编辑器」
css否CSS 字符串,自动注入页面 <head>,禁用时自动移除
setup是初始化函数 function(ctx),返回清理函数
options否子选项数组(自定义插件暂不支持)

二、setup 与 ctx

setup 接收 ctx 对象,提供:

  • ctx.Blockly —— scratch-blocks 的 Blockly 实例(原型、工具函数都在这里)
  • ctx.getWorkspace() —— 返回当前积木工作区实例
  • ctx.addon —— 当前插件自身的定义对象
  • ctx.document / ctx.window —— 直接操作页面 DOM
  • ctx.createElement(tag, props, children) —— 创建元素助手(自动挂事件/onX、style 对象、dataset)
  • ctx.injectCSS(css) —— 注入一段样式(禁用时自动移除)
  • ctx.addToolbarButton(label, onClick, opts) —— 在顶部工具栏右侧加一个按钮
  • ctx.mountPanel(opts) —— 在主区挂一个浮动面板,返回 {panel, body, remove}
  • ctx.effect(disposer) / ctx.addEventListener(target, ev, cb) —— 注册可逆副作用,禁用时统一回收
  • ctx.assets / ctx.loadAsset(name) / ctx.loadAssetText(name) / ctx.assetNames —— 读取插件包内资源(HTML·CSS·图片·JS 等,base64)
  • ctx.fileOverride(opt) —— 修改网页底层文件:覆盖 DOM 结构、注入 CSS/JS、替换图片、全局替换资源 URL,禁用时自动还原

setup 必须返回一个 清理函数(可 async 返回);也可以不返回、改用 ctx.effect(fn) 注册回收逻辑。重载页面或禁用插件时,所有注册的可逆操作都会被执行,用于还原原型、移除 DOM、解绑事件。

二·五、导入格式:单个 JS / 文件夹 / ZIP

在「设置 → 插件管理 → 安装插件」弹窗中,除粘贴来源外,还可本地导入:

  • 单个 JS:直接选中 .js 插件文件。
  • 文件夹:点「文件夹」后选整个目录,目录内需含 index.js 入口;其余 HTML·CSS·图片·JS 作为资源随插件持久化。
  • ZIP 包:点「ZIP 包」选 .zip,包内需含 index.js 入口,其余文件作为资源。

资源以 base64 形式存入 localStorage,页面重载后自动恢复,无需重新导入。

三·五、读取资源与改写网页底层文件

插件包内的资源可通过 ctx.loadAsset() 读取;ctx.fileOverride() 则提供「底层文件级」改造能力,所有改动在插件禁用时自动还原:

export default {
  id: 'asset-and-override',
  name: '资源与底层文件改写示例',
  description: '读取 ZIP/文件夹 资源,并改写编辑器底层文件',
  setup: function (ctx) {
    // 1) 读取插件包内的图片资源并显示在按钮上
    const icon = ctx.loadAsset('icon.png');     // base64 dataURL
    const css  = ctx.loadAssetText('theme.css'); // 资源文本
    if (css) ctx.injectCSS(css);

    // 2) 改写网页底层文件(禁用时自动还原)
    ctx.fileOverride({
      // 替换某个图片元素的 src
      images: { '.ext-logo img': icon },
      // 覆盖某容器 HTML 结构
      html: { selector: '.ext-builder-header', prepend: '<div class="badge">插件已加载</div>' },
      // 注入整段 CSS 到 body
      css: '.ext-builder { --brand: #2b7de9; }',
      // 注入并执行脚本
      js: 'console.log("插件底层脚本已运行");',
      // 全局替换资源 URL
      replaceUrl: { '/old-logo.png': '/new-logo.png' }
    });

    // 3) 返回清理函数(fileOverride 的还原会一并执行)
    return function cleanup() {
      console.log('插件已卸载');
    };
  }
};

三、改造网页界面(改页能力)

插件可以像 DeepSeek Harness 一样自由改造本编辑器界面——加按钮、挂面板、注入样式、操作任意 DOM:

export default {
  id: 'page-mods',
  name: '界面改造示例',
  description: '加工具栏按钮 + 浮动面板 + 主题样式',
  setup: function (ctx) {
    // 1) 顶部工具栏加一个按钮
    const btn = ctx.addToolbarButton('点我', () => {
      alert('你好,来自插件 ' + ctx.addon.id);
    }, {title: '示例按钮'});

    // 2) 挂一个浮动面板
    const panel = ctx.mountPanel({title: '我的面板', body: null});
    panel.body.appendChild(ctx.createElement('p', {style: {color: '#2b7de9'}}, ['这是插件注入的面板内容']));

    // 3) 注入主题样式
    ctx.injectCSS('.ext-builder { outline: 2px dashed #2b7de9; }');

    // 4) 直接操作 DOM
    const title = ctx.document.querySelector('.ext-menu-title');
    if (title) title.textContent = '扩展编辑器(已被插件改名)';

    // 5) 返回清理函数(也可改用 ctx.effect)
    return function cleanup () {
      btn.remove();
      panel.remove();
    };
  }
};

四、完整示例

三、完整示例

export default {
  id: 'my-first-addon',
  name: '我的第一个插件',
  description: '给带注释的积木加高亮描边',
  category: '自定义',
  // 可选:CSS 自动注入页面 <head>,禁用时自动移除
  css: '.blocklyMainBackground { opacity: 0.98; }',
  // 必填:初始化函数,可写 async;返回清理函数
  setup: function (ctx) {
    const B = ctx.Blockly;
    const ws = ctx.getWorkspace();

    // 1) 覆写原型方法(增强 Blockly)
    const origRender = B.BlockSvg.prototype.renderDraw_;
    B.BlockSvg.prototype.renderDraw_ = function (...args) {
      const r = origRender.call(this, ...args);
      if (this.comment) this.svgPath_.setAttribute('stroke', '#ff8c1a');
      return r;
    };

    // 2) 监听工作区事件
    const onChange = (e) => {
      if (e.isUiEvent) return;
      console.log('积木变化:', e.type);
    };
    ws.addChangeListener(onChange);

    // 3) 返回清理函数:还原一切
    return function cleanup () {
      B.BlockSvg.prototype.renderDraw_ = origRender;
      ws.removeChangeListener(onChange);
    };
  }
};
已复制到剪贴板 ✓