资讯专栏INFORMATION COLUMN

实现electron-bridge

Hujiawei / 452人阅读

electron-bridge

github链接 求star

Motivition

如果想一套代码同时能跑在web环境和electron环境中,就需要在代码中先判断环境,再分别写对应的逻辑。每次写到electron环境下的逻辑,又要区分渲染进程和主进程,因为有些事只能渲染进程做,有些事只能主进程做。所以,我希望能将这些抽象出来,某个方法,只能在electron环境下被调用,并且不需要关心在什么进程下,web只要判断环境,调不同的方法就行,不需要关心和electron的交互。

如果,我需要快速的开启另一个electron的项目,我希望我web里的代码能轻易的获取到electron的能力,而不是重新开始编写,这个时候,我希望有一层对electron能力的封装。

团队内有些成员对web很熟悉,但是对electron不是很了解,如果加入项目,就需要去学习electron的知识,这个时候,如果能有一个库列出了所有electron能做的事,你只需要调用,无需关心它是怎么实现的,能很大程度提高开发效率。

Goals

给web注入适当的环境变量,让web知道自己的环境

给web注入一个对象,包含所有electron能做的事(包括主进程、渲染进程)

How to do

在load web页面的时候,有个webPreferences配置,我们在这里预加载一个js文件,就是electron-bridge.js

这个文件拥有node的能力,并且它是属于渲染进程的,所以它能做渲染进程里的事, 也能跟主进程通讯。

st=>start: start
op0=>operation: index.js去调用bridge.js暴露出来的方法, ElectronBridge.setFullScreen()
op1=>operation: bridge.js通过ipcRender告诉ipacMain做什么,并把回调暂存起来
op2=>operation:  主进程做完告诉bridge.js做完了,发送数据
op4=>operation:  bridge.js带上收到的数据,执行暂存的回调
op3=>operation:  bridge.js直接做完,触发回调
cond=>condition: bridge.js判断是不是主进程做的事?
e=>end: end

st->op0->cond
cond(yes)->op1->op2->op4->e
cond(no)->op3->e
Let"s do it 给web注入适当的环境变量

加载bridge.js

win = new BrowserWindow({
  width: 800,
  height: 600,
  show: false,
  webPreferences: {
    preload: path.join(__dirname, "../bridge/bridge.js"),
    plugins: true
  }
});

当我们启动electron的时候,主进程开始通知这个渲染进程,给渲染进程注入主进程的环境变量,再有渲染进程挂载到window对象上,这样web就能获取自己的环境信息

//bridge.js

const {ipcRenderer} = require("electron");

//监听主进程,设置环境变量
ipcRenderer.on("set-env", (event, msg) => {
  for (const key in msg) {
    window[key] = msg[key];
  }
});
//main.js
const {BrowserWindow, ipcMain} = require("electron");

const win = new BrowserWindow({...});

//获取创建好的window对象发送消息
win.webContents.on("did-finish-load", function() {
  win.webContents.send("set-env", { //设置web环境变量
    __ELECTRON__: true,
    __DEV__: true,
    __PRO__: false,
    __SERVER__: false,
    windowLoaded: true
  });
});
通过bridge.js 来调用主进程的方法

我们通过ipcRender给主进程发送一系列消息,包括做什么事情(eventName), 根据哪些参数(params),对外根据不同的事件暴露不同的方法,接受参数,和回调函数。

先将回调函数放在 eventsMap上暂存起来,因为ipcRender不能发送函数,所有的信息会被序列化后再发送给主进程,所以,我们先生成一个时间戳,让 eventsMap[时间戳] = cb 并把时间戳一同发送过去,等一会儿,主进程通知渲染进程调用哪个时间戳函数

通过"resist-event"频道, 发送参数,包括 eventName、params、timeStamp

//bridge.js
const {ipcRenderer} = require("electron");

const eventsMap = {};

//调用原生事件
function registEvent(eventName, params, cb) {
  //允许只传两个数据
  if (!cb) {
    cb = params;
    params = {};
  }

  //如果win还未ready
  if (!windowLoaded) {
    cb(new Error("window not ready"));
    return;
  }

  const stamp = String(new Date().getTime());
  const opts = Object.assign({eventName}, params, {stamp});
  eventsMap[stamp] = cb; //注册唯一函数
  ipcRenderer.send("regist-event", opts); //发送事件
}

//进入全屏
function setFullScreen(cb) {
  registEvent(SET_FULL_SCREEN, cb);
}

window.ElectronBridge = {
  setFullScreen
};

