electron应用功能——外部URL启动

3,358 阅读5分钟

简介

本文一步步带你实现外链唤起electron应用的功能,并向你介绍其相关原理和实现过程中容易出现的坑。移动端的网页调起手机app应用的功能大家可能接触的多一些,其实pc端网页调起pc应用的功能从原理上面和移动端是一样的。主要原理就是使用系统提供的API注册一个自定义的协议,当你注册一个协议到系统协议列表中后,浏览器或者其他应用在打开你自定义的协议链接时会去系统协议列表中检测,检测到后就会唤起协议的默认处理程序(我们的应用)。

1.URL Scheme协议

不同的系统对于Scheme协议的配置方式是不一样的,mac系统是配置应用的info.plist中,每一个打包的app应用都有自己的plist文件。想要查看app的配置可以打开对用应用的plist文件查看URL types - URL Schemes,下面就是笔者在微信应用里面找到的自定义协议:

scheme协议.png

window系统不是配置在plist中,而是通过向注册表HKCR(HKEY_CALSSES_ROOT)目录下添加记录。没有window电脑在手边,找了一张网图给大家看看协议的注册位置:

image.png

1.1 electron应用怎么注册协议

首先electron提供了api给大家,用于自定义协议的注册。 app.setAsDefaultProtocolClient(protocol[, path, args])

  • protocol(string)就是我们要注册的自定义协议关键字,去掉://的部分。例如我们要注册weixin://的协议那就传入"weixin"
  • path(string)这个字段和下面的参数字段都是专门给windows系统使用的,path字段用来传入electron应用可执行文件的路径,默认值为process.execPath
  • args(string[])传递给执行程序的启动参数,默认为空数组

该接口返回boolean值表示调用是否成功。

注意

该接口注册完成后,在macOS系统中,因为系统要求只能注册添加在应用info.plist的协议,所以在打包app时应该在相应的plist文件中添加相应字段,大多数electron的打包工具都提供了相应的字段完成这一工作。例如electron-builder中提供配置字段protocols

相应的在Windows Store环境中使用也需要添加字段到 manifest当中。国内开发者很少会上架window应用商店所以这个大多数开发者用不到

这个接口底层主要使用了Windows注册表接口和苹果的LSSetDefaultHandlerForURLScheme接口。

1.2 代码实现

接下来我们用代码实例来展示一下要完成注册这一步怎么写

//由于macOS和windows系统注册自定义协议的参数不同所以创建一个函数处理注册
function setDefaultProtocol(scheme) {
  //判断系统
  if(process.platform === 'win32') {
    let args = []
    if(!app.isPackaged) {
      //开发阶段调试阶段需要将运行程序的绝对路径加入启动参数
      args.push(path.resolve(process.argv[1]))
    }
    //添加--防御自定义协议漏洞,忽略后面追加参数
    args.push('--')
    //判断是否已经注册
    if(!app.isDefaultProtocolClient(scheme,process.execPath, args)) {
      app.setAsDefaultProtocolClient(scheme,process.execPath, args)
    }
  }
  else {
    //判断是否已经注册
    if(!app.isDefaultProtocolClient(scheme)) {
      app.setAsDefaultProtocolClient(scheme)
    }
  }
}
setDefaultProtocol('st')

注意

为什么要添加--到启动参数末尾,主要是为了低版本的electron应用不受到CVE-2018-1000006漏洞威胁,Electron 自定义协议命令注入(CVE-2018-1000006)分析和 Url Scheme 安全考古这篇文章做了详细解读

2. 获取自定义参数

在上面的步骤之后我们成功注册协议到系统当中,这时候当浏览器在处理对应的协议时就会调起我们的应用。此时我们需要做的事情就是在应用被调起的时候获取自己填写在协议中的字段,然后进行对应的业务处理。

2.1 冷启动获取参数

