现在写个前端谁还不用个构建工具,每天早晨回去,找对应的项目目录,打开命令行工具,敲个
npm run xxx
,重复得有点无聊。特别面对着日渐增长的项目数量,好希望有个工具可以帮我管理所有的项目,两手抓,一手起项目,一手抓我的叉烧包。emmmm...
界面功能介绍
项目添加:通过拖拽项目package.json
文件到应用面板完成解析(不是.json文件?你试试)

项目管理:红色区域展示添加的项目,支持切换/删除/重命名;黄色区域展示package.json
中的scripts
脚本,点击即可执行;绿色区域管理该项目下的多个命令行窗口,支持增加/删除/切换;蓝色区域为命令行执行区域。

一次过满足您三个愿望:
- 小型的Teminal客户端,多tab切换,按项目分组管理
- 支持持久化存储,重新打开应用,基本的项目信息可复原、不丢失
- 开发/打包不用敲命令行了,轻轻一点击即可完成
高颜值,颜值即正义(不接受反驳 !!!)
技术栈
- 运行环境:
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-start
package.json目录的main
字段指定electron应用的主渲染进程文件,除了该js文件以外,其他的都属于在渲染进程运行。
"main": "electron/index.js"
在electron/index.js中,配置electron加载的文件——开发环境下加载开发服务器文件,生成环境下加载本地文件。
isDev
? mainWindow.loadURL('http://localhost:3000/index.html')
: mainWindow.loadFile(path.join(__dirname, '../react/build/index.html'))
react —— create-react-app
-
把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
-
componentDidUpdate
应用窗口resize后,web终端模拟器都要重新适配父元素的大小。在react中涉及dom更新后需要处理的逻辑(放在callback函数里),一是使用
this.setState({}, callback)
;二是在componentDidUpdate
里处理。前者特别方便快捷,更新行为跟数据源绑定到一起,类似于vue
的vm.$nextTick
。后者就要麻烦很多了(谁叫你把状态放在全局中处理呢),要特别设定一个isNew
变量来决定dom的更新回调是否执行。 -
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
-
异步dispatch action
可使用redux-thunk/redux-saga,由于nodejs环境原生支持文件同步读取
fs.readFileSync
,所以以下两种方法均可以。


-
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
-
redux-persist
- redux-persist也是使用的webStorage(localStorage/sessionStorage),只支持ES5的数据类型,因此需要对我们的store数据做过滤,只留下项目基本信息的字段。
- 官方文档也是够坑了,没有讲需要自己动手改造我们的reducer。最后google个天荒地老才在issue里发现宝藏刷新后数据无法恢复
node-pty + xterm.js
-
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
- nodejs和webpack的模块管理冲突
webpack继续使用import/require,node模块的引入使用
window.require
,就可以逃过webpack的编译

-
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调试信息一样展示在控制台中
-
-
主进程和渲染进程的通信
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) })
-
原生desktop app菜单
让换肤/toggle控制台/刷新程序等app功能常驻于程序菜单项里
打包发布electron-react项目
-
打包。工具使用electron-builder,确保系统环境
一定要使用nodev8版本
一定要使用nodev8版本
一定要使用nodev8版本
,曾经使用了v10
,把网上几乎所有的demo项目都运行过了一遍,发现都在打包过程中出错,绝望地死磕了3、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缓存目录 - react打包。文件引用使用相对路径——在
-
发布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
,谢谢鼓励