實(shí)現(xiàn)工具自由!開(kāi)源的桌面工具箱
在一切開(kāi)始之前,首先要致敬 uTools!如果沒(méi)有它就沒(méi)有 Rubick。

大家好,我是“拉比克”(Rubick)項(xiàng)目的作者木偶。我做的 Rubick 是一款基于 Electron 的開(kāi)源桌面工具箱,簡(jiǎn)單講就是好多工具的集合,然后加上快速啟動(dòng)、豐富的插件擴(kuò)展等功能于一體。

沒(méi)錯(cuò)!它的使用方式和外觀幾乎和 uTools 一摸一樣。那我為什么放著免費(fèi)的 uTools 不用,非要自己搞一個(gè)呢?
事情的起因是這樣的,出于安全方面的考慮有一些僅適用于公司內(nèi)部的插件不能發(fā)布到插件市場(chǎng),所以不能接入 uTools。但實(shí)在眼饞 uTools 式的便捷、用完即走的極簡(jiǎn)操作體驗(yàn)。在搜尋解決方案無(wú)果,同時(shí)也發(fā)現(xiàn)其他的小伙伴也有同樣的訴求,所以我動(dòng)手做了,然后把它開(kāi)源了。
Rubick 一款呼出超快、用完即走的開(kāi)源工具箱,因?yàn)殚_(kāi)源所以更自由!
項(xiàng)目地址:https://github.com/clouDr-f2e/rubick
希望它能幫助你解決同樣的煩惱,但目前僅支持 Windows 和 macOS,Linux 版本正在開(kāi)發(fā)中。想借助開(kāi)源的力量讓 Rubick 變強(qiáng),成為金牌輔助!幫助大家輕松“超神”!
在做 Rubick 的過(guò)程中還是遇到了不少問(wèn)題和挑戰(zhàn),下面就分享下我的心路歷程。
一、緣起
1.1 初識(shí) Electron
Electron 是 GitHub 開(kāi)源的一個(gè)框架。它通過(guò) Node.js 和 Chromium 的渲染引擎完成跨平臺(tái)的桌面 GUI 應(yīng)用程序的開(kāi)發(fā)。我起初沒(méi)有接觸過(guò) Electron,最開(kāi)始接觸它是因?yàn)榭吹搅?PicGo 的一個(gè)核心功能非常吸引我,就是 macOS 下可以直接拖拽圖片進(jìn)入任務(wù)托盤(pán)上傳圖片:

當(dāng)時(shí)正好我們團(tuán)隊(duì)也需要搞一個(gè)內(nèi)部的 CDN 圖片資源管理圖床,用于項(xiàng)目圖片資源壓縮并直接上傳到 CDN 上,之前我們做了個(gè)網(wǎng)頁(yè)版。而這里我深刻的感受到了 Electron 的強(qiáng)大,可以極大的提高工作效率,參考 PicGo 我嘗試做了第一個(gè) Electron 項(xiàng)目,完成了圖片壓縮上傳到內(nèi)部 CDN 的桌面端應(yīng)用。
1.2 演化
之后公司內(nèi)部因?yàn)殚_(kāi)發(fā)和后端進(jìn)行接口聯(lián)調(diào)測(cè)試環(huán)境時(shí),經(jīng)常會(huì)涉及到一些狀態(tài)改變要看交互樣式的問(wèn)題。比如測(cè)試需要測(cè)商品的待支付、支付中、支付完成等各種節(jié)點(diǎn)的交互樣式是否符合預(yù)期,這種情況測(cè)試一般會(huì)去造數(shù)據(jù)或者讓后端改數(shù)據(jù)庫(kù)接口。有的小伙伴可能會(huì)用 Charles 修改返回?cái)?shù)據(jù)進(jìn)行測(cè)試,但 Charles 的抓包體驗(yàn)和配置體驗(yàn)感覺(jué)有點(diǎn)麻煩,對(duì)新人不是很友好所以我們自己做了個(gè)非常易用 抓包&mock 工具:

這也是 Rubick 最早的雛形。隨后,我們發(fā)現(xiàn)當(dāng)頁(yè)面發(fā)布線上的時(shí)候,沒(méi)有辦法在微信環(huán)境內(nèi)對(duì)線上頁(yè)面進(jìn)行調(diào)試,所以開(kāi)發(fā)了一個(gè)基于 winner 的遠(yuǎn)程調(diào)試功能。
但隨著該 Rubick 在內(nèi)部不斷推廣和使用,所需功能也越來(lái)越多。我們需要 需求管理、性能評(píng)估、埋點(diǎn)檢測(cè) 等等工具。這些工具的增加一方面導(dǎo)致 Rubick 體積暴增,一方面又導(dǎo)致了用戶(hù)需要不斷更新軟件,導(dǎo)致用戶(hù)體驗(yàn)非常差。
其次,我們?cè)谕茝V給測(cè)試、UI 同學(xué)使用的時(shí)候,發(fā)現(xiàn)他們其實(shí)并不關(guān)注前面的頁(yè)面調(diào)試、性能測(cè)評(píng)等功能,可能只是用到其中某一項(xiàng),所以整個(gè)項(xiàng)目對(duì)他們來(lái)說(shuō)就顯得很臃腫。
1.3 靈感
直到有一天,我在掘金上看到這樣一個(gè)沸點(diǎn):

