前言 对于一个 URL 来说,其实就是指定了一个协议(protocol),然后让系统用对应的应用去打开它。如 myapp://openapp?name=1&pwd=2,系统会去找到已经注册了 myapp 这个协议的应用,然后把 URL 当做参数传过去。
这样我们就可以在浏览器中通过一个 <a> 标签简单地唤起应用了。
1 <a href ="myapp://openapp?name=1&pwd=2" > 打开应用</a >
单实例运行 首先,每次打开一个协议 URL,系统都会启动一个新的应用。这就需要应用自己去判断,把 URL 当做参数传给已有的应用,还是自己直接处理。
Electron 提供了一个简单的方法,来获取一个锁,只有第一个调用的实例才能获取成功,后面的其他实例则把参数传过去,然后退出就可以了。
1 2 3 4 5 6 7 8 const { app } = require ('electron' );const gotTheLock = app.requestSingleInstanceLock ();if (!gotTheLock) { app.quit (); }
注册协议 我们期望通过协议来启动应用,所以要先注册一个协议到系统中,调用 API 即可:
1 2 3 4 5 6 7 8 9 10 const PROTOCOL = 'myapp' ;const args = [];if (!app.isPackaged ) { args.push (path.resolve (process.argv [1 ])); } args.push ('--' ); app.setAsDefaultProtocolClient (PROTOCOL , process.execPath , args);
这里的 args 是预定义的参数,对 macOS 没有作用,但是在 Windows 上却是必不可少的。
在 Windows 上启动一个协议URL时,实际上是用如下参数启动了我们的应用:
1 ${process.execPath} ${...args} myapp://...
需要注意的是,在开发阶段,我们是通过 electron . 或者 electron path/to/script.js 来启动的应用,所以 process.argv[1] 是我们的脚本路径,传给系统时,这个参数也不能少,否则启动的就是一个纯粹的 Electron 壳,而不是我们的应用了。这时,这个参数就要通过这里的 args 一起注册到系统中了。
根据这个帖子 ,我们可以在预定义的参数最后加一个 -- ,来阻止其他参数直接被 Electron 处理。
获取参数 第二个实例运行的时候,自己就退出了,那么第一个实例如何能获取到启动第二个实例的参数呢?这里 macOS 和 Windows 上的行为是不一致的,需要分别处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 handleArgv (process.argv );app.on ('second-instance' , (event, argv ) => { if (process.platform === 'win32' ) { handleArgv (argv); } }); app.on ('open-url' , (event, urlStr ) => { handleUrl (urlStr); });
Windows 下 argv 的处理有一个需要注意的地方,就是开发阶段要跳过前两个参数(execPath 和当前启动脚本):
1 2 3 4 5 6 7 8 function handleArgv (argv ) { const prefix = `${PROTOCOL} :` ; const offset = app.isPackaged ? 1 : 2 ; const url = argv.find ((arg, i ) => i >= offset && arg.startsWith (prefix)); if (url) handleUrl (url); }
完整的协议链接跟HTTP链接类似,如下:
myapp://openapp?name=1&pwd=2
但大部分情况下,我们可能只需要最后的 key-value 参数就可以了,所以可以省略成:
myapp://?name=1&pwd=2
链接可以通过全局对象 URL 来解析:
1 2 3 4 5 6 7 8 9 function handleUrl (urlStr ) { const urlObj = new URL (urlStr); const { searchParams } = urlObj; console .log (urlObj.search ); console .log (searchParams.get ('name' )); console .log (searchParams.get ('pwd' )); }
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const path = require ('path' );const { app } = require ('electron' );const gotTheLock = app.requestSingleInstanceLock ();if (!gotTheLock) { app.quit (); } const PROTOCOL = 'myapp' ;const args = [];if (!app.isPackaged ) { args.push (path.resolve (process.argv [1 ])); } args.push ('--' ); app.setAsDefaultProtocolClient (PROTOCOL , process.execPath , args); function handleArgv (argv ) { const prefix = `${PROTOCOL} :` ; const offset = app.isPackaged ? 1 : 2 ; const url = argv.find ((arg, i ) => i >= offset && arg.startsWith (prefix)); if (url) handleUrl (url); } handleArgv (process.argv );app.on ('second-instance' , (event, argv ) => { if (process.platform === 'win32' ) { handleArgv (argv) } }); app.on ('open-url' , (event, urlStr ) => { handleUrl (urlStr); }); function handleUrl (urlStr ) { const urlObj = new URL (urlStr); const { searchParams } = urlObj; console .log (urlObj.search ); console .log (searchParams.get ('name' )); console .log (searchParams.get ('pwd' )); }
收到协议打开页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 let autologindata = null ;function handleUrl (urlStr ) { const urlObj = new URL (urlStr); const {searchParams} = urlObj; let userid = searchParams.get ('userid' ); let sectionid = searchParams.get ("sectionid" ); console .log (userid); console .log (sectionid); autologindata = { "userid" : userid, "sectionid" : sectionid }; console .info (autologindata); }
首页面设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const isDevelopment = !app.isPackaged ;function createWindow ( ) { loginWindow = new BrowserWindow ({ width : 750 , height : 472 , frame : false , show : false , transparent : true , resizable : false , webPreferences : { nodeIntegration : true , enableRemoteModule : true , webSecurity : false , contextIsolation : false , }, }); if (isDevelopment) { loginWindow.loadURL (`file://${__dirname} /login.html` ) } else { loginWindow.loadURL (format ({ pathname : path.join (__dirname, 'login.html' ), protocol : 'file' , slashes : true })) } loginWindow.once ("ready-to-show" , () => { loginWindow.show (); if (autologindata) { loginWindow.webContents .send ("autologinEvent" , autologindata); } }); }
这样设置的原因是
收到协议传输数据的时候页面还没创建,所以用外部变量保存接收的值,当窗口打开后再做处理
登录页面监听值
1 2 3 4 5 6 7 8 9 10 11 12 autologin_event ( ) { ipcRenderer.on ("autologinEvent" , (event, arg ) => { let {userid, sectionid} = arg; this .userid = userid; this .sectionid = sectionid; logger.info (`自动登录 userid:${userid} sectionid:${sectionid} ` ); localStorage .setItem ("sectionid" , this .sectionid ); this .autologin = 1 ; this .toAutologin (userid, sectionid); }); },
与单实例结合 我的程序是单实例的,如果打开第二个实例的时候,我这边的处理是显示第一个实例的窗口,
第一个实例正在打开的窗口可能是其他窗口,而协议的传参我只在登录处理了,所以这里就忽略了第二个实例的传参处理。
main.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const gotTheLock = app.requestSingleInstanceLock ();if (!gotTheLock) { app.quit () } else { app.on ('second-instance' , (event, commandLine, workingDirectory ) => { if (loginWindow && !loginWindow.isDestroyed ()) { if (loginWindow.isMinimized ()) { loginWindow.restore () } loginWindow.focus () } if (classcenterWin && !classcenterWin.isDestroyed ()) { if (classcenterWin.isMinimized ()) { classcenterWin.restore () } classcenterWin.focus () } }) app.on ('ready' , () => { }) } let autologindata = null ;const args = [];if (!app.isPackaged ) { args.push (path.resolve (process.argv [1 ])); } args.push ('--' ); const PROTOCOL = 'xhlivepc' ;app.setAsDefaultProtocolClient (PROTOCOL , process.execPath , args); function handleArgv (argv ) { const prefix = `${PROTOCOL} :` ; const offset = app.isPackaged ? 1 : 2 ; const url = argv.find ((arg, i ) => i >= offset && arg.startsWith (prefix)); if (url) handleUrl (url); } handleArgv (process.argv );app.on ('open-url' , (event, urlStr ) => { handleUrl (urlStr); }); function handleUrl (urlStr ) { const urlObj = new URL (urlStr); const {searchParams} = urlObj; let userid = searchParams.get ('userid' ); let sectionid = searchParams.get ("sectionid" ); autologindata = { "userid" : userid, "sectionid" : sectionid }; }
这里面删除了
1 2 3 4 5 6 app.on ('second-instance' , (event, argv ) => { if (process.platform === 'win32' ) { handleArgv (argv) } });
login.vue 登录页面这样处理
页面加载事件
1 2 3 4 5 6 7 mounted ( ) { this .autologin_event (); setTimeout (() => { this .init_version (); }, 500 ) },
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 methods: { autologin_event() { ipcRenderer.on("autologinEvent" , (event, arg) => { let { userid, sectionid} = arg; this.userid = userid; this.sectionid = sectionid; logger.info(`自动登录 userid: ${ userid} sectionid: ${ sectionid} `); localStorage.setItem("sectionid" , this.sectionid); this.autologin = 1 ; } ); } , async init_version() { let that = this; let needUpdate = false ; try { let data = await api_apppc_get_new({ } ) let res = data.data; if (res.code === 0 ) { let obj = res.obj; if (obj != null && obj.versioncode > versioncode) { if (obj.versionpath) { that.version_flag = true ; that.version_obj = obj; needUpdate = true ; } } } else { this.$Message.error("检测更新失败,请检查网络!" ) } } catch (e) { this.$Message.error("检测更新失败,请检查网络!" ) } if (!needUpdate) { if (this.autologin === 1 ) { await this.toAutologin(this.userid, this.sectionid); } } } , async toAutologin(userid, sectionid) { } , }