[實戰] Electron + React 打造一個高顏值 cli-to-gui 僞終端工具

如今寫個前端誰還不用個構建工具,天天早晨回去,找對應的項目目錄,打開命令行工具,敲個npm run xxx,重複得有點無聊。特別面對着日漸增加的項目數量,好但願有個工具能夠幫我管理全部的項目,兩手抓,一手起項目,一手抓個人叉燒包。emmmm...html

須要項目源碼點這裏哈😊前端

界面功能介紹

項目添加:經過拖拽項目package.json文件到應用面板完成解析(不是.json文件?你試試) vue

項目管理:紅色區域展現添加的項目,支持切換/刪除/重命名;黃色區域展現package.json中的scripts腳本,點擊便可執行;綠色區域管理該項目下的多個命令行窗口,支持增長/刪除/切換;藍色區域爲命令行執行區域。 node

一次過知足您三個願望react

  1. 小型的Teminal客戶端,多tab切換,按項目分組管理
  2. 支持持久化存儲,從新打開應用,基本的項目信息可復原、不丟失
  3. 開發/打包不用敲命令行了,輕輕一點擊便可完成
  4. 高顏值,顏值即正義 (不接受反駁 !!!)

技術棧

  • 運行環境: node v8 + electron v4.0 + macOSX v10.13(還沒支持windows,由於node-pty在windows上一直運行出錯,使用官網demo測試都不行,黔驢技窮呀。大佬們有興趣能夠試試,指點下我 連接
  • electron 負責將web打包成一個桌面應用
  • react + redux 網頁端的開發框架
  • redux-persist 應用數據持久化方案
  • node-pty + xtem.js web端構造shell命令行容器的解決方案(vscode內置的shell終端也是基於他倆實現哦)

開發環境搭建

項目目錄一覽

electron —— electron-quick-startwebpack

package.json目錄的main字段指定electron應用的主渲染進程文件,除了該js文件之外,其餘的都屬於在渲染進程運行。git

"main": "electron/index.js"github

electron/index.js中,配置electron加載的文件——開發環境下加載開發服務器文件,生成環境下加載本地文件。web

isDev 
    ? mainWindow.loadURL('http://localhost:3000/index.html')
    : mainWindow.loadFile(path.join(__dirname, '../react/build/index.html'))
複製代碼

react —— create-react-appvuex

  • 把react的index.html指向electron目錄,在react/config/paths.js中修改

    appHtml: resolveApp('../electron/index.html')

  • react的腳手架沒有默認支持stylus?心好痛啊。本身動手,豐衣足食。create-react-app腳手架默認把webpack配置藏到node_modules中,須要執行npm run eject後才能釋放出來。找到/config/webpack.config.js文件,參照sass的配置,寫一遍stylus的,這樣以後xx.styl的文件會被stylus-loader處理,xx.module.styl的文件會被當成局部樣式處理,相似於.vue文件的<style lang="stylus" scoped>

    const stylusRegex = /\.(styl)$/;
    const stylusModuleRegex = /\.module\.(styl)$/;
    
    // module裏追加stylus的配置
    {
      test: stylusRegex,
      exclude: stylusModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 2,
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
        'stylus-loader'
      ),
      sideEffects: true,
    },
    {
      test: stylusModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 2,
          sourceMap: isEnvProduction && shouldUseSourceMap,
          modules: true,
          getLocalIdent: getCSSModuleLocalIdent,
        },
        'stylus-loader'
      ),
    }, 
    複製代碼
  • 使用開發者工具。安裝react-devtool(待續...)

Coding 編碼開始