用户当前没有打开我们的应用也就是我们常说的冷启动的情况,我们应该如何获取协议字段呢。很简单,在app.on('ready')的回调函数里面通过字段process.argv获取协议字段。这里要注意一点,开发完成的应用的process.argv字段类型类似这样[${process.execPath},st://.....] ,我们需要获取process.argv[1],而开发中的应用或者说使用electron .命令启动的应用获取到的process.argv就多了一些字段类似这样[${process.execPath},${...args},myapp://...],${...args}使用electron .启动那就是一个. 所以在开发过程中一般第3个参数才是我们需要的自定义协议。 下面我们实现一下对于process.argv的处理以便获取自定义协议字段

//根据process.argv获取自定义协议
function handleArgv(argv,scheme) {
  let offset = 1
  if(!app.isPackaged) {
    offset++
  } 
  let mySchemeURL = argv.find((item,index) => {
    return index >= offset && item.startsWith(`${scheme}://`)
  })
  console.log('自定义协议',mySchemeURL)
  return mySchemeURL
}
//冷启动主进程代码执行直接在这里获取启动协议
handleArgv(process.argv)

2.2 热启动获取参数

由于系统在通过自定义协议调起应用的处理上遵循这样一个逻辑,就是一个URL开启一个应用。就是说就算你当前应用已经开启了,系统还是会去运行可执行程序开启一个新的应用。我们不想这样所以就要自己进行处理,在macOS上系统会自动帮你关闭后启动的应用实例并将第二个实例的启动参数通过open-urlopen-file事件发送给第一个实例。但是这一机制可以通过命令行启动的方式绕过。windows系统并没有open_urlopen_file事件。所以electron给我们提供了一个通用的锁定单实例的解决方案。

单实例

Electron提供了接口app.requestSingleInstanceLock获取一个实例锁,只有第一个实例返回是ture,其他实例返回false,当我们发现自己是第二实例的时候退出自己就可以了。electron会把第二实例的参数通过second-instance事件转发给单例。 单例模式的实现和对second-instance事件的处理如下

//添加检测单例代码第二实例调用退出接口
const isSingleLockApp = app.requestSingleInstanceLock()
if(!isSingleLockApp) {
  app.quit()
}

app.on('second-instance',(event,argv) => {
  //argv字段就是第二实例的process.argv
  handleArgv(argv)
})

由于macOS系统和windows系统在获取第二实例的参数时使用的接口不太一样,windows系统都依赖second-instance事件,macOS系统默认自动关闭第二实例通过open-url接受参数。

所以综合不同系统对于热启动自定义协议的获取代码如下:

const isSingleLockApp = app.requestSingleInstanceLock()
if(!isSingleLockApp) {
  app.quit()
}

app.on('second-instance',(event,argv) => {
  //argv字段就是第二实例的process.argv
  //只处理windows系统
  if (process.platform === 'win32') {
    handleArgv(argv);
  }
})
//处理macOS系统
app.on('open-url', (event, url) => {
  //由于open-url事件里面的参数url是系统直接帮我们拿到的自定义协议
  handleURL(url)
})

function handleURL(url) {
    //对于自定义协议的处理和使用
}

2.3 业务中的具体使用参数

在上面我们定义了handleURL函数用来统一接受我们的自定义参数,这里我们需要解析出我们具体要使用的参数,并调用到业务代码具体去处理。每个应用的业务逻辑都不同,这里我简单的介绍一下我在具体使用参数时的做法。

首先要完成我们的handleURL进行参数解析

//定义全局变量用来缓存我们取到的参数
let schemeTemp = null
function handleURL(url) {
  //利用URL类解析自定义协议,当然也可以自己写正则,因为大多数的自定协议都遵循'st://module?key1=value1&key2=value2'所以可以直接借助URL解析
  let obj = new URL(url)
  let typeKey = obj.pathname.slice(2)
  let params = obj.searchParams
  let data = {
    typeKey,
    params
  }
  //使用一个变量暂存
  schemeTemp = data
}

这里为什么要暂存参数呢,大多数业务处理都需要我们的渲染进程加载完毕,在渲染进程进行对应的业务跳转。所以我们在主进程接受到自定义参数后需要缓存然后在合适的时机发送出去,我一般这么干

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  // and load the index.html of the app.
  mainWindow.loadURL('https://www.baidu.com')
  mainWindow.once("ready-to-show",() => {
    mainWindow.show()
    if(schemeTemp) {
      mainWindow.webContents.send(`scheme_${schemeTemp.typeKey}`,schemeTemp.params)
      schemeTemp = null
    }
  })
}

后面只需要在渲染进程使用监听处理具体的业务就可以了。

结束

到这里整个通过网页外链调起electron应用的过程就差不多完成了,我们只需要在自己的H5页面当中配置自己的自定义协议就可以通过浏览器唤起我们的应用进行处理。当然在我们的应用内部也是可以拦击我们的自定义协议然后进行对应处理的,这部分官网protocol有详细的接受,后面笔者可能也会结合自己的理解写一下。

致谢