[js] 不用插件:绑定思源事件总线(eventBus)

不多说了,用的人都懂。

简洁版 (不推荐,有风险)

优点:代码简洁。

缺点:要求用户至少安装一个插件。

⚠️ 风险警告:当用户关闭第一个插件时,之前的绑定会失效。

function eventBusOn(eventName, callback) { const plugin = window.siyuan.ws.app.plugins[0]; if(!plugin) { console.log('绑定事件'+eventName+'失败,请至少安装一个插件'); return false; } plugin.eventBus.on(eventName, callback); return true; } function eventBusOff(eventName, callback) { const plugin = window.siyuan.ws.app.plugins[0]; if(!plugin) { console.log('解绑事件'+eventName+'失败,请至少安装一个插件'); return false; } plugin.eventBus.off(eventName, callback); return true; }

自定义 Plugin 版

优点:兼容性好。

缺点:代码量大。

// 参考 https://github.com/siyuan-note/siyuan/blob/f660b2ddfa98f93f778bbbbe5abc3bfaa5a932b6/app/src/plugin/index.ts function eventBusOn(eventName, callback) { const plugin = getMyPlugin(); plugin.eventBus.on(eventName, callback); } function eventBusOff(eventName, callback) { const plugin = getMyPlugin(); plugin.eventBus.off(eventName, callback); } function getMyPlugin(pluginName = "my-custom-plugin") { let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName); if(myPlugin) return myPlugin; class EventBus { constructor(name = "") { this.eventTarget = document.createComment(name); document.appendChild(this.eventTarget); } on(type, listener) { this.eventTarget.addEventListener(type, listener); } once(type, listener) { this.eventTarget.addEventListener(type, listener, { once: true }); } off(type, listener) { this.eventTarget.removeEventListener(type, listener); } emit(type, detail) { return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail, cancelable: true })); } } class Plugin { constructor(options) { this.app = options.app||window.siyuan.ws.app.appId; this.i18n = options.i18n; this.displayName = options.displayName||options.name; this.name = options.name; this.eventBus = new EventBus(options.name); this.protyleSlash = []; this.customBlockRenders = {}; this.topBarIcons = []; this.statusBarIcons = []; this.commands = []; this.models = {}; this.docks = {}; this.data = {}; this.protyleOptionsValue = null; } onload() {} onunload() {} uninstall() {} async updateCards(options) { return options; } // 返回选项本身 onLayoutReady() {} addCommand(command) {} addIcons(svg) {} addTopBar(options) { return null; } // 模拟返回null addStatusBar(options) { return null; } // 模拟返回null // 去掉设置,参考 https://github.com/siyuan-note/siyuan/blob/dae6158860cc704e353454565c96e874278c6f47/app/src/plugin/openTopBarMenu.ts#L25 //openSetting() {} loadData(storageName) { return Promise.resolve(null); } saveData(storageName, data) { return Promise.resolve(); } removeData(storageName) { return Promise.resolve(); } getOpenedTab() { return {}; } // 返回空对象 addTab(options) { return () => {}; } // 返回空函数模拟模型 addDock(options) { return {}; } // 返回空对象模拟 dock addFloatLayer(options) {} updateProtyleToolbar(toolbar) { return toolbar; } // 返回toolbar本身 set protyleOptions(options) {} get protyleOptions() { return this.protyleOptionsValue; } } myPlugin = new Plugin({name:pluginName}); window.siyuan.ws.app.plugins.push(myPlugin); return myPlugin; }

已封装到 openAny 中。

[js] 连续点击 openAny,小代码,大作用,让一切触手可达

在 openAny 中调用方法示例:

const handler = (args)=>console.log(args); openAny.on('open-menu-link', handler); openAny.once('open-menu-link', handler); openAny.off('open-menu-link', handler); openAny.emit('your event name', data);

折中版(推荐)

优点:代码简洁和稳定的结合。同时,该功能更强大,还可以通过这种继承实现无插件的方式对插件函数调用等。

缺点:至少得安装一个插件才行。

function eventBusOn(eventName, callback) { const pluginName = 'my-custom-plugin'; if(window.siyuan.ws.app.plugins?.length === 0) { console.log('绑定事件'+eventName+'失败,请至少安装一个插件'); return false; } let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName); if(!myPlugin) { const Plguin = Object.getPrototypeOf(window.siyuan.ws.app.plugins[0].constructor); const MyPlugin = class extends Plguin{}; myPlugin = new MyPlugin({app:window.siyuan.ws.app.appId, name:pluginName, displayName:pluginName}); myPlugin.openSetting = null; // 防止顶部插件按钮添加设置菜单 window.siyuan.ws.app.plugins.push(myPlugin); } myPlugin.eventBus.on(eventName, callback); return true; } function eventBusOff(eventName, callback) { const pluginName = 'my-custom-plugin'; if(window.siyuan.ws.app.plugins?.length === 0) { console.log('绑定事件'+eventName+'失败,请至少安装一个插件'); return false; } let myPlugin = window.siyuan.ws.app.plugins.find(item=>item.name === pluginName); if(!myPlugin) { console.log('解绑事件'+eventName+'失败,未找到名为'+pluginName+'的插件'); return false; } myPlugin.eventBus.off(eventName, callback); return true; }
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    26459 引用 • 110058 回帖 • 1 关注
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    207 引用 • 1448 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • EmptyLight

    第一时间想的是能在主题里做什么,想了一段时间之后还是脑袋空空,不过可扩展性很大

    1 回复
  • wilsons

    是的,如果愿意,还可以把插件的其他功能加进来。

    如果本来就是插件的话题,这个用不到其实。

    1 回复
  • EmptyLight

    如果在开发环境里面用的是 ts,应该可以把插件的 d.ts 定义文件引入进来,在编辑插件 API 部分获得类似插件的开发效果

    1 回复
  • wilsons

    是的,只要不报错,仅提供需要的实现即可,达到轻量级目的。

    我刚开始本来打算用代理,仅有 evenBus 属性实现,但发现思源默认插件方法属性返回值等都是存在的,如果兼容做好,代码量和实现空类差不多,甚至更多,关键可读性还不好。

  • emmmm,集市里有一款插件叫 OpenAPIdoge

    1 回复
  • wilsons

    是的,runjs 也支持。

    但这些需要依赖用户安装了插件,自用还不错。

    这个好处是可以不依赖其他插件,后面会集成到 openAny 中。😀

    尚未发现副作用。

  • w 佬,请问如何通过 js 实现通过快捷键对当前 tab 或者弹出的思源窗口设置成右侧(下侧)分屏?

    1 回复
  • wilsons

    openAny 可以实现。

    另外你这里的“或”是指实现任意一个就行吗?还是“和”?

    弹出的思源窗口是指悬浮窗?比如鼠标悬浮到引用上时弹出的悬浮窗?

    1 回复
  • 主要是悬浮的, 比如一个 引用笔记, 或者提及, 都是悬浮打开的, 这时如果能快捷打开到 分屏 就很方便了.

    1 回复
  • wilsons

    悬浮窗分屏显示

    需要先安装 openAny [js] 连续点击 openAny,小代码,大作用,让一切触手可达

    // 一键让悬浮窗分屏显示 setTimeout(()=>{ // 设置分屏方向 right 向右 down 向下 const splitTo = 'right'; // 设置快捷键 const pressKey = 'alt+z'; openAny.setKeymap(pressKey, async (event, {sleep, whenElementExist}) => { event.preventDefault(); event.stopPropagation(); // 函数嵌套中最好使用新实例,避免内外相互等待而死锁 const openAny = new OpenAny(); // 把悬浮窗变为标签 await openAny.click('.block__popover [data-type="stickTab"]'); // 等待把悬浮窗变为标签 await sleep(100); // 获取悬浮窗标签 const tab = await whenElementExist(()=>document.querySelector('[data-type="wnd"].layout__wnd--active .layout-tab-bar li.item--focus')||document.querySelector('[data-type="wnd"] .layout-tab-bar li.item--focus')); // 打开悬浮窗标签右键菜单 await openAny.press('mouseright', tab); // 选择菜单中的分屏方式 await openAny.click(`#commonMenu [data-id="splitMove${splitTo==='down'?'B':'R'}"]`); }); }, 2000);
    1 回复
  • 大神啊, 太感谢了.正是我要的.

    我在本论坛多次要求作者实现一个类似的快速分屏的办法, 每次都被各种拒绝.

  • 我目前发现两个问题,请确认一下:

    1. updateProtyleToolbar(toolbar) 需要原样返回:updateProtyleToolbar(toolbar) { return toolbar; },返回空数组的话会清空工具栏,导致工具栏无法显示。相关代码:https://github.com/siyuan-note/siyuan/blob/f660b2ddfa98f93f778bbbbe5abc3bfaa5a932b6/app/src/protyle/toolbar/index.ts#L70

    2. openSetting() {} 执行之后会在右上角的插件菜单添加一个空选项

      image.png

    1 回复
    1 操作
    JeffreyChen 在 2025-06-04 19:05:32 更新了该回帖
  • wilsons 1 1 评论

    感谢反馈!这两个问题已解决!

    我对比了一遍原 plugin 对象的属性和方法,应该没有问题了。

    主要修改点

    image.png

    你的代码注释没改
    JeffreyChen 1 赞同
  • wilsons

    @JeffreyChen 折中版问世了

    折中版

请输入回帖内容 ...
wilsons
正在努力开发 wilsons 工具箱中 🛠️ 目前已正式入驻爱发电啦!💖 想催更、提需求?欢迎访问 👉 https://afdian.com/a/wilsons