前言

Electron很闻名,很多人可能理解过,晓得它是用来开发桌面端的利用,然而始终没有在我的项目中实际过,不足练手的实际我的项目。

很多开源的命令行终端都是应用Electron来开发的,本文将从零开始手把手的教大家用Electron写一个命令行终端。

作为一个残缺的实战我的项目示例,该终端demo也将集成到Electron开源学习我的项目electron-playground中,目前这个我的项目领有800+ Star⭐️,它最大的特点是所见即所得的演示Electron的各种个性,帮忙大家疾速学习、上手Electron

大家跟着本文一起来试试Electron吧~

终端成果

开源地址: electron-terminal-demo

giit提交代码演示

目录

  1. 初始化我的项目。
  2. 我的项目目录构造
  3. Electron启动入口index-创立窗口
  4. 过程通信类-processMessage。
  5. 窗口html页面-命令行面板
  6. 命令行面板做了哪些事件

    • 外围办法:child_process.spawn-执行命令行监听命令行的输入
    • stderr不能间接辨认为命令行执行谬误
    • 命令行终端执行命令保留输入信息的外围代码
    • html残缺代码
    • 命令行终端的更多细节
  7. 下载试玩

    • 我的项目演示
    • 我的项目地址
    • 启动与调试
  8. 小结

初始化我的项目

npm initnpm install electron -D

如果Electron装置不下来,须要增加一个.npmrc文件,来批改Electron的装置地址,文件内容如下:

registry=https://registry.npm.taobao.org/electron_mirror=https://npm.taobao.org/mirrors/electron/chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver

批改一下package.json的入口mainscripts选项, 当初package.json长这样,很简洁:

{  "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

Electron启动入口index-创立窗口

  1. 创立窗口, 赋予窗口间接应用node的能力。
  2. 窗口加载本地html页面
  3. 加载主线程和渲染过程通信逻辑
// ./src/index.jsconst { 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)

过程通信类-processMessage

electron分为主过程和渲染过程,因为过程不同,在各种事件产生的对应机会须要互相告诉来执行一些性能。

这个类就是用于它们之间的通信的,electron通信这部分封装的很简洁了,照着用就能够了。

// ./src/processMessage.jsconst { 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

窗口html页面-命令行面板

在创立窗口的时候,咱们赋予了窗口应用node的能力, 能够在html中间接应用node模块。

所以咱们不须要通过过程通信的形式来执行命令和渲染输入,能够间接在一个文件外面实现。

终端的外围在于执行命令,渲染命令行输入,保留命令行的输入

这些都在这个文件外面实现了,代码行数不到250行。

命令行面板做了哪些事件

  • 页面: 引入vue、element,css文件来解决页面
  • template模板-渲染以后命令行执行的输入以及历史命令行的执行输入
  • 外围:执行命令监听命令行输入

    • 执行命令并监听执行命令的输入,同步渲染输入。
    • 执行结束,保留命令行输入的信息。
    • 渲染历史命令行输入。
    • 对一些命令进行非凡解决,比方上面的细节解决。
  • 围绕执行命令行的细节解决

    • 辨认cd,依据零碎保留cd门路
    • 辨认clear清空所有输入。
    • 执行胜利与失败的箭头图标展现。
    • 聚焦窗口,聚焦输出。
    • 命令执行结束滚动底部。
    • 等等细节。

外围办法:child_process.spawn-执行命令行监听命令行的输入

child_process.spawn介绍

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不能间接辨认为命令行执行谬误

stderr尽管是规范谬误输入,但外面的信息不全是谬误的信息,不同的工具会有不同的解决。

对于git来说,有很多命令行操作的输入信息都输入在stederr上。

比方git clonegit 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 pushstderrMsgHandle(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残缺代码

这里是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目录主动补全
  • 命令保留高低键滑动
  • git等罕用性能独自非凡解决。
  • 输入信息色彩变动
  • 等等

下载试玩

即便这个终端demo的代码量很少,正文足够具体,但还是须要上手体验一下一个Electron我的项目运行的细节。

我的项目演示

clear命令演示

实际上就是将历史命令行输入的数组重置为空数组。

执行失败箭头切换

依据子过程close事件,判断执行是否胜利,切换一下图标。

cd命令

辨认cd命令,依据零碎增加获取门路(pwd/chdir)的命令,再将获取到的门路,更改为最终门路。

giit提交代码演示

我的项目地址

开源地址: electron-terminal-demo

启动与调试

装置

npm install

启动

  1. 通过vscode的调试运行我的项目,这种模式能够间接在VSCode中进行debugger调试。

  2. 如果不是应用vscode编辑器, 也能够通过应用命令行启动。
npm run start

小结

命令行终端的实现原理就是这样啦,强烈推荐各位下载体验一下这个我的项目,最好单步调试一下,这样会更相熟Electron

我的项目idea诞生于咱们团队开源的另一个开源我的项目:electron-playground, 目标是为了让小伙伴学习electron实战我的项目。

electron-playground是用来帮忙前端小伙伴们更好、更快的学习和了解前端桌面端技术Electron, 尽量少走弯路。

它通过如下形式让咱们疾速学习electron。

  1. 带有gif示例和可操作的demo的教程文章。
  2. 系统性的整顿了Electron相干的api和性能。
  3. 搭配演练场,本人入手尝试electron的各种个性。

前端进阶积攒、公众号、GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2021/01/12