web端分屏异显实现-Angular+Electron
要点:
主屏实现-Angular实现,部署在服务器
副屏实现-Angular+Electron,本地应用
主副屏通信
背景:
类似银行柜台操作的一个应用场景:银行人员在个人电脑上一界面操作,但需要同时对办理业务的客户展示相关信息,但要求只展示操作界面的部分信息,同时硬件设备只提供一台主机和两个显示器。
技术方案:
在上述背景下,开始想的是利用操作系统本身的显示器扩展能力,打开两个不同的浏览器标签放置在不同的显示器展示主副屏内容。详情参考这篇博客
后面考虑到用户的可用性和便于维护,决定使用exe程序进行副屏展示,所以在现有的技术储备上,选择了Electron实现exe,为了便于开发,最后选择把Electron集成到Angular项目中。
具体实现:
环境
Angular
Electron
node 10.11.0,
Chrome 69.0.3497.106
Electron 4.0.2.
步骤:
- 创建Angular项目,https://www.angular.cn/guide/quickstart
- 创建Electron项目,https://electronjs.org/docs/tutorial/first-app
- 把Electron集成到Angular项目中
- 实现各功能点
- 打包成exe
前两个不多说,官网上写的比较清楚,主要说下Electron集成和exe的打包。
集成 :
原因:降低开发门槛,提高开发效率,减小维护成本
过程描述:通过创建Electron项目,我们知道,Electron应用由一个主进程和多个渲染进程,主进程由main.js文件代表,index.html页面代表一个渲染进程。所以,Electron应用重要的两个文件就是main.js和index.html,其次是package.json它描述了一个Electron应用所需要的包和需要执行的基本命令。
实施:
编译angular项目,生成最终的index.html页面
拷贝main.js到angular项目目录,根据需要修改
在angular项目中的package.json添加所需包,脚本命令等
功能点:
- 副屏页面,运行在electron的渲染进程中
- websoket服务,与主屏进行双向通信,这里用到soket.io
- Electron主进程和渲染进程的通信,因为wesoket服务是运行在主进程中,那么要获取主屏传输过来的数据就必须实现主进程和渲染进程的通信。这里用到electron.ipcRenderer
打包:
使用electron-packager,详情见官网,本文中使用的命令:
最后,拣重点截下图和贴上部分源码
目录结构,dist:angular的编译目录,electron-app:打包后的exe目录,electron-src:electron主进程源文件目录,src:渲染进程源文件目录,main.js:electron启动文件
src/index.html,Electron在这里引入才能被识别
main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')
const io = require('socket.io')();
const { WebsoketServer } = require('./electron-src/websoket');
const { ipcMain } = require('electron');
//创建websoket服务
let ws = new WebsoketServer(io);
ws.listen(8090, (client) => {
client.on('req', function (data) {
console.log(data);
client.emit('resp', { data: '数据响应' });
});
})
//main process与render process通信
ipcMain.on('renderMsg', (event, arg) => {
event.sender.send('reply', '异步消息');
});
//同步消息
ipcMain.on('sync-msg', (event, arg) => {
event.returnValue = '同步消息';
})
// 保持对window对象的全局引用,如果不这么做的话,当JavaScript对象被
// 垃圾回收的时候,window对象将会自动的关闭
let win
function createWindow() {
// 创建浏览器窗口。
win = new BrowserWindow({ width: 800, height: 600 })
// 然后加载应用的 index.html。
win.loadURL(url.format({
pathname: path.join(__dirname, 'dist/index.html'),//编译后的index.html
protocol: 'file:',
slashes: true
}))
// 打开开发者工具
win.webContents.openDevTools()
// 当 window 被关闭,这个事件会被触发。
win.on('closed', () => {
// 取消引用 window 对象,如果你的应用支持多窗口的话,
// 通常会把多个 window 对象存放在一个数组里面,
// 与此同时,你应该删除相应的元素。
win = null
})
}
// Electron 会在初始化后并准备
// 创建浏览器窗口时,调用这个函数。
// 部分 API 在 ready 事件触发后才能使用。
app.on('ready', createWindow)
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持**。
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if (win === null) {
createWindow()
}
})
package.json
{
"name": "angular-base",
"version": "0.0.0",
"main": "main.js",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"compodoc": "compodoc -p src/tsconfig.app.json",
"electron": "ng build --prod && electron .",
"electron:build": "ng build --prod && electron-packager . app --win --out electron-app --arch=x64 --app-version 1.0.0 --overwrite",
"electron:build-x": "ng build --prod && electron-packager . app --win --out electron-app --arch=x64 --app-version 1.0.0 --overwrite --ignore=node_modules"
},
"private": true,
"dependencies": {
"@angular/animations": "~7.2.0",
"@angular/cdk": "^7.3.6",
"@angular/common": "~7.2.0",
"@angular/compiler": "~7.2.0",
"@angular/core": "~7.2.0",
"@angular/forms": "~7.2.0",
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"better-scroll": "^1.15.0",
"bootstrap": "^4.3.1",
"core-js": "^2.5.4",
"echarts": "^4.1.0",
"lodash": "^4.17.11",
"ngx-echarts": "^4.1.0",
"primeicons": "^1.0.0",
"primeng": "^7.0.5",
"rxjs": "~6.3.3",
"socket.io-client": "^2.2.0",
"socket.io": "^2.2.0",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.3",
"@angular/compiler-cli": "~7.2.0",
"@angular/language-service": "~7.2.0",
"@compodoc/compodoc": "^1.1.9",
"@types/better-scroll": "^1.12.1",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/lodash": "^4.14.123",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2",
"electron": "^4.0.2",
"electron-packager": "^13.0.1"
}
}
实现效果: