|
| 1 | +# 实现工具自由,开源的桌面工具箱 |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +在一切开始之前,首先要致敬 uTools!如果没有它就没有 Rubick。 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +大家好,我是“拉比克”(Rubick)项目的作者木偶。我做的 Rubick 是一款基于 Electron 的开源桌面工具箱,简单讲就是好多工具的集合,然后加上快速启动、丰富的插件扩展等功能于一体。 |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +没错!它的使用方式和外观几乎和 uTools 一摸一样。那我为什么放着免费的 uTools 不用,非要自己搞一个呢? |
| 14 | + |
| 15 | +事情的起因是这样的,出于安全方面的考虑有一些仅适用于公司内部的插件不能发布到插件市场,所以不能接入 uTools。但实在眼馋 uTools 式的便捷、用完即走的极简操作体验。在搜寻解决方案无果,同时也发现其他的小伙伴也有同样的诉求,所以我动手做了,然后把它开源了。 |
| 16 | + |
| 17 | +**Rubick 一款呼出超快、用完即走的开源工具箱,因为开源所以更自由!** |
| 18 | + |
| 19 | +> 项目地址:https://github.com/clouDr-f2e/rubick |
| 20 | +
|
| 21 | +希望它能帮助你解决同样的烦恼,但目前仅支持 [Windows 和 macOS](https://github.com/clouDr-f2e/rubick#%E5%AE%89%E8%A3%85%E5%8C%85),Linux 版本正在开发中。想借助开源的力量让 Rubick 变强,成为金牌辅助!帮助大家轻松“超神”! |
| 22 | + |
| 23 | +在做 Rubick 的过程中还是遇到了不少问题和挑战,下面就分享下我的心路历程。 |
| 24 | + |
| 25 | + |
| 26 | +## 一、缘起 |
| 27 | +### 1.1 初识 Electron |
| 28 | +Electron 是 GitHub 开源的一个框架。它通过 Node.js 和 Chromium 的渲染引擎完成跨平台的桌面 GUI 应用程序的开发。我起初没有接触过 Electron,最开始接触它是因为看到了 PicGo 的一个核心功能非常吸引我,就是 macOS 下可以直接拖拽图片进入任务托盘上传图片: |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +当时正好我们团队也需要搞一个内部的 CDN 图片资源管理图床,用于项目图片资源压缩并直接上传到 CDN 上,之前我们做了个网页版。而这里我深刻的感受到了 Electron 的强大,可以极大的提高工作效率,参考 PicGo 我尝试做了第一个 Electron 项目,完成了图片压缩上传到内部 CDN 的桌面端应用。 |
| 33 | + |
| 34 | +### 1.2 演化 |
| 35 | +之后公司内部因为开发和后端进行接口联调测试环境时,经常会涉及到一些状态改变要看交互样式的问题。比如测试需要测商品的待支付、支付中、支付完成等各种节点的交互样式是否符合预期,这种情况测试一般会去造数据或者让后端改数据库接口。有的小伙伴可能会用 Charles 修改返回数据进行测试,但 Charles 的抓包体验和配置体验感觉有点麻烦,对新人不是很友好所以我们自己做了个非常易用 抓包&mock 工具: |
| 36 | + |
| 37 | + |
| 38 | + |
| 39 | +**这也是 Rubick 最早的雏形**。随后,我们发现当页面发布线上的时候,没有办法在微信环境内对线上页面进行调试,所以开发了一个基于 winner 的远程调试功能。 |
| 40 | + |
| 41 | +但随着该 Rubick 在内部不断推广和使用,所需功能也越来越多。我们需要 需求管理、性能评估、埋点检测 等等工具。这些工具的增加一方面导致 Rubick 体积暴增,一方面又导致了用户需要不断更新软件,导致用户体验非常差。 |
| 42 | + |
| 43 | +其次,我们在推广给测试、UI 同学使用的时候,发现他们其实并不关注前面的页面调试、性能测评等功能,可能只是用到其中某一项,所以整个项目对他们来说就显得很臃肿。 |
| 44 | + |
| 45 | +### 1.3 灵感 |
| 46 | +直到有一天,我在掘金上看到这样一个沸点: |
| 47 | + |
| 48 | + |
| 49 | + |
| 50 | +下面有个评论提到了 uTools 这是我第一次和 uTools 产生了交集,在体验了 uTools 功能后,我长吸一口气:这不就是我想要的嘛!然后就去 GitHub 上找 uTools 的源码,发现它并没有开源。 |
| 51 | + |
| 52 | +所以就想把上面提到的那些工具, 发布到 uTools 市场在 uTools 里通过插件的方式使用他们。但我发现发布插件只能发布到公网,但这又涉及到数据安全的问题。 |
| 53 | + |
| 54 | +无奈,难道真的要自己做一个这样的工具吗?真的是有点头大。不过想想也挺有意思的。至此,我萌生了要开发一个媲美 uTools 的开源工具箱的念头。 |
| 55 | + |
| 56 | +## 二、研发 |
| 57 | + |
| 58 | +开篇第一步,按照我之前的套路都是先取好名字先占个坑。我是个 Dota 玩家,之前写了一本《从0开始可视化搭建》的小册,里面使用了 Dota 中一个英雄的名字 coco(船长)。这次我取名的是 rubick 即 拉比克。Rubick(拉比克) 也是 Dota 里面的英雄之一,其核心技能是插件化使用其他英雄的技能,用完即走。非常符合本工具的设计理念,所以取名 Rubick。 |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +我的核心目标就是需要让 Rubick 支持插件化,解决前面提到的问题: |
| 63 | +- 每个人的工具箱不同 |
| 64 | +- 软件体积暴增 |
| 65 | +- 每增加一个工具就需要更新版本 |
| 66 | + |
| 67 | +其次,通过调研了解到团队内有些同学已经在使用 uTools 了,要想让他们从 uTools 上把插件零成本迁移到 Rubick 上,就必须实现 uTools 的部分 API 能力,以及插件的定义和写法也需要和 uTools 规范保持一致。 |
| 68 | + |
| 69 | +### 2.1 开发者模式 |
| 70 | + |
| 71 | +插件开发需要和 Rubick 进行联调,所以 Rubick 需要支持开发者模式,帮助开发者更好的开发插件。首先先建一个 `plugin.json` 用于描述插件的基础信息: |
| 72 | + |
| 73 | +```json |
| 74 | +{ |
| 75 | + "pluginName": "测试插件", |
| 76 | + "author": "muwoo", |
| 77 | + "description": "我的第一个 rubick 插件", |
| 78 | + "main": "index.html", |
| 79 | + "version": "0.0.2", |
| 80 | + "logo": "logo.png", |
| 81 | + "name": "rubick-plugin-demo", |
| 82 | + "gitUrl": "", |
| 83 | + "features": [ |
| 84 | + { |
| 85 | + "code": "hello", |
| 86 | + "explain": "这是一个测试的插件", |
| 87 | + "cmds":["hello222", "你好"] |
| 88 | + } |
| 89 | + ], |
| 90 | + "preload": "preload.js" |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +#### 2.1.1 核心字段 |
| 95 | +* `name` 插件仓库名称 |
| 96 | +* `pluginName` 插件名称 |
| 97 | +* `description` 插件描述,简洁的说明这个插件的作用 |
| 98 | +* `main` 入口文件,如果没有定义入口文件,此插件将变成一个模版插件 |
| 99 | +* `version` 插件的版本,用于版本更新提示 |
| 100 | +* `features` 插件核心功能列表 |
| 101 | +* `features.code` 插件某个功能的识别码,可用于区分不同的功能 |
| 102 | +* `features.cmds` 通过哪些方式可以进入这个功能 |
| 103 | + |
| 104 | + |
| 105 | +#### 2.1.2 示例 |
| 106 | +开发插件的方式是复制 `plugin.json` 进入到 Rubick 的搜索框,所以需要监听搜索框的 `change` 事件,用于读取当前剪切板复制的内容: |
| 107 | +```js |
| 108 | +onSearch ({ commit }, paylpad) { |
| 109 | + // 获取剪切板复制的文件路径 |
| 110 | + const fileUrl = clipboard.read('public.file-url').replace('file://', ''); |
| 111 | + |
| 112 | + // 如果是复制 plugin.json 文件 |
| 113 | + if (fileUrl && value === 'plugin.json') { |
| 114 | + // 读取 json 文件 |
| 115 | + const config = JSON.parse(fs.readFileSync(fileUrl, 'utf-8')); |
| 116 | + // 生成插件配置 |
| 117 | + const pluginConfig = { |
| 118 | + ...config, |
| 119 | + // 记录 index.html 存方的路径 |
| 120 | + sourceFile: path.join(fileUrl, `../${config.main || 'index.html'}`), |
| 121 | + id: uuidv4(), |
| 122 | + // 标记为开发者 |
| 123 | + type: 'dev', |
| 124 | + // 读取 icon |
| 125 | + icon: 'image://' + path.join(fileUrl, `../${config.logo}`), |
| 126 | + // 标记是否是模板 |
| 127 | + subType: (() => { |
| 128 | + if (config.main) { |
| 129 | + return '' |
| 130 | + } |
| 131 | + return 'template'; |
| 132 | + })() |
| 133 | + }; |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | +到这里我们已经可以根据复制的 `plugin.json` 能获取到插件的最基础的信息,接下来就是需要展示搜索框: |
| 138 | + |
| 139 | +```js |
| 140 | + commit('commonUpdate', { |
| 141 | + options: [ |
| 142 | + { |
| 143 | + name: '新建rubick开发插件', |
| 144 | + value: 'new-plugin', |
| 145 | + icon: 'https://xxx.com/img.png', |
| 146 | + desc: '新建rubick开发插件', |
| 147 | + click: (router) => { |
| 148 | + commit('commonUpdate', { |
| 149 | + showMain: true, |
| 150 | + selected: { |
| 151 | + key: 'plugin', |
| 152 | + name: '新建rubick开发插件' |
| 153 | + }, |
| 154 | + current: ['dev'], |
| 155 | + }); |
| 156 | + ipcRenderer.send('changeWindowSize-rubick', { |
| 157 | + height: getWindowHeight(), |
| 158 | + }); |
| 159 | + router.push('/home/dev') |
| 160 | + } |
| 161 | + }, |
| 162 | + { |
| 163 | + name: '复制路径', |
| 164 | + desc: '复制路径', |
| 165 | + value: 'copy-path', |
| 166 | + icon: 'https://xxx.com/img.png', |
| 167 | + click: () => { |
| 168 | + clipboard.writeText(fileUrl); |
| 169 | + commit('commonUpdate', { |
| 170 | + showMain: false, |
| 171 | + selected: null, |
| 172 | + options: [], |
| 173 | + }); |
| 174 | + ipcRenderer.send('changeWindowSize-rubick', { |
| 175 | + height: getWindowHeight([]), |
| 176 | + }); |
| 177 | + remote.Notification('Rubick 通知', { body: '复制成功' }); |
| 178 | + } |
| 179 | + } |
| 180 | + ] |
| 181 | +}); |
| 182 | + |
| 183 | +``` |
| 184 | + |
| 185 | +到这里,当复制 `plugin.json` 进入搜索框时,变可直接出现 2 个选项,一个新建插件,一个复制路径的功能: |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | +当点击 `新建 rubick 插件` 功能时,则需要跳转到 `home` 页,加载插件的基础类容,唯一需要注意的是 `home` 页加载的内容高度应该是 Rubick 最大窗口的高度。所以需要调整窗口大小: |
| 190 | +```js |
| 191 | + ipcRenderer.send('changeWindowSize-rubick', { |
| 192 | + height: getWindowHeight(), |
| 193 | + }); |
| 194 | +``` |
| 195 | + |
| 196 | +关于 `renderer` 里面的 Vue 代码这里就不再详细介绍了,因为大多是 css 画一下就好了,直接来看展示界面: |
| 197 | + |
| 198 | + |
| 199 | + |
| 200 | +到这里,就完成了开发者模式,接下来再聊聊插件是如何在 Rubick 中跑起来的。 |
| 201 | + |
| 202 | +### 2.3 插件运行原理 |
| 203 | + |
| 204 | +运行插件需要容器 Electron 提供了一个 `webview` 的容器来加载外部网页。所以可以借助 `webview` 的能力实现动态网页渲染,这里所谓的网页就是插件。但是网页无法使用 node 的能力,而且做插件的目的就是为了开放与约束,需要对插件开放一些内置的 API 能力。好在 `webview` 提供了一个 `preload` 的能力,可以在页面加载的时候去预置一个脚本来执行。 |
| 205 | + |
| 206 | +也就是说可以给自己的插件写一个 `preload.js` 来加载。但这里需要注意既要保持插件的个性又得向插件内注入全局 `API` 供插件使用,所以可以直接加载 Rubick 内置 `preload.js`,在 `preload.js` 内再加载个性化的 `preload.js`: |
| 207 | + |
| 208 | +```html |
| 209 | +// webview plugin.vue |
| 210 | +<webview id="webview" :src="path" :preload="preload"></webview> |
| 211 | +<script> |
| 212 | +export default { |
| 213 | + name: "index.vue", |
| 214 | + data() { |
| 215 | + return { |
| 216 | + path: `File://${this.$route.query.sourceFile}`, |
| 217 | + // 加载当前 static 目录中的 preload.js |
| 218 | + preload: `File://${path.join(__static, './preload.js')}`, |
| 219 | + webview: null, |
| 220 | + query: this.$route.query, |
| 221 | + config: {}, |
| 222 | + } |
| 223 | + } |
| 224 | +} |
| 225 | +</script> |
| 226 | +``` |
| 227 | + |
| 228 | +对于 `preload.js` 就可以这么用啦: |
| 229 | + |
| 230 | +```js |
| 231 | +if (location.href.indexOf('targetFile') > -1) { |
| 232 | + filePath = decodeURIComponent(getQueryVariable('targetFile')); |
| 233 | +} else { |
| 234 | + filePath = location.pathname.replace('file://', ''); |
| 235 | +} |
| 236 | + |
| 237 | + |
| 238 | +window.utools = { |
| 239 | + // utools 所有的 api 实现 |
| 240 | +} |
| 241 | +// 加载插件 preload.js |
| 242 | +require(path.join(filePath, '../preload.js')); |
| 243 | +``` |
| 244 | + |
| 245 | +到这里就已经实现了一个最基础的插件加载,效果如下: |
| 246 | + |
| 247 | + |
| 248 | + |
| 249 | +### 2.4 支持更多体验能力 |
| 250 | +随后为了更加贴近 uTools 的体验,我又开始着手让 Rubick 支持更多原生体验增强的特性:超级面板、模版、系统命令、全局快捷键等 |
| 251 | + |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | + |
| 256 | +## 三、最后 |
| 257 | + |
| 258 | +再次致敬 uTools!我做 Rubick 旨在技术分享,并不以商业化为目的。 |
| 259 | + |
| 260 | +这就是我和 Rubick 的故事,如果它对您有帮助请给个 Star ✨ 鼓励一下: |
| 261 | + |
| 262 | +> https://github.com/clouDr-f2e/rubick |
| 263 | +
|
| 264 | +--- |
| 265 | + |
| 266 | +机缘巧合我发现了 **HelloGitHub 一个推荐开源项目的平台**,了解到卤蛋也是喜欢打 Dota,我想那他应该能感受到 Rubick 的魅力,所以我就抱着试一试的心态投稿了。先是有幸入选了月刊[第 64 期](https://mp.weixin.qq.com/s/5vWG0-n-NMVl0KWz6FXneg),然后受邀写了这篇关于 Rubick 的故事。 |
| 267 | + |
| 268 | +最后,感谢 HelloGitHub 让 Rubick 被更多人发现和喜欢,特别感谢卤蛋对文章的润色和修改,让本文增色不少。 |
0 commit comments