(防盗镇楼)本文地址:https://blog.csdn.net/cbaili/article/details/128651549
前言
最近在撸VUE,想要实现一份代码既能构建Web又能构建Electron应用
并且能够判断环境是浏览器还是Electron,随后在Electron中做一些特定的事情
以往的Electron通信依靠IPC通信完成,但是发布到Web端运行会报错找不到__dirname
这是因为网页浏览器与Electron不同,Electron集成了Node环境,所以Electron能够访问到.
苦寻良久,终于在一个帖子上发现了解决方案:网页与Electron通信
解决方案
这个方案利用Electron的自定义协议实现.
在前端项目中无需引用任何Electron库即可实现与Electron通信,过程可控,完美!
什么是自定义协议?
标准协议就是http https这类的,自定义协议顾名思义就是允许你定义一个协议,自己来实现通信数据处理过程。
你大概率见到过一些网页唤醒本机应用程序的案例,比如百度网盘、Steam等.它们都是利用了这个机制实现。
你可以自定义一个myapp的协议,通过myAPP://lala/?data=123这样的url访问资源
但我们需要的不同,我们不希望去唤醒应用程序,而是直接让Electron拦截掉这个请求。
来看看API
Electron为我们提供了protocol类,它用于注册自定义协议和拦截自定义请求并允许自定义数据处理过程.
这东西用起来很简单,看代码案例:
//electron项目的index.js
const URL = require('url');
const { app, BrowserWindow, protocol } = require('electron')
const MY_CUSTOM_PROTOCOL_SCHEMA="myapp";//命名需要遵循URL PROTOCOL协议
//#第一步:注册自定义协议,必须在app ready之前完成,且只能调用一次
protocol.registerSchemesAsPrivileged([ { scheme: MY_CUSTOM_PROTOCOL_SCHEMA}])
app.on('ready', () => {
win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL('http://127.0.0.1:5173/#/')
if (!process.env.IS_TEST) win.webContents.openDevTools();
//#第二步:定义刚刚注册的自定义协议返回数据类型
//protocol.registerBufferProtocol
//protocol.registerFileProtocol
//protocol.registerHttpProtocol
//protocol.registerStreamProtocol
//protocol.registerStringProtocol
//这里以返回文本为例
protocol.registerStringProtocol(MY_CUSTOM_PROTOCOL_SCHEMA, (req, res) => {
//我们可以使用node.js的url模块对地址进行解析
//参考:https://cloud.tencent.com/developer/article/1653911
let url = URL.parse(req.url, true)
console.log("myapp:// Requested", req, res,url);
//do something it here..
res("lalala")//返回给网页的数据,你可以做成JSON返回
})
})
//前端js调用部分
callMyapp("asd/ccc?eee=123")
function callMyapp(path){
const MY_CUSTOM_PROTOCOL_SCHEMA="myapp"
const url=`${MY_CUSTOM_PROTOCOL_SCHEMA}://${path}`
//构建get请求(axios不支持自定义协议会报错)
var httpRequest = new XMLHttpRequest();
httpRequest.open('GET', url, true);
httpRequest.send();
httpRequest.onreadystatechange = () => {
if (httpRequest.readyState == 4 && httpRequest.status == 200) {
console.log(httpRequest.responseText);
}
};
}
以上就实现了简单的通信请求,我们可以通过在Electron中判断请求URL做一些事情了.
还有,URL最大支持2083个字符,所以你要传递的数据文本长度不可以超过这个数字…
Electron持续发送数据到Web(SSE技术)
上面我们实现了HTML->Electron的数据发送,但有时我们还需要将Electron中产生的事件发送到HTML中。
总归来讲大概有三种方式:ajax轮训 、Socket通信、SSE服务器推送事件(Server-sent Events).
前两者不说了,前者不友好后者太鸡肋。我们一起来看看这个SSE:
SSE使用数据流传输数据
这是一种服务器向网页单向持续传输数据的机制
当网页向服务器发送一个请求时,服务器返回的是一个数据流,这个流会让会话一直保持连接.
这常见于文件下载、视频传输等场景.
因此我们可以创建一个自定流,然后随时随地的 往里面写各种数据.
遗憾的是Edge/IE没有这个服务的实现,但有第三方库eventsource使用纯JS实现了这个方法.
不过此文我们用到SSE的地方是在Electron环境里呀,它可是Chrome内核的所以放心搞就好了,浏览器环境中运行不到这部分代码所以不会报错.
上代码:
//Electron端
import STREAM from 'stream';
const { app, BrowserWindow, protocol,ProtocolResponse } = require('electron')
const MY_CUSTOM_PROTOCOL_SCHEMA="myapp-sse";//命名需要遵循URL PROTOCOL协议
//#第一步:注册自定义协议,必须在app ready之前完成,且只能调用一次
protocol.registerSchemesAsPrivileged([ { scheme: MY_CUSTOM_PROTOCOL_SCHEMA}])
app.on('ready', () => {
//#第二步:注册一个流自定义协议
protocol.registerStreamProtocol(MY_CUSTOM_PROTOCOL_SCHEMA, (req, res) => {
const stream = new STREAM.PassThrough()
stream.on("close", () => {/*网页关闭或链接中断时触发*/stop() })
stream.on("error", (err) => {/*异常时触发*/ stop() })
//#第三步:构造HTTP流协议响应数据并响应请求
res({
statusCode: 200,
mimeType: "text/event-stream",
headers: {
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
},
data: stream,// PassThrough 也是一个 ReadableStream,可以设置给data
})
//#第四步:持续向流中写入数据
const timer = setInterval(() => { send("传递的数据", "message") }, 1000)// 每秒发送一次数据
//封装一些方法
let msgID = 0//消息ID,每次发送新数据时自增1
let _timeout = 3000//链接超时,如果超过这个时间没有数据传输,浏览器则会自动断开链接,所以我们需要定时发送一个数据来保持链接
function stop() { clearInterval(timer) }//当网页关闭或链接中断时停止发送数据
function send(data, event, id = undefined) {
//构造SSE响应数据
//它有一个标准的格式,每一行都是以\n结尾,每一行的格式为: key: value\n
if (id == undefined) msgID++;
let ret = `data: ${data}\n`//传递的数据,可以是任意类型,但是必须是字符串,如果是对象,则需要JSON.stringify()转换为字符串
ret += `event: ${event}\n` //事件类型:默认为message,可以自定义
ret += `id: ${id || msgID}\n` //消息ID:每次发送新数据都应该自增,用于浏览器判断是否有新数据和数据重发
ret += `retry: ${_timeout}\n`//重试时间
ret += `\n`//数据的结尾必须是两个\n
stream.push(ret)//将数据写入流,随后浏览器就会收到数据
}
})
})
//网页端部分代码
//Electron环境判断
const isElectron = /electron/i.test(navigator.userAgent);
//有了环境判断以后我们就可以避免在浏览器中执行SSE请求了
if (isElectron && typeof (EventSource) !== 'undefined') {
//构造请求地址
const MY_CUSTOM_PROTOCOL_SCHEMA="myapp-sse"
const type = "aa"
const parm = { dd: "ff" }
const event = "ccc"
const url = `${MY_CUSTOM_PROTOCOL_SCHEMA}://${type}/?event=${event}&parm=${btoa(JSON.stringify(parm))}`
//构造EventSource(浏览器SSE请求)
const evtSource = new EventSource(url, { withCredentials: true }) // 后端接口,要配置允许跨域属性
// 与事件源的连接刚打开时触发
evtSource.onopen = function (e) {
console.log("sse onopen", e);
}
// 当从事件源接收到数据时触发
evtSource.onmessage = function (e) {
console.log("sse onmessage", e);
}
// 与事件源的连接无法打开时触发
evtSource.onerror = (e: Event) => {
console.log("sse onerror", e);
evtSource.close(); // 关闭连接
}
//Electron代码中send函数内自定义的Event
evtSource.on("自定义Event", (e: Event) => {
console.log("sse custom event", e);
})
} else {
console.log("sse not support", '当前浏览器不支持使用EventSource接收服务器推送事件!');
}
上面的代码可能需要花费一些时间才会理解,阅读下列文章可以帮助你快速整明白
- 除了 Websocket ,服务端还有什么办法能向浏览器主动推送信息
- SSE(Server Sent Events) HTTP服务端推送详解
- SSE技术详解:使用 HTTP 做服务端数据推送应用的技术
由此,我们完成了Electron端向Web端持续发送消息的机制,这特别适合一些事件的即时响应.
小拓展
判断网页是否运行在自己的APP中
上面的前端代码实现了判断是否运行在Electron中
但运行的Electron不一定是我们自己的app,它内部没有我们注册的自定义协议,这样会导致调用请求的时候报错
所以我们还需要更精准的判断:
我们可以通过查看输出navigator.userAgent,发现其中有一段包含了我们app的名字和版本号
所以,我们可以通过修改上面的判断条件实现
const isRunInApp = /YOUR_APP_NAME/i.test(navigator.userAgent)
安全考虑
这个方案显然还有一些安全上的问题需要处理,比如:
- 接口来源认证:不能说随便一个网站用了这个方法就都能调用我们APP的接口
- 暴力请求过滤:防止系统资源耗尽
- 加密敏感数据:如非必要不要传输这些个啥
- 防注入等等…
所以我们尽量不要在接口中进行一些高危操作,如果要有必须要加验证机制.
最终
有了以上两部分内容,我们可以针对网站运行在我们自己的客户端情况下,做出一些特殊功能支持,比如一些情景:
- 模拟窗口:在网页浏览器中不显示标题栏按钮 但在Electron中显示并能够让Election响应最大化最小化关闭等按钮事件
- 绕过一些安全限制:可以从Elecron访问文件系统、操作硬件之类的(注意安全)
- 为网站提供一些本地化信息
话说,这个文章标题真难起…
好用别忘了一按三联哦~