下面有個(gè)評(píng)論提到了 uTools 這是我第一次和 uTools 產(chǎn)生了交集,在體驗(yàn)了 uTools 功能后,我長(zhǎng)吸一口氣:這不就是我想要的嘛!然后就去 GitHub 上找 uTools 的源碼,發(fā)現(xiàn)它并沒(méi)有開(kāi)源。
所以就想把上面提到的那些工具, 發(fā)布到 uTools 市場(chǎng)在 uTools 里通過(guò)插件的方式使用他們。但我發(fā)現(xiàn)發(fā)布插件只能發(fā)布到公網(wǎng),但這又涉及到數(shù)據(jù)安全的問(wèn)題。
無(wú)奈,難道真的要自己做一個(gè)這樣的工具嗎?真的是有點(diǎn)頭大。不過(guò)想想也挺有意思的。至此,我萌生了要開(kāi)發(fā)一個(gè)媲美 uTools 的開(kāi)源工具箱的念頭。
二、研發(fā)
開(kāi)篇第一步,按照我之前的套路都是先取好名字先占個(gè)坑。我是個(gè) Dota 玩家,之前寫(xiě)了一本《從0開(kāi)始可視化搭建》的小冊(cè),里面使用了 Dota 中一個(gè)英雄的名字 coco(船長(zhǎng))。這次我取名的是 rubick 即 拉比克。Rubick(拉比克) 也是 Dota 里面的英雄之一,其核心技能是插件化使用其他英雄的技能,用完即走。非常符合本工具的設(shè)計(jì)理念,所以取名 Rubick。

我的核心目標(biāo)就是需要讓 Rubick 支持插件化,解決前面提到的問(wèn)題:
每個(gè)人的工具箱不同 軟件體積暴增 每增加一個(gè)工具就需要更新版本
其次,通過(guò)調(diào)研了解到團(tuán)隊(duì)內(nèi)有些同學(xué)已經(jīng)在使用 uTools 了,要想讓他們從 uTools 上把插件零成本遷移到 Rubick 上,就必須實(shí)現(xiàn) uTools 的部分 API 能力,以及插件的定義和寫(xiě)法也需要和 uTools 規(guī)范保持一致。
2.1 開(kāi)發(fā)者模式
插件開(kāi)發(fā)需要和 Rubick 進(jìn)行聯(lián)調(diào),所以 Rubick 需要支持開(kāi)發(fā)者模式,幫助開(kāi)發(fā)者更好的開(kāi)發(fā)插件。首先先建一個(gè) plugin.json 用于描述插件的基礎(chǔ)信息:
{
"pluginName": "測(cè)試插件",
"author": "muwoo",
"description": "我的第一個(gè) rubick 插件",
"main": "index.html",
"version": "0.0.2",
"logo": "logo.png",
"name": "rubick-plugin-demo",
"gitUrl": "",
"features": [
{
"code": "hello",
"explain": "這是一個(gè)測(cè)試的插件",
"cmds":["hello222", "你好"]
}
],
"preload": "preload.js"
}
2.1.1 核心字段
name插件倉(cāng)庫(kù)名稱(chēng)pluginName插件名稱(chēng)description插件描述,簡(jiǎn)潔的說(shuō)明這個(gè)插件的作用main入口文件,如果沒(méi)有定義入口文件,此插件將變成一個(gè)模版插件version插件的版本,用于版本更新提示features插件核心功能列表features.code插件某個(gè)功能的識(shí)別碼,可用于區(qū)分不同的功能features.cmds通過(guò)哪些方式可以進(jìn)入這個(gè)功能
2.1.2 示例
開(kāi)發(fā)插件的方式是復(fù)制 plugin.json 進(jìn)入到 Rubick 的搜索框,所以需要監(jiān)聽(tīng)搜索框的 change 事件,用于讀取當(dāng)前剪切板復(fù)制的內(nèi)容:
onSearch ({ commit }, paylpad) {
// 獲取剪切板復(fù)制的文件路徑
const fileUrl = clipboard.read('public.file-url').replace('file://', '');
// 如果是復(fù)制 plugin.json 文件
if (fileUrl && value === 'plugin.json') {
// 讀取 json 文件
const config = JSON.parse(fs.readFileSync(fileUrl, 'utf-8'));
// 生成插件配置
const pluginConfig = {
...config,
// 記錄 index.html 存方的路徑
sourceFile: path.join(fileUrl, `../${config.main || 'index.html'}`),
id: uuidv4(),
// 標(biāo)記為開(kāi)發(fā)者
type: 'dev',
// 讀取 icon
icon: 'image://' + path.join(fileUrl, `../${config.logo}`),
// 標(biāo)記是否是模板
subType: (() => {
if (config.main) {
return ''
}
return 'template';
})()
};
}
}
到這里我們已經(jīng)可以根據(jù)復(fù)制的 plugin.json 能獲取到插件的最基礎(chǔ)的信息,接下來(lái)就是需要展示搜索框:
commit('commonUpdate', {
options: [
{
name: '新建rubick開(kāi)發(fā)插件',
value: 'new-plugin',
icon: 'https://xxx.com/img.png',
desc: '新建rubick開(kāi)發(fā)插件',
click: (router) => {
commit('commonUpdate', {
showMain: true,
selected: {
key: 'plugin',
name: '新建rubick開(kāi)發(fā)插件'
},
current: ['dev'],
});
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight(),
});
router.push('/home/dev')
}
},
{
name: '復(fù)制路徑',
desc: '復(fù)制路徑',
value: 'copy-path',
icon: 'https://xxx.com/img.png',
click: () => {
clipboard.writeText(fileUrl);
commit('commonUpdate', {
showMain: false,
selected: null,
options: [],
});
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight([]),
});
remote.Notification('Rubick 通知', { body: '復(fù)制成功' });
}
}
]
});
到這里,當(dāng)復(fù)制 plugin.json 進(jìn)入搜索框時(shí),便可直接出現(xiàn) 2 個(gè)選項(xiàng),一個(gè)新建插件,一個(gè)復(fù)制路徑的功能:

