實現工具自由,開源的桌面工具箱

HelloGitHub發表於2021-08-04

在一切開始之前,首先要致敬 uTools!​如果沒有它就沒有 Rubick。

大家好,我是“拉比克”(Rubick)專案的作者木偶。我做的 Rubick 是一款基於 Electron 的開源桌面工具箱,簡單講就是好多工具的集合,然後加上快速啟動、豐富的外掛擴充套件等功能於一體。

沒錯!它的使用方式和外觀幾乎和 uTools 一摸一樣。那我為什麼放著免費的 uTools 不用,非要自己搞一個呢?

事情的起因是這樣的,出於安全方面的考慮有一些僅適用於公司內部的外掛不能釋出到外掛市場,所以不能接入 uTools。但實在眼饞 uTools 式的便捷、用完即走的極簡操作體驗。在搜尋解決方案無果,同時也發現其他的小夥伴也有同樣的訴求,所以我動手做了,然後把它開源了。

Rubick 一款撥出超快、用完即走的開源工具箱,因為開源所以更自由!

專案地址:https://github.com/clouDr-f2e/rubick

希望它能幫助你解決同樣的煩惱,但目前僅支援 Windows 和 macOS,Linux 版本正在開發中。想借助開源的力量讓 Rubick 變強,成為金牌輔助!幫助大家輕鬆“超神”!

在做 Rubick 的過程中還是遇到了不少問題和挑戰,下面就分享下我的心路歷程。

一、緣起

1.1 初識 Electron

Electron 是 GitHub 開源的一個框架。它通過 Node.js 和 Chromium 的渲染引擎完成跨平臺的桌面 GUI 應用程式的開發。我起初沒有接觸過 Electron,最開始接觸它是因為看到了 PicGo 的一個核心功能非常吸引我,就是 macOS 下可以直接拖拽圖片進入任務托盤上傳圖片:

當時正好我們團隊也需要搞一個內部的 CDN 圖片資源管理圖床,用於專案圖片資源壓縮並直接上傳到 CDN 上,之前我們做了個網頁版。而這裡我深刻的感受到了 Electron 的強大,可以極大的提高工作效率,參考 PicGo 我嘗試做了第一個 Electron 專案,完成了圖片壓縮上傳到內部 CDN 的桌面端應用。

1.2 演化

之後公司內部因為開發和後端進行介面聯調測試環境時,經常會涉及到一些狀態改變要看互動樣式的問題。比如測試需要測商品的待支付、支付中、支付完成等各種節點的互動樣式是否符合預期,這種情況測試一般會去造資料或者讓後端改資料庫介面。有的小夥伴可能會用 Charles 修改返回資料進行測試,但 Charles 的抓包體驗和配置體驗感覺有點麻煩,對新人不是很友好所以我們自己做了個非常易用 抓包&mock 工具:

這也是 Rubick 最早的雛形。隨後,我們發現當頁面釋出線上的時候,沒有辦法在微信環境內對線上頁面進行除錯,所以開發了一個基於 winner 的遠端除錯功能。

但隨著該 Rubick 在內部不斷推廣和使用,所需功能也越來越多。我們需要 需求管理、效能評估、埋點檢測 等等工具。這些工具的增加一方面導致 Rubick 體積暴增,一方面又導致了使用者需要不斷更新軟體,導致使用者體驗非常差。

其次,我們在推廣給測試、UI 同學使用的時候,發現他們其實並不關注前面的頁面除錯、效能測評等功能,可能只是用到其中某一項,所以整個專案對他們來說就顯得很臃腫。

1.3 靈感

直到有一天,我在掘金上看到這樣一個沸點:

下面有個評論提到了 uTools 這是我第一次和 uTools 產生了交集,在體驗了 uTools 功能後,我長吸一口氣:這不就是我想要的嘛!然後就去 GitHub 上找 uTools 的原始碼,發現它並沒有開源。

所以就想把上面提到的那些工具, 釋出到 uTools 市場在 uTools 裡通過外掛的方式使用他們。但我發現釋出外掛只能釋出到公網,但這又涉及到資料安全的問題。

無奈,難道真的要自己做一個這樣的工具嗎?真的是有點頭大。不過想想也挺有意思的。至此,我萌生了要開發一個媲美 uTools 的開源工具箱的念頭。

二、研發

開篇第一步,按照我之前的套路都是先取好名字先佔個坑。我是個 Dota 玩家,之前寫了一本《從0開始視覺化搭建》的小冊,裡面使用了 Dota 中一個英雄的名字 coco(船長)。這次我取名的是 rubick 即 拉比克。Rubick(拉比克) 也是 Dota 裡面的英雄之一,其核心技能是外掛化使用其他英雄的技能,用完即走。非常符合本工具的設計理念,所以取名 Rubick。

我的核心目標就是需要讓 Rubick 支援外掛化,解決前面提到的問題:

  • 每個人的工具箱不同
  • 軟體體積暴增
  • 每增加一個工具就需要更新版本

其次,通過調研瞭解到團隊內有些同學已經在使用 uTools 了,要想讓他們從 uTools 上把外掛零成本遷移到 Rubick 上,就必須實現 uTools 的部分 API 能力,以及外掛的定義和寫法也需要和 uTools 規範保持一致。

2.1 開發者模式

外掛開發需要和 Rubick 進行聯調,所以 Rubick 需要支援開發者模式,幫助開發者更好的開發外掛。首先先建一個 plugin.json 用於描述外掛的基礎資訊:

