系统插件的加载和取色插件的开发
## 前言
在 Rubick 插件系统中,插件主要分为两大类,一类是 UI 类插件,这类插件是通过 BrwoserView 进行加载,因为通过 BrowserView 加载,所以通常会有视图 UI 界面,插件销毁后,不会驻留在内存中。还有一类是系统类插件,这类插件是会常驻在 Rubick 运行时中的,可以是一段 JavaScript 代码片段,会伴随着 Rubick 特定运行状态钩子而启动运行。
在章节《实战篇:插件的安装、发布、卸载》中,我们介绍了插件安装的方法;在《实战篇:如何支持工具插件化》中,我们介绍了如何加载安装好的 UI 类插件。
接下来,我们将接着介绍如何加载安装好的系统类插件,然后再教大家编写一个简单的 UI 类插件和系统类插件。
系统类插件的目录结构说明
前面我们介绍了 UI 类插件的目录结构,接下来,我们再看一下最简单的系统类插件目录结构:
plugin
|-- index.js
└── package.json1. package.json
和之前 UI 类插件字段是一样的,区别是需要额外提供一个 entry 字段来标记入口 js,以及 pluginType 字段为 system 来标记是一个系统类插件:
{
"pluginType": "system",
"entry": "index.js"
}2. index.js
系统插件的入口 js 文件,通过自定义一些系统插件钩子函数,来实现代码片段注入的能力:
// index.js
module.exports = () => {
return {
// 定义 beforeReady 钩子函数
beforeReady() {
// ...
}
// 定义 onReady 钩子函数
onReady(ctx) {
// ...
},
// 定义 onRunning 钩子函数
onRunning(ctx) {
// ...
},
// 定义 onQuit 钩子函数
onQuit() {
// ...
}
}
}加载系统类插件
我们知道,Electron 运行时主要是通过 app 对象来完成对应用生命周期的监听和处理,我们想要实现系统插件,就是希望可以注入到这些生命周期中运行的代码段。因此,我们可以把 app 的启动类比成 Vue 的生命周期那样设置几个关键阶段:
- beforeReady:Electron App 启动前的准备工作,执行在 Electron 钩子函数
app.on('ready')之前。这里会加载执行系统插件的onBeforeReady钩子函数。 - onReady:Electron
app.on('ready')函数执行期,会做一些更新检测、创建系统菜单、创建主窗口等操作。这里会加载执行系统插件的onReady钩子函数。 - onRunning:这里会处理 Electron 的
app.on('second-instance')钩子函数和app.on('activate')钩子函数。这里会加载执行系统插件的onRuning钩子函数。 - onQuit:这里会处理 Electron 的
app.on('window-all-closed')钩子函数和app.on('will-quit')钩子函数。这里会加载执行系统插件的onQuit钩子函数。
所以,我们需要对入口 main/index.js 文件做一下改造:
import electron, { app, protocol } from 'electron';
class App {
constructor() {
// 注册协议
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } },
]);
// 处理多应用实例
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
// 注册系统插件
this.systemPlugins = registerSystemPlugin();
// 注册生命周期
this.beforeReady();
this.onReady();
this.onRunning();
this.onQuit();
}
}
beforeReady() {
// ...
// 触发 onBeforeReady
this.systemPlugins.triggerBeforeReadyHooks()
}
createWindow() {
this.windowCreator.init();
}
onReady() {
const readyFunction = async () => {
// ...
// 触发 onReady
this.systemPlugins.triggerReadyHooks();
};
if (!app.isReady()) {
app.on('ready', readyFunction);
} else {
readyFunction();
}
}
onRunning() {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// ...
if (win) {
if (win.isMinimized()) {
win.restore();
}
win.focus();
}
});
app.on('activate', () => {
// ...
});
// 触发 onRunning
this.systemPlugins.triggerOnRunningHooks();
}
onQuit() {
// ...
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
// ...
// 触发 OnQuit
this.systemPlugins.triggerOnQuitHooks();
});
}
}
export default new App();上面代码也很简单,我们可以详细介绍一下:
- 首先通过一个
App类来定义了四个钩子函数,并通过constructor来实现对钩子函数的注册调用。 - 在
constructor中,通过registerSystemPlugin函数完成对系统插件的注册,并赋值给了this.systemPlugins。 - 监听
app阶段性钩子函数,并通过this.systemPlugins.triggerXXXHooks完成对系统插件钩子函数的触发调用。
接下来,我们以实现 triggerXXXHooks 来作为示例,来说明一个插件入口 js 程序中的 onReady 钩子是如何被执行的:
// registerSystemPlugin.js
import path from 'path';
import fs from 'fs';
// 插件通过 npm 安装下载的路径
import { PLUGIN_INSTALL_DIR } from '@/common/constans/main';
const registerSystemPlugin = () => {
// ...
// 通过 pluginType 从所有插件 totalPlugins 中过滤出系统插件
let systemPlugins = totalPlugins.filter(
(plugin) => plugin.pluginType === 'system'
);
// 处理插件的 entry 路径,由相对路径转为绝对路径
systemPlugins = systemPlugins
.map((plugin) => {
try {
const pluginPath = path.resolve(
PLUGIN_INSTALL_DIR,
'node_modules',
plugin.name
);
return {
...plugin,
indexPath: path.join(pluginPath, './', plugin.entry),
};
} catch (e) {
return false;
}
})
.filter(Boolean);
// 定义插件的所有钩子函数
const hooks = {
onReady: [],
};
// 收集所有系统插件的 onReady 钩子函数
systemPlugins.forEach((plugin) => {
if (fs.existsSync(plugin.indexPath)) {
const pluginModule = __non_webpack_require__(plugin.indexPath)();
hooks.onReady.push(pluginModule.onReady);
}
});
// 定义触发所有插件的 onReady 钩子
const triggerReadyHooks = (ctx) => {
hooks.onReady.forEach((hook: any) => {
try {
hook && hook(ctx);
} catch (e) {
console.log(e);
}
});
};
return {
triggerReadyHooks,
};
};上述代码中,首先通过 pluginType = system 这个条件来过滤出所有的系统插件,因为插件中的 package.json 定义的 entry 入口文件是一个相对路径,所以我们将其处理成了一个绝对路径 indexPath。接下来就是通过 __non_webpack_require__ 函数动态引用入口 index.js 文件,并把其中注册的 onReady 钩子函数派发到 hooks 中。然后包装成一个 triggerReadyHooks 函数在特定时机触发。
__non_webpack_require__是 Node.js 中的一个全局变量,它是为了避免 Webpack 对require函数的处理而引入的。在 Webpack 打包的过程中,它会把所有的
require函数替换成一些用于模块加载的特殊函数,这可能导致某些情况下无法直接使用原生的 Node.js 模块加载方式。为了绕过这种替换,可以使用__non_webpack_require__来调用原生的require函数。当你使用 Webpack 打包的代码中需要引入 Node.js 模块时,可以使用
__non_webpack_require__,这样就可以确保使用原生的 Node.js 的模块加载机制而不受 Webpack 的影响。
插件的开发
前面我们已经分别介绍了关于 UI 类插件和系统类插件的最基础的目录结构。但是开发过程中,使用最基础的目录结构肯定是不行的,为了界面样式的统一和美观我们可能会使用一些组件库,为了提升开发效率,我们也可能会使用一些开发框架,比如 Vue、React。
因此,这里我们将以 Vue 3 + antdv 作为基础依赖,实现一个桌面取色器插件。
1. 初始化工程
我们使用 vueCli 来创建一个插件应用:
$ vue create rubick-plugin-colorpicker得到的目录结构如下:
.
├── README.md
├── babel.config.js
├── jsconfig.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ └── main.js
├── vue.config.js
└── yarn.lockvueCli 在执行 npm run build 命令构建后,会把 public 目录下的文件拷贝到 dist 目录,所以我们可以在 public 目录下新建一个 preload.js 和一个 package.json,调整后的 public 目录结构如下:
public
├── favicon.ico
├── index.html
├── package.json
└── preload.js然后我们给 package.json 文件加入以下内容:
{
"name": "rubick-plugin-colorpicker",
"pluginName": "取色器",
"version": "1.0.0",
"description": "取色器",
"main": "index.html",
"preload": "preload.js",
"logo": "https://pic1.zhimg.com/80/v2-5f1810a71af6eefcd77edbbf07ea1cc7_720w.png",
"pluginType": "ui",
"features": [
{
"code": "colorpicker",
"explain": "取色器",
"cmds": [
"colorpicker",
"qs",
"取色"
]
}
]
}因为插件是以文件方式引用资源,所以需要调整一下 vue.confg.js 设置 publicPath 的路径:
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
// ...
publicPath: process.env.NODE_ENV === "production" ? "" : "/",
})我们开始构建这个项目:
$ npm run build构建完成后,看一下 dist 目录结构:
dist
├── css
│ └── app.2cf79ad6.css
├── favicon.ico
├── index.html
├── js
│ ├── app.42798930.js
│ ├── app.42798930.js.map
│ ├── chunk-vendors.4114fffc.js
│ └── chunk-vendors.4114fffc.js.map
├── package.json
└── preload.js这个就是我们后面需要发布的 npm 插件包。
2. 调试插件
因为我们的插件是基于 npm 的,所以可以通过 npm link 的方式进行插件调试,在 dist 目录下,执行:
$ npm link然后在 rubick 中通过路径的方式安装这个插件:
安装的本质就是 npm link rubick-plugin-colorpicker。安装完成后,我们就可以通过搜索框输入关键词:colorpicker、qs、取色 来进行唤起插件。
但这样有一个问题就是每次无法热更新,每次修改代码都需要进行 npm run build 后才能显示结果,因此,需要在插件的 package.json 中添加一个 development 字段来标记开发环境的前端入口地址:
// public/package.json
{
"development": "http://localhost:8080",
}这样,每次更新代码,也会热更新插件页面。
3. 功能完善
插件页面已经搭建好了,接下来要实现的就是屏幕取色的能力,这里我们可以直接使用 electron-color-picker 这个开源库,接下来,需要在 public 目录下安装这个依赖库:
$ npm install electron-color-picker然后在 preload.js 中使用这个库:
const {
getColorHexRGB,
darwinGetScreenPermissionGranted,
darwinRequestScreenPermissionPopup
} = require('electron-color-picker');
const os = require("os");
const isDarwin = os.platform === 'darwin';
window.colorpicker = async () => {
try {
window.rubick.hideMainWindow();
// 如果是 macOS 需要检测屏幕录制权限
if (isDarwin) {
const permission = await darwinGetScreenPermissionGranted();
if (!permission) {
return darwinRequestScreenPermissionPopup();
}
}
// 调用 color picker 来取色
const result = await getColorHexRGB();
// 取色成功写入剪贴板,然后并展示系统通知
if (result) {
window.rubick.copyText(result);
window.rubick.showNotification(`${result}, 取色成功!已复制剪切板`);
}
} catch (e) {
console.log(e);
}
}然后我们在前端页面中加一个取色按钮,点击后,触发取色功能:
<!-- App.vue -->
<template>
<button @click="window.colorpicker">取色</button>
</template>点击按钮后,就会出现取色的功能:
接下来,如果你想发布你的插件,那么只需要在构建完成后,在 dist 目录下执行:
$ npm publish这样就可以完成对插件的发布能力。
总结
本小节,我们详细介绍了关于系统插件的加载实现过程,以及基于 vueCli 完成了一个屏幕取色插件的开发。细心的同学可能留意到这是个 UI 类型的插件。后面,我们将通过 《实现超级面板插件》 的实战章节,讲解如何开发一个好用的系统插件。