當(dāng)點(diǎn)擊 新建 rubick 插件 功能時(shí),則需要跳轉(zhuǎn)到 home 頁(yè),加載插件的基礎(chǔ)內(nèi)容,唯一需要注意的是 home 頁(yè)加載的內(nèi)容高度應(yīng)該是 Rubick 最大窗口的高度。所以需要調(diào)整窗口大?。?/p>
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight(),
});
關(guān)于 renderer 里面的 Vue 代碼這里就不再詳細(xì)介紹了,因?yàn)榇蠖嗍?css 畫(huà)一下就好了,直接來(lái)看展示界面:

到這里,就完成了開(kāi)發(fā)者模式,接下來(lái)再聊聊插件是如何在 Rubick 中跑起來(lái)的。
2.3 插件運(yùn)行原理
運(yùn)行插件需要容器 Electron 提供了一個(gè) webview 的容器來(lái)加載外部網(wǎng)頁(yè)。所以可以借助 webview 的能力實(shí)現(xiàn)動(dòng)態(tài)網(wǎng)頁(yè)渲染,這里所謂的網(wǎng)頁(yè)就是插件。但是網(wǎng)頁(yè)無(wú)法使用 node 的能力,而且做插件的目的就是為了開(kāi)放與約束,需要對(duì)插件開(kāi)放一些內(nèi)置的 API 能力。好在 webview 提供了一個(gè) preload 的能力,可以在頁(yè)面加載的時(shí)候去預(yù)置一個(gè)腳本來(lái)執(zhí)行。
也就是說(shuō)可以給自己的插件寫(xiě)一個(gè) preload.js 來(lái)加載。但這里需要注意既要保持插件的個(gè)性又得向插件內(nèi)注入全局 API 供插件使用,所以可以直接加載 Rubick 內(nèi)置 preload.js,在 preload.js 內(nèi)再加載個(gè)性化的 preload.js:
// webview plugin.vue
<webview id="webview" :src="path" :preload="preload"></webview>
<script>
export default {
name: "index.vue",
data() {
return {
path: `File://${this.$route.query.sourceFile}`,
// 加載當(dāng)前 static 目錄中的 preload.js
preload: `File://${path.join(__static, './preload.js')}`,
webview: null,
query: this.$route.query,
config: {},
}
}
}
</script>
對(duì)于 preload.js 就可以這么用啦:
if (location.href.indexOf('targetFile') > -1) {
filePath = decodeURIComponent(getQueryVariable('targetFile'));
} else {
filePath = location.pathname.replace('file://', '');
}
window.utools = {
// utools 所有的 api 實(shí)現(xiàn)
}
// 加載插件 preload.js
require(path.join(filePath, '../preload.js'));
到這里就已經(jīng)實(shí)現(xiàn)了一個(gè)最基礎(chǔ)的插件加載,效果如下:

2.4 支持更多體驗(yàn)?zāi)芰?span style="display: none;">
隨后為了更加貼近 uTools 的體驗(yàn),我又開(kāi)始著手讓 Rubick 支持更多原生體驗(yàn)增強(qiáng)的特性:超級(jí)面板、模板、系統(tǒng)命令、全局快捷鍵等

三、最后
再次致敬 uTools!我做 Rubick 旨在技術(shù)分享,并不以商業(yè)化為目的。
以上就是我和 Rubick 的故事,如果 Rubick 對(duì)您有幫助,那么就請(qǐng)給個(gè) Star ? 鼓勵(lì)一下:
https://github.com/clouDr-f2e/rubick
機(jī)緣巧合我發(fā)現(xiàn)了 HelloGitHub 一個(gè)推薦開(kāi)源項(xiàng)目的平臺(tái),了解到鹵蛋也是喜歡打 Dota,我想那他應(yīng)該能感受到 Rubick 的魅力,所以我就抱著試一試的心態(tài)投稿了。先是有幸入選了月刊第 64 期,然后受邀寫(xiě)了這篇關(guān)于 Rubick 的故事。
