Merge branch 'dev'

This commit is contained in:
muwoo 2021-08-17 17:39:08 +08:00
commit 629e4b85c1
10 changed files with 227 additions and 16 deletions

View File

@ -32,7 +32,8 @@ module.exports = {
title: 'TODO: 原理解析', title: 'TODO: 原理解析',
children: [ children: [
{ {
title: '插件化实现原理' title: '插件化实现原理',
path: '/blogs/plugin/',
}, },
{ {
title: '右击增强实现原理' title: '右击增强实现原理'

View 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';
})()
};
}
```
实现效果如下:
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b40162dd4c774a3ca6db2aa63c3606eb~tplv-k3u1fbpfcp-watermark.image)
### rubick 内搜索插件原理
接下来就是进行命令搜索插件:
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/236e9308fa324a3bac266ff7332cd1ab~tplv-k3u1fbpfcp-watermark.image)
实现这个功能其实也就是对之前存储的`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)
}
});
```
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37cf1909b1374606bdd1fbae657433c7~tplv-k3u1fbpfcp-watermark.image)
本页写的插件demo已上传 [github](https://github.com/clouDr-f2e/rubick-plugin-demo)

View File

@ -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();
}); });
} }

View File

@ -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>

View File

@ -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}`;
} }
} }
} }

View File

@ -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

View File

@ -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;

View File

@ -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;
}) })

View File

@ -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';