基于Electron+React+TS实现一个桌面翻译工具

2,344 阅读6分钟

基于Electron+React+TS实现一个桌面翻译工具

  • 好久没有用react写东西了,这几天看electron的官方文档学习使用electron,于是决定结合这两者开发一个简单的桌面翻译工具,以下是自己的开发总结。
  • 给这个翻译器命名为BaiT
  • 主要功能:
    • 监听输入框变化自动翻译
    • 选择翻译的语言,支持自动识别
    • 划词翻译(划词这个功能需要监听pc的鼠标选取事件,但是我在官网没找到对应的api😓,最后改为了监听剪切板内容的改变来自动翻译)
    • 支持快捷键唤醒和关闭
    • 支持最小化到系统托盘

起步

  1. 使用基于vite的react+electron脚手架:electron-vite-react来搭建项目
  2. 在终端输入命令npm create electron-vite来创建一个项目,框架选择react
  3. 创建好了之后运行npm i来安装依赖
  4. 最后运行npm run dev来打开项目

image-20220705104850471.png

  1. 申请百度翻译api的使用权限(不知道什么原因,我居然能申请高级版的使用权限🙃):百度翻译api通用翻译文档

翻译器组件设计

界面设计

  1. 需要一个导航栏Nav(本来是还想做单词查询的,后来发现百度翻译的词典api需要尊享版权限才可以使用😅)

  2. 需要一个语言类型选择组件,组件里面包括语言的选择,翻译按钮,划词翻译开关

  3. 两个输入文本框,一个用来输入要翻译的语言,另一个输出翻译结果

    静态页面展示:

image-20220705105454245.png

组件实现

  • 毕竟是一个自用的工具,所以没必要添加组件库,自己使用css来实现绘制了这些组件,为这些组件添加一些hover动画。

  • 组件之间的通信也是同样,没有引入状态管理库,仅使用prop来实现父子组件通信,两个子组件中则使用了自己实现的发布订阅模块来实现通信

  • 组件层次:

image-20220705110406645.png

  • 相信会使用react的同学,快速搭建一个这样的界面应该不是很难。难的是如何优雅地处理组件之间的通信,这个项目中的每一个组件几乎都要跟它的父组件和兄弟组件通信,一种很好的办法是将通信的功能统一交给父组件帮忙转发,这样每个子组件可以更专注于它的主要功能;还有一种就是使用发布订阅模式来进行通信,这样做的虽然可以偷懒,但是代码维护起来比较难,子组件的中写的代码相对臃肿。

渲染端与electron主进程通信

  1. 主进程与渲染端通信一般有三种办法:官网示例
    • 单向的渲染端到主进程通信:
      • 渲染端使用ipcRenderer.send(channel, ...args)来发送事件和传递参数
      • 主进程监听这个channel:mainwin.on(channel,listener),接收到渲染端的事件后运行listener这个回调
    • 双向的渲染端到主进程通信:
      • 与前者类似,渲染端触发事件,主进程响应,但是可以将响应回调函数的返回值返回给渲染端
      • 渲染端使用ipcRenderer.invoke(channel, ...args)来与主进程通信
      • 而主进程则使用ipcMain.handle(channel, listener)来监听,listener这个回调函数会返回一个promise,promise的结果就是所需要的返回值
    • 主进程到渲染端的通信
      • 与单向的渲染端到主进程类似,主进程使用mainwin.webContents.send(channel, ...args)向渲染端发送消息
      • 渲染端通过ipcRenderer.on(channel, listener)来接收并相应消息
  2. 翻译功能的实现
  • 为了避开浏览器的同源策略的限制,可以让主进程来请求翻译接口然后将请求结果返回给渲染端:

    渲染端请求翻译api:将get-translate事件发送给主进程

    const getTranslation = async(translatedLang:string,from:string,to:string)=>{
    
        if(translatedLang==="") return
        // 移除换行符
        translatedLang =translatedLang.replace(/[\r\n]/g,"");        
        const q = translatedLang,
        appid = "XXXXX",//百度翻译的appid
        salt = "XXXXX", //生成的随机数
        sign = getSign(appid+q+salt+key) //使用md5加密库加密的sing签名,key是向百度翻译申请的密钥
        const result = await ipcRenderer.invoke('get-translate',JSON.stringify({
            q,from,to,appid,salt,sign
        }))
        //获取到返回结果后发布'translated'事件,通知订阅者展示翻译结果
        pubsub.emit('translated',result.trans_result[0].dst)
    }
    //获取md5签名
    function getSign(str:string){
        return md5(str)
    }
    

​ 主进程订阅get-translate事件:


  ipcMain.handle('get-translate',async (channel, listener)=>{
      //解析字符串
    listener = JSON.parse(listener);
    try {
        //发起网络请求
        const axiosPromise =  await axios({
          url:"https://fanyi-api.baidu.com/api/trans/vip/translate",
          method:"get",
          params:{
            ...listener
          }
      })   
      //返回数据
      return axiosPromise.data 
    } catch (error) {
      return error
    } 
  })

对文本输入框的onChange函数做防抖

系统通过监听文本输入框的变化来自动提交输入框内的文本,获取对应的翻译结果。为了避免发送大量的毫无意义的请求浪费资源(当然还有一个原因是我怕百度翻译的服务器把我给封了😅),为onChange函数做防抖是很有必要的。

复习时间!防抖函数的实现:

export default function debounce(func:Function,delay=500,immediate=true){
    let timer:NodeJS.Timeout|null;
    return function(this:any,...args:Array<any>){
        const context = this;
        if(timer) clearTimeout(timer);
        if(immediate){
            if(!timer) func.apply(context,args);
            timer = setTimeout(()=>{
                timer = null;
            },delay)
        }else{
            timer = setTimeout(() => {
                func.apply(context,args)
            }, delay);
        }
    }
}

监听剪切板变化

  1. electron中使用clipboard模块可以操作剪切板:clipboard
  2. 开启一个定时器,定时比较当前剪切板的内容和之前保存的内容是否一致,如果不一致则触发剪切板更新事件
  3. 使用主进程到渲染端通信的方法来将这个剪切板更新事件和更新内容发送到渲染端

在electron中监听剪切板的变化这篇文章使用了一个类来封装监听剪切板的内容,将剪切板监听的逻辑单独抽离了出来,我照着这位大佬的方法对自己原来的方法做了修改

最小化到系统托盘

使用electron中的Tray模块可以让窗口隐藏到系统托盘

  //* 定义系统托盘
  // 设置托盘
  tray = new Tray(join(ROOT_PATH.public, 'BaiT.png'));
  tray.setToolTip('BaiT');
  // 定义托盘目录,可以右键点击触发
  const contextMene = Menu.buildFromTemplate([
    {
      label:"退出",
      click:()=>{win?.destroy()}
    }
  ])
  // 定义托盘事件,双击显示和关闭窗口
  tray.on('double-click',()=>{
      if(!win?.isVisible()){
        win?.show();
      }else{
        win.hide()
      }
  })
  tray.setContextMenu(contextMene);

打包

项目完成后,运行npm run build将项目打包,会生成一个release文件夹,其中包括项目的.exe运行文件和安装文件。