{
  "pluginName": "測試外掛",
  "author": "muwoo",
  "description": "我的第一個 rubick 外掛",
  "main": "index.html",
  "version": "0.0.2",
  "logo": "logo.png",
  "name": "rubick-plugin-demo",
  "gitUrl": "",
  "features": [
    {
      "code": "hello",
      "explain": "這是一個測試的外掛",
      "cmds":["hello222", "你好"]
    }
  ],
  "preload": "preload.js"
}

2.1.1 核心欄位

  • name 外掛倉庫名稱
  • pluginName 外掛名稱
  • description 外掛描述,簡潔的說明這個外掛的作用
  • main 入口檔案,如果沒有定義入口檔案,此外掛將變成一個模版外掛
  • version 外掛的版本,用於版本更新提示
  • features 外掛核心功能列表
  • features.code 外掛某個功能的識別碼,可用於區分不同的功能
  • features.cmds 通過哪些方式可以進入這個功能

2.1.2 示例

開發外掛的方式是複製 plugin.json 進入到 Rubick 的搜尋框,所以需要監聽搜尋框的 change 事件,用於讀取當前剪下板複製的內容:

onSearch ({ commit }, paylpad) {
  // 獲取剪下板複製的檔案路徑
  const fileUrl = clipboard.read('public.file-url').replace('file://', '');
  
  // 如果是複製 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(),
        // 標記為開發者
        type: 'dev',
        // 讀取 icon
        icon: 'image://' + path.join(fileUrl, `../${config.logo}`),
        // 標記是否是模板
        subType: (() => {
          if (config.main) {
            return ''
          }
          return 'template';
        })()
      };
  }
}

到這裡我們已經可以根據複製的 plugin.json 能獲取到外掛的最基礎的資訊,接下來就是需要展示搜尋框:

 commit('commonUpdate', {
    options: [
      {
        name: '新建rubick開發外掛',
        value: 'new-plugin',
        icon: 'https://xxx.com/img.png',
        desc: '新建rubick開發外掛',
        click: (router) => {
          commit('commonUpdate', {
            showMain: true,
            selected: {
              key: 'plugin',
              name: '新建rubick開發外掛'
            },
            current: ['dev'],
          });
          ipcRenderer.send('changeWindowSize-rubick', {
            height: getWindowHeight(),
          });
          router.push('/home/dev')
        }
      },
      {
        name: '複製路徑',
        desc: '複製路徑',
        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: '複製成功' });
        }
      }
    ]
});

到這裡,當複製 plugin.json 進入搜尋框時,變可直接出現 2 個選項,一個新建外掛,一個複製路徑的功能:

當點選 新建 rubick 外掛 功能時,則需要跳轉到 home 頁,載入外掛的基礎類容,唯一需要注意的是 home 頁載入的內容高度應該是 Rubick 最大視窗的高度。所以需要調整視窗大小:

 ipcRenderer.send('changeWindowSize-rubick', {
    height: getWindowHeight(),
 });

關於 renderer 裡面的 Vue 程式碼這裡就不再詳細介紹了,因為大多是 css 畫一下就好了,直接來看展示介面:

到這裡,就完成了開發者模式,接下來再聊聊外掛是如何在 Rubick 中跑起來的。

2.3 外掛執行原理

執行外掛需要容器 Electron 提供了一個 webview 的容器來載入外部網頁。所以可以藉助 webview 的能力實現動態網頁渲染,這裡所謂的網頁就是外掛。但是網頁無法使用 node 的能力,而且做外掛的目的就是為了開放與約束,需要對外掛開放一些內建的 API 能力。好在 webview 提供了一個 preload 的能力,可以在頁面載入的時候去預置一個指令碼來執行。

也就是說可以給自己的外掛寫一個 preload.js 來載入。但這裡需要注意既要保持外掛的個性又得向外掛內注入全域性 API 供外掛使用,所以可以直接載入 Rubick 內建 preload.js,在 preload.js 內再載入個性化的 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}`,
      // 載入當前 static 目錄中的 preload.js
      preload: `File://${path.join(__static, './preload.js')}`,
      webview: null,
      query: this.$route.query,
      config: {},
    }
  }
}
</script>

對於 preload.js 就可以這麼用啦:

if (location.href.indexOf('targetFile') > -1) {
  filePath = decodeURIComponent(getQueryVariable('targetFile'));
} else {
  filePath = location.pathname.replace('file://', '');
}


window.utools = {
  // utools 所有的 api 實現
}
// 載入外掛 preload.js
require(path.join(filePath, '../preload.js'));

到這裡就已經實現了一個最基礎的外掛載入,效果如下:

2.4 支援更多體驗能力

隨後為了更加貼近 uTools 的體驗,我又開始著手讓 Rubick 支援更多原生體驗增強的特性:超級皮膚、模版、系統命令、全域性快捷鍵等

三、最後

再次致敬 uTools!我做 Rubick 旨在技術分享,並不以商業化為目的。

以上就是我和 Rubick 的故事,如果 Rubick ​對您有幫助,那麼就請給個 Star ✨ 鼓勵一下:

https://github.com/clouDr-f2e/rubick


機緣巧合我發現了 HelloGitHub 一個推薦開源專案的平臺,瞭解到滷蛋也是喜歡打 Dota,我想那他應該能感受到 Rubick 的魅力,所以我就抱著試一試的心態投稿了。先是有幸入選了月刊第 64 期,然後受邀寫了這篇關於 Rubick 的故事。

​最後,感謝 HelloGitHub 讓 Rubick 被更多人發現和喜歡,特別感謝滷蛋對文章的潤色和修改,讓本文增色不少。


關注 HelloGitHub 公眾號 第一時間收到更新。

還有更多開源專案的介紹和寶藏專案等待你的發現。

相關文章