react

  1. componentDidUpdate

    應用窗口resize後,web終端模擬器都要從新適配父元素的大小。在react中涉及dom更新後須要處理的邏輯(放在callback函數裏),一是使用this.setState({}, callback);二是在componentDidUpdate裏處理。前者特別方便快捷,更新行爲跟數據源綁定到一塊兒,相似於vuevm.$nextTick。後者就要麻煩不少了(誰叫你把狀態放在全局中處理呢),要特別設定一個isNew變量來決定dom的更新回調是否執行。

  2. ref屬性引用的傳遞

    ref屬性不屬於props,所以不走尋常路,在高階組件裏須要「委曲求全」地轉發(官宣)。

    // Layout.js
    import Main from 'Main.js'
    export default class Layout extends Component {
      constructor (props) {
        super(props)
        this.mainRef = React.createRef()
      }
    
      render () {
        return (
          <Main ref={this.mainRef} />
        )
      }
    }
    
    // Main.js
    class Main extends Component {
      render () {
        const {
          myRef
        } = this.props
    
        return (
          <div ref={myRef}></div>
        )
      }
    }
    
    export default React.forwardRef((props, ref) => {
      // 把ref引用賦給名爲'myRef'的props,達到傳遞的目的
      return (
        <Main
          myRef={ref}
          {...props}
        />
      )
    })
    複製代碼

redux

  1. 異步dispatch action

    可以使用redux-thunk/redux-saga,因爲nodejs環境原生支持文件同步讀取fs.readFileSync,因此如下兩種方法都可以。

  1. reselect

    相似vuex的computed屬性

    // /store/selectors/project.js
    import { createSelector } from 'reselect'
    
    // 計算依賴值
    const projectsSelector = state => state.project.projects
    const activeIdSelector = state => state.project.activeId
    
    export const getXtermList = createSelector(
      projectsSelector,
      activeIdSelector,
      (projects, id) => {
        // 入參對應createSelector前兩位參數的結果值
        const project = projects.find(p => p.id === id)
    
        // must return a new "xterms", otherwhiles, it cannot update. 這裏使用[ ...project.xterms ]是返回新的對象引用,不然不被看作有更新
        const xterms = (project && project.xterms) ? [...project.xterms] : []
    
        return xterms
      }
    )
    
    
    // /src/Tab.js
    import { getXtermList } from '/store/selectors/project.js'
    @connect(
      state => ({
        xterms: getXtermList(state),
      })
    )
    class Tabs extends Component {
    
      render () {
    
        <div>
          {
            this.props.xterms.map(() => (
              <div>
                {/* ... */}
              </div>
            ))
          }
        </div>
      }
    }
    export default Tabs
    複製代碼
  2. redux-persist

    • redux-persist也是使用的webStorage(localStorage/sessionStorage),只支持ES5的數據類型,所以須要對咱們的store數據作過濾,只留下項目基本信息的字段。
    • 官方文檔也是夠坑了,沒有講須要本身動手改造咱們的reducer。最後google個天荒地老纔在issue裏發現寶藏刷新後數據沒法恢復

node-pty + xterm.js

  1. node-pty僞終端是node和系統shell之間的通信中間庫;xterm.js負責繪製瀏覽器端的終端模擬器。web終端使用表單模擬輸入,基本具有全部表單的api能力,支持代碼自動觸發和手動輸入觸發。

    const os = window.require('os')
    const pty = window.require('node-pty')
    const Terminal = window.require('xterm').Terminal
    
    class Xterm {
    
      constructor () {
    
        this.xterm = null
        this.ptyProcess = null
    
        this.createTerminal()
      }
    
      createTerminal () {
    
        const shell = Xterm.shell
        // 建立僞終端進程
        this.ptyProcess = pty.spawn(shell, [], this.opts)
        // 建立web終端模擬器
        this.xterm = new Terminal()
    
        this.initEvent()
      }
    
      initEvent () {
    
        // web終端模擬器監聽用戶輸入,寫入系統shell
        this.xterm.on('data', data => {
          this.ptyProcess.write(data)
        })
        // node-pty監聽系統shell輸出,寫入web終端模擬器
        this.ptyProcess.on('data', data => {
          this.xterm.write(data)
        })
      }
    
      /**
       * 獲取系統信息,拿到對應的shell終端
       */
      static get shell () {
        return window.process.env[os.platform() === 'win32' ? 'COMSPEC' : 'bash']
      }
    }
    複製代碼

