Electron
很出名,不少人可能瞭解過,知道它是用來開發桌面端的應用,可是一直沒有在項目中實踐過,缺少練手的實踐項目。css
不少開源的命令行終端都是使用Electron
來開發的,本文將從零開始手把手的教你們用Electron
寫一個命令行終端。html
做爲一個完整的實戰項目示例,該終端demo也將集成到Electron
開源學習項目electron-playground中,目前這個項目擁有800+ Star⭐️,它最大的特色是所見即所得的演示Electron
的各類特性,幫助你們快速學習、上手Electron
。前端
你們跟着本文一塊兒來試試Electron吧~vue
開源地址: electron-terminal-demonode
giit提交代碼演示linux
命令行面板作了哪些事情android
下載試玩git
npm init npm install electron -D
若是Electron安裝不上去,須要添加一個.npmrc
文件,來修改Electron
的安裝地址,文件內容以下:github
registry=https://registry.npm.taobao.org/ electron_mirror=https://npm.taobao.org/mirrors/electron/ chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver
修改一下package.json
的入口main
和scripts
選項, 如今package.json
長這樣,很簡潔:web
{ "name": "electron-terminal", "version": "1.0.0", "main": "./src/index.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^11.1.1" } }
咱們最終實現的項目將是下面這樣子的,頁面css文件不算的話,咱們只須要實現src下面的三個文件便可。
. ├── .vscode // 使用vscode的調試功能啓動項目 ├── node_dodules ├── src │ ├── index.js // Electron啓動入口-建立窗口 │ └── processMessage.js // 主進程和渲染進程通訊類-進程通訊、監聽時間 │ └── index.html // 窗口html頁面-命令行面板、執行命令並監聽輸出 │ └── index.css // 窗口html的css樣式 這部分不寫 ├── package.json └── .npmrc // 修改npm安裝包的地址 └── .gitignore
// ./src/index.js const { app, BrowserWindow } = require('electron') const processMessage = require('./processMessage') // 建立窗口 function createWindow() { // 建立窗口 const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, // 頁面直接使用node的能力 用於引入node模塊 執行命令 }, }) // 加載本地頁面 win.loadFile('./src/index.html') win.webContents.openDevTools() // 打開控制檯 // 主線程和渲染進程通訊 const ProcessMessage = new processMessage(win) ProcessMessage.init() } // app ready 建立窗口 app.whenReady().then(createWindow)
electron分爲主進程和渲染進程,由於進程不一樣,在各類事件發生的對應時機須要相互通知來執行一些功能。
這個類就是用於它們之間的通訊的,electron通訊這部分封裝的很簡潔了,照着用就能夠了。
// ./src/processMessage.js const { ipcMain } = require('electron') class ProcessMessage { /** * 進程通訊 * @param {*} win 建立的窗口 */ constructor(win) { this.win = win } init() { this.watch() this.on() } // 監聽渲染進程事件通訊 watch() { // 頁面準備好了 ipcMain.on('page-ready', () => { this.sendFocus() }) } // 監聽窗口、app、等模塊的事件 on() { // 監聽窗口是否聚焦 this.win.on('focus', () => { this.sendFocus(true) }) this.win.on('blur', () => { this.sendFocus(false) }) } /** * 窗口聚焦事件發送 * @param {*} isActive 是否聚焦 */ sendFocus(isActive) { // 主線程發送事件給窗口 this.win.webContents.send('win-focus', isActive) } } module.exports = ProcessMessage
在建立窗口的時候,咱們賦予了窗口使用node的能力, 能夠在html中直接使用node模塊。
因此咱們不須要經過進程通訊的方式來執行命令和渲染輸出,能夠直接在一個文件裏面完成。
終端的核心在於執行命令,渲染命令行輸出,保存命令行的輸出。
這些都在這個文件裏面實現了,代碼行數不到250行。
核心:執行命令監聽命令行輸出
圍繞執行命令行的細節處理
spawn
是node子進程模塊child_process
提供的一個異步方法。
它的做用是執行命令而且能夠實時監聽命令行執行的輸出。
當我第一次知道這個API的時候,我就感受這個方法簡直是爲命令行終端量身定作的。
終端的核心也是執行命令行,而且實時輸出命令行執行期間的信息。
下面就來看看它的使用方式。
const { spawn } = require('child_process'); const ls = spawn('ls', { encoding: 'utf8', cwd: process.cwd(), // 執行命令路徑 shell: true, // 使用shell命令 }) // 監聽標準輸出 ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); // 監聽標準錯誤 ls.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); // 子進程關閉事件 ls.on('close', (code) => { console.log(`子進程退出,退出碼 ${code}`); });
api的使用很簡單,可是終端信息的輸出,須要不少細節的處理,好比下面這個。
stderr
雖然是標準錯誤輸出,但裏面的信息不全是錯誤的信息,不一樣的工具會有不一樣的處理。
對於git
來講,有不少命令行操做的輸出信息都輸出在stederr
上。
好比git clone
、git push
等,信息輸出在stederr
中,咱們不能將其視爲錯誤。
git
老是將詳細的狀態信息和進度報告,以及只讀信息,發送給stederr
。
具體細節能夠查看git stderr(錯誤流)探祕等資料。
暫時還不清楚其餘工具/命令行也有沒有相似的操做,可是很明顯咱們不能將stederr
的信息視爲錯誤的信息。
PS: 對於git若是想提供更好的支持,須要根據不一樣的git
命令進行特殊處理,好比對下面clear
命令和cd
命令的特殊處理。
根據子進程close事件判斷命令行是否執行成功
咱們應該檢測close
事件的退出碼code
, 若是code
爲0則表示命令行執行成功,不然即爲失敗。
下面這段是命令行面板的核心代碼,我貼一下你們重點看一下,
其餘部分都是一些細節、優化體驗、狀態處理這樣的代碼,下面會將完整的html貼上來。
const { spawn } = require('child_process') // 使用node child_process模塊 // 執行命令行 actionCommand() { // 處理command命令 const command = this.command.trim() this.isClear(command) if (this.command === '') return // 執行命令行 this.action = true this.handleCommand = this.cdCommand(command) const ls = spawn(this.handleCommand, { encoding: 'utf8', cwd: this.path, // 執行命令路徑 shell: true, // 使用shell命令 }) // 監聽命令行執行過程的輸出 ls.stdout.on('data', (data) => { const value = data.toString().trim() this.commandMsg.push(value) console.log(`stdout: ${value}`) }) ls.stderr.on('data', this.stderrMsgHandle) ls.on('close', this.closeCommandAction) }, // 錯誤或詳細狀態進度報告 好比 git push stderrMsgHandle(data) { console.log(`stderr: ${data}`) this.commandMsg.push(`stderr: ${data}`) }, // 執行完畢 保存信息 更新狀態 closeCommandAction(code) { // 保存執行信息 this.commandArr.push({ code, // 是否執行成功 path: this.path, // 執行路徑 command: this.command, // 執行命令 commandMsg: this.commandMsg.join('\r'), // 執行信息 }) // 清空 this.updatePath(this.handleCommand, code) this.commandFinish() console.log( `子進程退出,退出碼 ${code}, 運行${code === 0 ? '成功' : '失敗'}` ) }
這裏是html的完整代碼,代碼中有詳細註釋,建議根據上面的命令行面板作了哪些事情,來閱讀源碼。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>極簡electron終端</title> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" /> <script src="https://unpkg.com/vue"></script> <!-- 引入element --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <!-- css --> <link rel="stylesheet" href="./index.css" /> </head> <body> <div id="app"> <div class="main-class"> <!-- 渲染過往的命令行 --> <div v-for="item in commandArr"> <div class="command-action"> <!-- 執行成功或者失敗圖標切換 --> <i :class="['el-icon-right', 'command-action-icon', { 'error-icon': item.code !== 0 }]" ></i> <!-- 過往執行地址和命令行、信息 --> <span class="command-action-path">{{ item.path }} $</span> <span class="command-action-contenteditable" >{{ item.command }}</span > </div> <div class="output-command">{{ item.commandMsg }}</div> </div> <!-- 當前輸入的命令行 --> <div class="command-action command-action-editor" @mouseup="timeoutFocusInput" > <i class="el-icon-right command-action-icon"></i> <!-- 執行地址 --> <span class="command-action-path">{{ path }} $</span> <!-- 命令行輸入 --> <span :contenteditable="action ? false : 'plaintext-only'" class="command-action-contenteditable" @input="onDivInput($event)" @keydown="keyFn" ></span> </div> <!-- 當前命令行輸出 --> <div class="output-command"> <div v-for="item in commandMsg">{{item}}</div> </div> </div> </div> <script> const { ipcRenderer } = require('electron') const { spawn } = require('child_process') const path = require('path') var app = new Vue({ el: '#app', data: { path: '', // 命令行目錄 command: '', // 用戶輸入命令 handleCommand: '', // 通過處理的用戶命令 好比清除首尾空格、添加獲取路徑的命令 commandMsg: [], // 當前命令信息 commandArr: [], // 過往命令行輸出保存 isActive: true, // 終端是否聚焦 action: false, // 是否正在執行命令 inputDom: null, // 輸入框dom addPath: '', // 不一樣系統 獲取路徑的命令 mac是pwd window是chdir }, mounted() { this.addGetPath() this.inputDom = document.querySelector( '.command-action-contenteditable' ) this.path = process.cwd() // 初始化路徑 this.watchFocus() ipcRenderer.send('page-ready') // 告訴主進程頁面準備好了 }, methods: { // 回車執行命令 keyFn(e) { if (e.keyCode == 13) { this.actionCommand() e.preventDefault() } }, // 執行命令 actionCommand() { const command = this.command.trim() this.isClear(command) if (this.command === '') return this.action = true this.handleCommand = this.cdCommand(command) const ls = spawn(this.handleCommand, { encoding: 'utf8', cwd: this.path, // 執行命令路徑 shell: true, // 使用shell命令 }) // 監聽命令行執行過程的輸出 ls.stdout.on('data', (data) => { const value = data.toString().trim() this.commandMsg.push(value) console.log(`stdout: ${value}`) }) // 錯誤或詳細狀態進度報告 好比 git push、 git clone ls.stderr.on('data', (data) => { const value = data.toString().trim() this.commandMsg.push(`stderr: ${data}`) console.log(`stderr: ${data}`) }) // 子進程關閉事件 保存信息 更新狀態 ls.on('close', this.closeCommandAction) }, // 執行完畢 保存信息 更新狀態 closeCommandAction(code) { // 保存執行信息 this.commandArr.push({ code, // 是否執行成功 path: this.path, // 執行路徑 command: this.command, // 執行命令 commandMsg: this.commandMsg.join('\r'), // 執行信息 }) // 清空 this.updatePath(this.handleCommand, code) this.commandFinish() console.log( `子進程退出,退出碼 ${code}, 運行${code === 0 ? '成功' : '失敗'}` ) }, // cd命令處理 cdCommand(command) { let pathCommand = '' if (this.command.startsWith('cd ')) { pathCommand = this.addPath } else if (this.command.indexOf(' cd ') !== -1) { pathCommand = this.addPath } return command + pathCommand // 目錄自動聯想...等不少細節功能 能夠作但不必2 }, // 清空歷史 isClear(command) { if (command === 'clear') { this.commandArr = [] this.commandFinish() } }, // 獲取不一樣系統下的路徑 addGetPath() { const systemName = getOsInfo() if (systemName === 'Mac') { this.addPath = ' && pwd' } else if (systemName === 'Windows') { this.addPath = ' && chdir' } }, // 命令執行完畢 重置參數 commandFinish() { this.commandMsg = [] this.command = '' this.inputDom.textContent = '' this.action = false // 激活編輯器 this.$nextTick(() => { this.focusInput() this.scrollBottom() }) }, // 判斷命令是否添加過addPath updatePath(command, code) { if (code !== 0) return const isPathChange = command.indexOf(this.addPath) !== -1 if (isPathChange) { this.path = this.commandMsg[this.commandMsg.length - 1] } }, // 保存輸入的命令行 onDivInput(e) { this.command = e.target.textContent }, // 點擊div timeoutFocusInput() { setTimeout(() => { this.focusInput() }, 200) }, // 聚焦輸入 focusInput() { this.inputDom.focus() //解決ff不獲取焦點沒法定位問題 var range = window.getSelection() //建立range range.selectAllChildren(this.inputDom) //range 選擇obj下全部子內容 range.collapseToEnd() //光標移至最後 this.inputDom.focus() }, // 滾動到底部 scrollBottom() { let dom = document.querySelector('#app') dom.scrollTop = dom.scrollHeight // 滾動高度 dom = null }, // 監聽窗口聚焦、失焦 watchFocus() { ipcRenderer.on('win-focus', (event, message) => { this.isActive = message if (message) { this.focusInput() } }) }, }, }) // 獲取操做系統信息 function getOsInfo() { var userAgent = navigator.userAgent.toLowerCase() var name = 'Unknown' if (userAgent.indexOf('win') > -1) { name = 'Windows' } else if (userAgent.indexOf('iphone') > -1) { name = 'iPhone' } else if (userAgent.indexOf('mac') > -1) { name = 'Mac' } else if ( userAgent.indexOf('x11') > -1 || userAgent.indexOf('unix') > -1 || userAgent.indexOf('sunname') > -1 || userAgent.indexOf('bsd') > -1 ) { name = 'Unix' } else if (userAgent.indexOf('linux') > -1) { if (userAgent.indexOf('android') > -1) { name = 'Android' } else { name = 'Linux' } } return name } </script> </body> </html>
以上就是整個項目的代碼實現,總共只有三個文件。
更多細節
本項目終究是一個簡單的demo,若是想要作成一個完整的開源項目,還須要補充不少細節。
還會有各類各樣奇奇怪怪的需求和須要定製的地方,好比下面這些:
command+c
終止命令cd
目錄自動補全即便這個終端demo的代碼量不多,註釋足夠詳細,但仍是須要上手體驗一下一個Electron項目運行的細節。
clear命令演示
實際上就是將歷史命令行輸出的數組重置爲空數組。
執行失敗箭頭切換
根據子進程close
事件,判斷執行是否成功,切換一下圖標。
cd命令
識別cd
命令,根據系統添加獲取路徑(pwd
/chdir
)的命令,再將獲取到的路徑,更改成最終路徑。
giit提交代碼演示
開源地址: electron-terminal-demo
安裝
npm install
啓動
npm run start
命令行終端的實現原理就是這樣啦,強烈推薦各位下載體驗一下這個項目,最好單步調試一下,這樣會更熟悉Electron
。
項目idea誕生於咱們團隊開源的另外一個開源項目:electron-playground, 目的是爲了讓小夥伴學習electron
實戰項目。
electron-playground是用來幫助前端小夥伴們更好、更快的學習和理解前端桌面端技術Electron, 儘可能少走彎路。
它經過以下方式讓咱們快速學習electron。
前端進階積累、公衆號、GitHub、wx:OBkoro一、郵箱:obkoro1@foxmail.com
以上2021/01/12