共计 10531 个字符,预计需要花费 27 分钟才能阅读完成。
前言
Electron
很闻名,很多人可能理解过,晓得它是用来开发桌面端的利用,然而始终没有在我的项目中实际过,不足练手的实际我的项目。
很多开源的命令行终端都是应用 Electron
来开发的,本文将从零开始手把手的教大家用 Electron
写一个命令行终端。
作为一个残缺的实战我的项目示例,该终端 demo 也将集成到 Electron
开源学习我的项目 electron-playground 中,目前这个我的项目领有 800+ Star⭐️,它最大的特点是所见即所得的演示 Electron
的各种个性,帮忙大家疾速学习、上手Electron
。
大家跟着本文一起来试试 Electron 吧~
终端成果
开源地址: electron-terminal-demo
giit 提交代码演示
目录
- 初始化我的项目。
- 我的项目目录构造
- Electron 启动入口 index- 创立窗口
- 过程通信类 -processMessage。
- 窗口 html 页面 - 命令行面板
-
命令行面板做了哪些事件
- 外围办法:child_process.spawn- 执行命令行监听命令行的输入
- stderr 不能间接辨认为命令行执行谬误
- 命令行终端执行命令保留输入信息的外围代码
- html 残缺代码
- 命令行终端的更多细节
-
下载试玩
- 我的项目演示
- 我的项目地址
- 启动与调试
- 小结
初始化我的项目
npm init | |
npm 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
的入口 main
和scripts
选项, 当初 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- 创立窗口
- 创立窗口, 赋予窗口间接应用 node 的能力。
- 窗口加载本地 html 页面
- 加载主线程和渲染过程通信逻辑
// ./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) |
过程通信类 -processMessage
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 |
窗口 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 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 残缺代码
这里是 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
启动
- 通过 vscode 的调试运行我的项目,这种模式能够间接在 VSCode 中进行 debugger 调试。
- 如果不是应用 vscode 编辑器, 也能够通过应用命令行启动。
npm run start
小结
命令行终端的实现原理就是这样啦,强烈推荐各位下载体验一下这个我的项目,最好单步调试一下,这样会更相熟Electron
。
我的项目 idea 诞生于咱们团队开源的另一个开源我的项目:electron-playground, 目标是为了让小伙伴学习 electron
实战我的项目。
electron-playground 是用来帮忙前端小伙伴们更好、更快的学习和了解前端桌面端技术 Electron, 尽量少走弯路。
它通过如下形式让咱们疾速学习 electron。
- 带有 gif 示例和可操作的 demo 的教程文章。
- 系统性的整顿了 Electron 相干的 api 和性能。
- 搭配演练场,本人入手尝试 electron 的各种个性。
前端进阶积攒、公众号、GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com
以上 2021/01/12