手把手教你實現一個支援外掛化的 uTools 工具箱(一)

muwooo發表於2021-07-01

前言

對於前端同學來說,我們會經常用到各種小工具,比如:圖床、顏色拾取、二維碼生成器、url 管理、文字比對、json 格式化。當然我們可以 chrome 收藏夾來管理各種線上的小工具,但作為一個有追求的前端,我們不僅僅要自己用的爽,也可以將一些好用的工具給團隊用,提高團隊的研發效率。

所以基於以上訴求,市面上或者很多公司內部都會做一些滿足自己團隊需要的客戶端工具,大多是基於 electron 來實現。但如果本篇文章只介紹如何通過 electron 來做一個工具集,那就小了,格局小了! 要做就做大的,我們不僅僅為前端賦能,我們更需要為後端、測試、UI、產品甚至老闆提效賦能。所以一旦做大了,工具箱將會變得非常臃腫,體積也會越來越大。每次更新所有人都必須升級才能體驗新功能。已於以上問題,我們需要設計一款可插拔式的設計方式,用到時才安裝,用不到不需要進行安裝使用。外掛獨立於工具箱之外單獨釋出。

如果你對這樣一款工具也感興趣,可以繼續閱讀實現過程。不過可以直接上原始碼先:

Rubick 原始碼

取名

開篇第一步,按照我之前的套路都是先取好名字先佔個坑,之前寫了一本《從0開始視覺化搭建》的小冊,裡面基於 dota 取了個 coco 的名字。這次我取名的是 rubick拉比克。知道 rubick 技能的就能領會啥意思了,主要是可以隨心所欲想用啥技能就用啥技能,可插拔。和我們的理念也非常一致:

image.png

實現

初始化專案

這裡我採用的是 electron-vue 來做的專案腳手架,直接按照官網介紹,開始 create 好一個 electron 專案:

# 安裝 vue-cli 和 腳手架樣板程式碼
npm install -g vue-cli
vue init simulatedgreg/electron-vue rubick

# 安裝依賴並執行你的程式
cd rubick
yarn # 或者 npm install
yarn run dev # 或者 npm run dev

這裡需要注意的是,由於 electron-vue 這玩意使用的 electron 版本太低了,而官方的 electron 文件已經更新到最新的了,所以如果僅僅按照 electron-vue 的版本來開發,再參考官方文件,會發現有很多文件中有但是無法使用的情況。所以儘量升級到最新的版本。

如果你是windows使用者,在安裝期間遇到了關於node-gyp、C++庫等方面的問題的話,請參考官方文件給出的解決辦法

main 程式 和 renderer 程式

開發之前,有必要先了解一下 electronmain 程式 和 renderer 程式之間的關係,什麼是 main 程式:electron 專案啟動的時候會執行 main.js 的程式就是主程式,且一個專案有且只有一個主程式,建立視窗等所有系統事件都要在主程式中進行。簡單的說就是我們的 electron 專案的主程式只有一個, 主程式的執行程式碼需要寫到 main.js 中, 所有跟系統事件相關的程式碼統統都要寫在這裡。

什麼是 renderer 程式呢?最粗淺的理解就是 Renderer 程式負責的就是我們熟悉的頁面UI渲染。這裡其實有篇部落格總結的很好,想繼續瞭解的可以檢視 這裡我先引用一下這裡面的圖。這張圖包含了main 程式和 renderer 程式所具備的能力。

手把手教你實現一個支援外掛化的 uTools 工具箱(一)

有了這些基礎知識,假設你對 electron 相關的瞭解已經達到一個基本熟悉的層次。我們開始來進行開發工作。接下來的開發,我將會參考 utools 的設計互動,來一步步設計出一個類似於 utools 的 electron 工具箱。

視窗初始化

electron 可以通過 BrowserWindow 來新建一個視窗物件,這個時候需要構建一個 800 * 60 的主搜尋視窗,開啟 main.js 調整視窗尺寸和大小:

mainWindow = new BrowserWindow({
    height: 60,
    useContentSize: true,
    width: 800,
    frame: false,
    title: '拉比克',
    webPreferences: {
      webSecurity: false,
      enableRemoteModule: true,
      webviewTag: true,
      nodeIntegration: true
    }
})

這裡有幾個 webPreferences 引數需要說明一下:

  • webSecurity 如果 BrowserWindow 發起了一些跨域請求,webSecurity 可以設定成 false
  • enableRemoteModule 可以讓 renderer 程式使用 remote 模組
  • webviewTag 允許使用 webview 標籤
  • nodeIntegration 允許在網頁中使用 node

到這裡,我們就可以看到一個最基礎的互動視窗了!

image.png

開發者模式

外掛開發需要和 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"
}
核心欄位說明
  • name 外掛倉庫名稱,用於 github dowload 標緻
  • pluginName : 外掛名稱。
  • description : 外掛描述,簡潔的說明這個外掛的作用
  • main : 入口檔案,如果沒有定義入口檔案,此外掛將變成一個模版外掛
  • version : 外掛的版本,需要符合 Semver (語義化版本) 規範。用於版本更新提示
  • features : 外掛核心功能列表
  • features.code : 外掛某個功能的識別碼,在進入外掛時會傳遞給你的程式碼,可用於區分不同的功能,顯示不同的 UI
  • features.cmds : 通過哪些方式可以進入這個功能,中文會自動支援 拼音及拼音首字母,無須重複新增

開發外掛的方式是複製 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://static.91jkys.com/activity/img/b37ff555c748489f88f3adac15b76f18.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://static.91jkys.com/activity/img/ac0d4df0247345b9a84c8cd7ea3dd696.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個選項,一個新建外掛,一個複製路徑的功能:

image.png

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

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

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

image.png

到這裡,就完成了開發者模式,接下來再介紹如何讓外掛在 rubick 中跑起來。

執行外掛

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

也就是說我們可以給自己的外掛寫一個 preload.js 來載入。但這裡需要注意我們既要保持外掛的個性又得向外掛內注入全域性 API 供外掛使用,所以可以直接載入 rubick 內建 preload.jspreload.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'));

到這裡就已經實現了外掛的載入,我們來看看效果:

image.png

結語

本篇主要介紹如何實現一個類似於 utools 的外掛載入過程,當然這遠遠不是 utools 的全部,下期我們再介紹如何實現 utools 的全域性外掛,比如螢幕取色、截圖、搜尋等能力。歡迎大家前往體驗 Rubick 有問題可以隨時提 issue 我們會及時反饋。

另外,如果覺得設計實現思路對你有用,也歡迎給個 Star:https://github.com/clouDr-f2e/rubick

相關文章