electron

  1. nodejs和webpack的模塊管理衝突 webpack繼續使用import/require,node模塊的引入使用window.require,就能夠逃過webpack的編譯

  1. main process 調試

    • 熱重啓:

      • electron加載 .html文件時可以使用electron-reload插件工具
      • 由於咱們的electron加載的是一個webpack-dev-server開發服務器,因此須要用nodemon(監聽除了react源碼——app文件夾之外的其餘文件)來作應用重啓,react代碼的熱重啓基於自身腳手架。
      // package.json
      "scripts": {
       "start": "electron .",
      "watch": "nodemon --watch . --ignore 'app' --exec \"npm start\"",
      "rebuild": "electron-rebuild -f -w node-pty"
      }
      複製代碼
    • 打印:使用electron-log,打印信息和node調試信息同樣展現在控制檯中

  2. 主進程和渲染進程的通訊

    Electron爲主進程( main process)和渲染器進程(renderer processes)通訊提供了多種實現方式,如可使用ipcRenderer 和 ipcMain模塊發送消息。經過這種方式能夠模擬右鍵菜單進行系統級的操做(如打開系統的某個文件目錄)

    // react
    const { ipcRenderer } = window.require('electron')
    // renderer process 發送顯示右鍵菜單的請求
    ipcRenderer.send('show-context-menu'})
    
    // electron
    const {
      app,
      BrowserWindow,
      ipcMain,
      Menu,
      MenuItem
    } = require('electron')
    
    const template = [
      {
        label: '重命名',
        click: this.rename.bind(this)
      },
      {
        label: '打開文件目錄',
        click: this.openFileManager.bind(this)
      }
    ]
    // 建立右鍵菜單
    const menu = Menu.buildFromTemplate(template)
    
    // main process 監聽renderer process請求
    ipcMain.on('show-context-menu', (e, data) => {
    
      const win = BrowserWindow.fromWebContents(e.sender)
      // 彈出右鍵菜單
      menu.popup(win)
    })
    複製代碼
  3. 原生desktop app菜單

    讓換膚/toggle控制檯/刷新程序等app功能常駐於程序菜單項裏

打包發佈electron-react項目

  • 打包。工具使用electron-builder,確保系統環境 必定要使用nodev8版本 必定要使用nodev8版本 必定要使用nodev8版本,曾經使用了v10,把網上幾乎全部的demo項目都運行過了一遍,發現都在打包過程當中出錯,絕望地死磕了三、4天。

    • react打包。文件引用使用相對路徑——在package.json中加入 "homepage": "./"。由於electron應用加載資源是使用本地文件的方式,使用相對路徑,而之前web服務後臺習慣使用絕對路徑加載。

    打包後的路徑爲相對路徑

    • node原生模塊的編譯

      若是項目裏使用了一些node原生模塊(用 C++ 編寫的 Node.js 擴展),在安裝後須要通過編譯才能被使用。例如該項目使用了node-pty,能夠經過如下兩種辦法編譯,不編譯會報錯!!第一種方式在npm install後將自動執行,第二種則須要手動執行。

      To ensure your native dependencies are always matched electron version 源自electron-builder的說明

      • "postinstall": "electron-builder install-app-deps"
      • "rebuild": "electron-rebuild -f -w node-pty"
    • electron依賴下載。windows和mac都有全局緩存路徑的,若是使用npm下載卡住沒法進行下去,能夠嘗試去淘寶鏡像網站(electron下載連接)下載文件放到對應系統的緩存目錄,而後使用npm install安裝已經下載的版本號,緩存的electron文件便可被使用。(我?固然是搬個🍇(和諧了)直接下載)

    mac緩存目錄
    windows緩存目錄

  • 發佈release版本

    使用Travis配合electron-builder --publish指令,git push後自動經過travis-ci打包,把app提交到github的release中。

    • 配置Travis CI,讓代碼倉庫和CI發佈流程關聯起來。參考教程
    • 配置 .travis.yml,打包發佈工做流的配置文件

    • 發佈後效果

    GitHub上已提供打包後的程序,歡迎下載使用或者下載源碼自行構建(目前僅支持macOS)下載體驗地址

  • 安裝程序

    安裝時提示非信任應用程序??抱歉,來不及作macOS簽名。因此須要自行容許運行程序。處理教程

後話

  • 體驗度、集成度更高的的electron+react項目模板 electron-react-boilerplate
  • 各位看官若是喜歡的話,麻煩點個贊star,謝謝鼓勵

參考鳴謝

相關文章
相關標籤/搜索