主进程监听‘resist-event’频道,做对应的事。我们会将所有主进程能做的事,放在eventsList对象下,当接受到渲染进程的通知,去eventsList找有没有对应的事能做,有,做完通过promise,或者通过回调函数,去在‘fire-event’频道通知,渲染进程,事情已经做完,并把数据传回去,包括 stamp(之前渲染进程传过来的,现在传回去,告诉渲染进程执行哪个回调函数) 、 payload(返回数据) 、err (错误信息)

//main.js
const {ipcMain} = require("electron");

//监听对原生的调用
ipcMain.on("regist-event", (event, arg) => {
  const nativeEvent = eventsList[arg.eventName];
  if (nativeEvent) {
    const result =  nativeEvent(app, win, arg.params);
    if (isPromise(result)) {
      result.then(res => {
        event.sender.send("fire-event", {
          stamp: arg.stamp,
          payload: res
        });
      }).catch(err => {
        event.sender.send("fire-event", {
          stamp: arg.stamp,
          err
        });
      });
    } else {
      event.sender.send("fire-event", {
        stamp: arg.stamp,
        payload: result
      });
    }
  } else {
    event.sender.send("fire-event", {
      stamp: arg.stamp,
      err: new Error("event not support")
    });
  }
});

渲染进程监听‘fire-event’执行对应时间戳回调函数,并把主进程传过来的数据传给回调函数。触发完成后,删掉该回调函数。

//bridge.js

//触发事件回调
ipcRenderer.on("fire-event", (event, arg) => {
  const cb = eventsMap[arg.stamp];
  if (cb) {
    if (arg.err) {
      cb(arg.err, arg.payload);
    } else {
      cb(false, arg.payload);
    }
    delete eventsMap[arg.stamp];
  }
});

如果是渲染进程能做的事,就不需要再和主进程通讯,可以直接完成触发回调

//bridge.js
const {webFrame} = require("electron");
//设置缩放比,只能在渲染进程中实现
function setZoomFactor(params, cb) {
  webFrame.setZoomFactor(params);
  cb && cb();
}

window.ElectronBridge = {
  setZoomFactor
};

最终web中的js代码去调用bridge.js暴露出来的方法

// ../web/index.js

$btn1.addEventListener("click", function() {
  if (__ELECTRON__ && ElectronBridge) { //electron 环境
    ElectronBridge.setFullScreen((err) => {
      if (err) return;
      console.log("done");
    });
  } else { //web 环境
    alert("不能设置全屏")
    //do something else
  }
});

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83226.html

相关文章

  • Dubbo SPI机制和IOC

    摘要:要构建自适应实例,先要有自适应的实现类,实现类有两种方式一种通过配置文件,一种是通过是字节码的方式动态生成。 SPI机制 SPI,即(service provider interface)机制,有很多组件的实现,如日志、数据库访问等都是采用这样的方式,一般通用组件为了提升可扩展性,基于接口编程,将操作接口形成标准规范,但是可以开放多种扩展实现,这种做法也符合开闭设计原则,使组件具有可插...

    Scorpion 评论0 收藏0
  • 练习项目备选清单

    摘要:练习项目备选清单文件下载器功能概要设计实现新建下载功能以为基础给出下载链接可以启动下载任务实现局域网内下载传输文件以单线程下载方式实现附加功能支持断点续传实现多线程下载实现下载参考技术套接字编程多线程编程音视频播放器功能概要设计实现播放常见 练习项目备选清单 Utilities 1. 文件下载器 功能概要设计: 实现新建下载功能(以ftp为基础) 给出下载链接可以启动下载任务 实现局...

    guyan0319 评论0 收藏0
  • 练习项目备选清单

    摘要:练习项目备选清单文件下载器功能概要设计实现新建下载功能以为基础给出下载链接可以启动下载任务实现局域网内下载传输文件以单线程下载方式实现附加功能支持断点续传实现多线程下载实现下载参考技术套接字编程多线程编程音视频播放器功能概要设计实现播放常见 练习项目备选清单 Utilities 1. 文件下载器 功能概要设计: 实现新建下载功能(以ftp为基础) 给出下载链接可以启动下载任务 实现局...

    peixn 评论0 收藏0
  • 前端路由原理解析和实现

    摘要:如何实现前端路由要实现前端路由,需要解决两个核心如何改变却不引起页面刷新如何检测变化了下面分别使用和两种实现方式回答上面的两个核心问题。 原文链接:github.com/whinc/blog/… 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本...

    lavor 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<