手把手教你實現一個支持插件化的 uTools 工具箱(一)


前言

對於前端同學來說,我們會經常用到各種小工具,比如:圖床、顏色拾取、二維碼生成器、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 進程所具備的能力。

有了這些基礎知識,假設你對 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM