mirror of
https://github.com/rubickCenter/rubick
synced 2025-06-21 03:32:44 +08:00
Merge branch 'dev'
This commit is contained in:
commit
629e4b85c1
@ -32,7 +32,8 @@ module.exports = {
|
|||||||
title: 'TODO: 原理解析',
|
title: 'TODO: 原理解析',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: '插件化实现原理'
|
title: '插件化实现原理',
|
||||||
|
path: '/blogs/plugin/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '右击增强实现原理'
|
title: '右击增强实现原理'
|
||||||
|
167
docs/docs/blogs/plugin/README.md
Normal file
167
docs/docs/blogs/plugin/README.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
## 插件化原理
|
||||||
|
浏览器是打开不同网页进行浏览就是一个天然的插件,我们在做 `hybird` 混合开发的时候,App 内的 H5 页面也是可以类比成一个个
|
||||||
|
插件。微信小程序在微信环境内运行也可以看做一个插件。他们都有一个共性:**在宿主环境内运行插件页面,需要使用宿主能力时
|
||||||
|
调用宿主提供的API来完成自身能力的增强**。
|
||||||
|
|
||||||
|
所以 electron 也可以看做一个移动端 APP,我们通过 `webview` 来加载 `H5` 页面,`H5` 页面调用 `electron App` 内置 API
|
||||||
|
完成功能增强。所以这就是我们核心的原理思想。
|
||||||
|
|
||||||
|
## electron webview 方式
|
||||||
|
### 1. electron 中使用 webview
|
||||||
|
|
||||||
|
```html
|
||||||
|
<webview src="https://xxx.xx.com/index.html" preload="preload.js" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 实现 `bridge`
|
||||||
|
```js
|
||||||
|
// preload.js
|
||||||
|
window.rubickBridge = {
|
||||||
|
sayHello() {
|
||||||
|
console.log('hello world')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 插件借助 `bridge` 调用 `electron` 的能力
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div>这是一个插件<div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
window.rubickBridge.sayHello()
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 通信
|
||||||
|
因为 `proload.js` 是 `electron` 的 `renderer` 进程的,所以如果需要使用部分 `main` 进程的能力,则需要使用通信机制:
|
||||||
|
```js
|
||||||
|
// main process
|
||||||
|
ipcMain.on('msg-trigger', async (event, arg) => {
|
||||||
|
const window = arg.winId ? BrowserWindow.fromId(arg.winId) : mainWindow
|
||||||
|
const operators = arg.type.split('.');
|
||||||
|
let fn = Api;
|
||||||
|
operators.forEach((op) => {
|
||||||
|
fn = fn[op];
|
||||||
|
});
|
||||||
|
const data = await fn(arg, window);
|
||||||
|
event.sender.send(`msg-back-${arg.type}`, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// renderer process
|
||||||
|
ipcRenderer.send('msg-trigger', {
|
||||||
|
type: 'getPath',
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
ipcRenderer.on(`msg-back-getPath`, (e, result) => {
|
||||||
|
console.log(result)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 插件加载原理
|
||||||
|
### rubick 使用插件
|
||||||
|
首先我们需要实现一个插件,必须要有个 `plugin.json`,这玩意就是用来告诉 `rubick` 插件的信息。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pluginName": "helloWorld",
|
||||||
|
"description": "我的第一个uTools插件",
|
||||||
|
"main": "index.html",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"logo": "logo.png",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"code": "hello",
|
||||||
|
"explain": "hello world",
|
||||||
|
"cmds":["hello", "你好"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
接下来是将写好的插件用 `rubick` 跑起来,复制 `plugin.json` 到 `rubick` 搜索框即可,所以当 `rubick` 检测到输入框内执行
|
||||||
|
`ctrl/command + c` 时,读取剪切板内容,如果剪切板复制的是文件类型的 `plugin.json`,那么就将构造插件的 `pluginConfig` 配置文件,用于后续搜索
|
||||||
|
时使用。
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 监听 input change
|
||||||
|
// 读取剪切板内容
|
||||||
|
const fileUrl = clipboard.read('public.file-url').replace('file://', '');
|
||||||
|
// 复制文件
|
||||||
|
if (fileUrl && value === 'plugin.json') {
|
||||||
|
// 读取 plugin.json 配置
|
||||||
|
const config = JSON.parse(fs.readFileSync(fileUrl, 'utf-8'));
|
||||||
|
const pluginConfig = {
|
||||||
|
...config,
|
||||||
|
// index.html 文件位置,用于webview加载
|
||||||
|
sourceFile: path.join(fileUrl, `../${config.main || 'index.html'}`),
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'dev',
|
||||||
|
icon: 'image://' + path.join(fileUrl, `../${config.logo}`),
|
||||||
|
subType: (() => {
|
||||||
|
if (config.main) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return 'template';
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
实现效果如下:
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### rubick 内搜索插件原理
|
||||||
|
|
||||||
|
接下来就是进行命令搜索插件:
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
实现这个功能其实也就是对之前存储的`pluginConfig`的里面的 `features` 进行遍历,找到相应的 `cmd` 后进行下拉框展示即可。
|
||||||
|
|
||||||
|
```js
|
||||||
|
state.devPlugins.forEach((plugin) => {
|
||||||
|
// dev 插件未开启
|
||||||
|
if (plugin.type === 'dev' && !plugin.status) return;
|
||||||
|
const feature = plugin.features;
|
||||||
|
feature.forEach((fe) => {
|
||||||
|
// fe.cmds: 所有插件的命令; value: 当前输入框内搜索的名称
|
||||||
|
const cmds = searchKeyValues(fe.cmds, value);
|
||||||
|
options = [
|
||||||
|
...options,
|
||||||
|
...cmds.map((cmd) => ({
|
||||||
|
name: cmd,
|
||||||
|
value: 'plugin',
|
||||||
|
icon: plugin.sourceFile ? 'image://' + path.join(plugin.sourceFile, `../${plugin.logo}`) : plugin.logo,
|
||||||
|
desc: fe.explain,
|
||||||
|
type: plugin.type,
|
||||||
|
click: (router) => {
|
||||||
|
// 跳转到指定插件页面
|
||||||
|
actions.openPlugin({ commit }, { cmd, plugin, feature: fe, router });
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
当点击 input 内插件时,需要跳转到插件 `webview` 加载页面:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// actions.openPlugin
|
||||||
|
router.push({
|
||||||
|
path: '/plugin',
|
||||||
|
query: {
|
||||||
|
...plugin,
|
||||||
|
_modify: Date.now(),
|
||||||
|
detail: JSON.stringify(feature)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
本页写的插件demo已上传 [github](https://github.com/clouDr-f2e/rubick-plugin-demo)
|
@ -215,6 +215,10 @@ class Listener {
|
|||||||
});
|
});
|
||||||
const pos = this.getPos(robot.getMousePos());
|
const pos = this.getPos(robot.getMousePos());
|
||||||
win.setPosition(parseInt(pos.x), parseInt(pos.y));
|
win.setPosition(parseInt(pos.x), parseInt(pos.y));
|
||||||
|
win.setAlwaysOnTop(true);
|
||||||
|
win.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true});
|
||||||
|
win.focus();
|
||||||
|
win.setVisibleOnAllWorkspaces(false, {visibleOnFullScreen: true});
|
||||||
win.show();
|
win.show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,15 @@
|
|||||||
class="icon-more"
|
class="icon-more"
|
||||||
type="more"
|
type="more"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="selected && selected.icon" style="position: relative">
|
||||||
|
<a-spin v-show="pluginLoading" class="loading">
|
||||||
|
<a-icon slot="indicator" type="loading" style="font-size: 42px" spin />
|
||||||
|
</a-spin>
|
||||||
<img
|
<img
|
||||||
class="icon-tool"
|
class="icon-tool"
|
||||||
v-if="selected && selected.icon"
|
|
||||||
:src="selected.icon"
|
:src="selected.icon"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div v-else class="rubick-logo">
|
<div v-else class="rubick-logo">
|
||||||
<img src="./assets/imgs/logo.png" />
|
<img src="./assets/imgs/logo.png" />
|
||||||
</div>
|
</div>
|
||||||
@ -163,7 +167,18 @@ export default {
|
|||||||
feature.forEach((fe) => {
|
feature.forEach((fe) => {
|
||||||
const cmd = searchKeyValues(fe.cmds, args)[0];
|
const cmd = searchKeyValues(fe.cmds, args)[0];
|
||||||
const systemPlugin = fileLists.filter(
|
const systemPlugin = fileLists.filter(
|
||||||
(plugin) => plugin.name.indexOf(args) >= 0
|
(plugin) => {
|
||||||
|
let has = false;
|
||||||
|
plugin.keyWords.some(keyWord => {
|
||||||
|
if (keyWord.toLocaleUpperCase().indexOf(args.toLocaleUpperCase()) >= 0) {
|
||||||
|
has = keyWord;
|
||||||
|
plugin.name = keyWord;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return has;
|
||||||
|
}
|
||||||
)[0];
|
)[0];
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
config = {
|
config = {
|
||||||
@ -325,6 +340,7 @@ export default {
|
|||||||
"searchValue",
|
"searchValue",
|
||||||
"subPlaceHolder",
|
"subPlaceHolder",
|
||||||
"pluginInfo",
|
"pluginInfo",
|
||||||
|
"pluginLoading",
|
||||||
]),
|
]),
|
||||||
showOptions() {
|
showOptions() {
|
||||||
// 有选项值,且不在显示主页
|
// 有选项值,且不在显示主页
|
||||||
@ -449,5 +465,10 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.loading {
|
||||||
|
position:absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<webview v-if="!pluginInfo.subType" id="webview" :src="path" :preload="preload"></webview>
|
<webview
|
||||||
|
v-if="!pluginInfo.subType"
|
||||||
|
id="webview"
|
||||||
|
:src="path"
|
||||||
|
:preload="preload"
|
||||||
|
></webview>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<webview id="webview" :src="templatePath" :preload="preload"></webview>
|
<webview id="webview" :src="templatePath" :preload="preload"></webview>
|
||||||
</div>
|
</div>
|
||||||
@ -29,6 +34,14 @@ export default {
|
|||||||
this.webview.addEventListener('dom-ready', () => {
|
this.webview.addEventListener('dom-ready', () => {
|
||||||
this.webview.send('onPluginReady', this.pluginInfo);
|
this.webview.send('onPluginReady', this.pluginInfo);
|
||||||
this.webview.send('onPluginEnter', this.pluginInfo);
|
this.webview.send('onPluginEnter', this.pluginInfo);
|
||||||
|
this.commonUpdate({
|
||||||
|
pluginLoading: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.webview.addEventListener('did-finish-load', () => {
|
||||||
|
this.commonUpdate({
|
||||||
|
pluginLoading: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.setSubPlaceHolder('Hi, Rubick');
|
this.setSubPlaceHolder('Hi, Rubick');
|
||||||
this.webview.addEventListener('ipc-message', (event) => {
|
this.webview.addEventListener('ipc-message', (event) => {
|
||||||
@ -83,10 +96,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
...mapMutations('main', ['setSubPlaceHolder', 'commonUpdate']),
|
...mapMutations('main', ['setSubPlaceHolder', 'commonUpdate']),
|
||||||
},
|
},
|
||||||
beforeRouteUpdate() {
|
|
||||||
this.path = `File://${this.pluginInfo.sourceFile}`;
|
|
||||||
this.webview.send('onPluginEnter', this.pluginInfo);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
const webview = document.querySelector('webview');
|
const webview = document.querySelector('webview');
|
||||||
webview && webview.send('onPluginOut', this.pluginInfo)
|
webview && webview.send('onPluginOut', this.pluginInfo)
|
||||||
@ -97,10 +106,13 @@ export default {
|
|||||||
return (this.devPlugins.filter(plugin => plugin.name === this.pluginInfo.name)[0] || {}).features
|
return (this.devPlugins.filter(plugin => plugin.name === this.pluginInfo.name)[0] || {}).features
|
||||||
},
|
},
|
||||||
path() {
|
path() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.webview && this.webview.send('onPluginEnter', this.pluginInfo);
|
||||||
|
});
|
||||||
return `File://${this.pluginInfo.sourceFile}`
|
return `File://${this.pluginInfo.sourceFile}`
|
||||||
},
|
},
|
||||||
templatePath() {
|
templatePath() {
|
||||||
return `File://${path.join(__static, './plugins/tpl/index.html')}?code=${JSON.parse(this.pluginInfo.detail).code}&targetFile=${encodeURIComponent(this.pluginInfo.sourceFile)}&preloadPath=${this.pluginInfo.preload}`;
|
return `File://${path.join(__static, './plugins/tpl/index.html')}?code=${this.pluginInfo.detail.code}&targetFile=${encodeURIComponent(this.pluginInfo.sourceFile)}&preloadPath=${this.pluginInfo.preload}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ const state = {
|
|||||||
searchValue: '',
|
searchValue: '',
|
||||||
devPlugins: mergePlugins(sysFile.getUserPlugins() || []),
|
devPlugins: mergePlugins(sysFile.getUserPlugins() || []),
|
||||||
subPlaceHolder: '',
|
subPlaceHolder: '',
|
||||||
|
pluginLoading: true,
|
||||||
pluginInfo: (() => {
|
pluginInfo: (() => {
|
||||||
try {
|
try {
|
||||||
console.log(window.pluginInfo);
|
|
||||||
return window.pluginInfo || {};
|
return window.pluginInfo || {};
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})()
|
})()
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 15 KiB |
@ -40,8 +40,8 @@
|
|||||||
background: #314659;
|
background: #314659;
|
||||||
}
|
}
|
||||||
.top .img img {
|
.top .img img {
|
||||||
width: 22px;
|
width: 26px;
|
||||||
height: 22px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
.top .text {
|
.top .text {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
@ -181,6 +181,8 @@ new Vue({
|
|||||||
...JSON.parse(res),
|
...JSON.parse(res),
|
||||||
src: msg,
|
src: msg,
|
||||||
});
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.$set(this.selectData, 'translate', null);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
})
|
})
|
||||||
|
@ -252,6 +252,10 @@ window.rubick = {
|
|||||||
isWindows() {
|
isWindows() {
|
||||||
return os.type() === 'Windows_NT';
|
return os.type() === 'Windows_NT';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shellOpenPath(path) {
|
||||||
|
shell.openPath(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const preloadPath = getQueryVariable('preloadPath') || './preload.js';
|
const preloadPath = getQueryVariable('preloadPath') || './preload.js';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user