您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
智管有方客户端诞生记
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
智管有方客户端诞生记
智管有方前端研发组
2021-01-25
IP归属:未知
38040浏览
> 道生一,一生二,二生三,三生万物 ——《道德经》 # 道生一 先回到最具有哲学性的问题 —— 为什么要做客户端?当然不是为了什么KPI,讲的严肃一点就是为了更好的服务于客户。 当前智管有方已经有了自己的网页端,网页端运行在了浏览器中,在针对浏览器以外的世界却是无能为力。为此如果想进入更高的境界,就需要客户端入场了。 客户端带来的优点有 - 多端协作,适应于各种业务场合 - 可以处理系统的操作,突破浏览器的限制 - 可以带来更佳的用户体验 - 可以结合C++等底层技术,而浏览器的Webassembly还没有推广 # 一生二 ## 选型 显而易见的是,如果客户端交给前端团队来做,那么选型基本上是从`Electron`和`NW.js`之间做选择了。 两者都是基于`Chromium`和`Node.js`,对于新特性都能无缝支持,比较一下两者(不支持表格真难受) ![compare](//img1.jcloudcs.com/developer.jdcloud.com/719b76d8-4b80-40de-9874-f6b3ee0e32e320210124233719.png) 近年来由于`Electron`的活跃程度很高,又有像VSCode、Postman这样的神级应用来背书,采用`Electron`的场景越来越多,因此我们也选择使用`Electron`。 `Node.js`的版本综合考虑到各种依赖的情况,最后定为10.22.x。 主要用到的技术栈和依赖有 ![](//img1.jcloudcs.com/developer.jdcloud.com/441d87be-111e-439e-9539-af9de110203320210124233828.png) ## 目录结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/6144f164-c78e-4081-8e37-b18d7c9b30d720210124233909.png) ## 网络请求 为了避免渲染进程的跨域等一些限制性问题,我们在主进程里对`axios`进行了封装 然后在渲染进程里(`Vue`)通过 ```javascript Vue.http = Vue.prototype.$http = remote.app.sendRequest ``` 注入到`Vue`的原型中去,这样在渲染进程中调用网络请求实际是从主进程里发起。 ## 视图 客户端视图层技术栈我们选用和智管有方网页端一致的`Vue`生态圈,为的是站在巨人的肩膀上复用既有的网页端的代码基础。 客户端本身的视图层功能主要是管理顶部的用来显示菜单和系统功能的Bar和管理窗口,而真正的业务部分的视图我们是把智管有方网页端给集成进来。 ## 集成智管有方网页端 由于智管有方网页端珠玉在前,不应该重复在客户端里再造智管有方的业务模块,而是尽量将网页端集成到客户端中。于是我们用到了`BrowserWindow`模块。 ### 集成 `BrowserWindow`中有三种方式嵌入网页,分别为 - `iframe`: 可以安全的加载web页面,但是在处理事件与通信方面很弱势 - `webView`: 仅在`Electron`内部起作用。且有很多自定义方法和事件,可以很好的对内容进行控制 - `browserView`: 可以对加载的内容进行最大的控制,但是位置不受DOM和CSS的控制,且mac系统中,最大化`browserView`后,会出现不响应点击的致命BUG。 为了达到下面的四个要求 1. 安全的加载web网页 2. 能对加载的网页进行控制 3. 拥有自定义的窗口UI设计 4. 渲染进程 与 加载的web页面 进行node操作 我们先使用`BrowserWindow`进行了自己的窗口UI设计,并使用上述的方案1让`browserWindow`可以随意调用electron的api。 然后在`browserWindow`使用`<webView>`加载web网页,并使用方案2为信任的web网页注入脚本。 集成之后,即便是客户端特有的功能,也可以跟随网页端部署,实现快速上线。 ### 判断 由于网页端和客户端会共享一套代码,而一些业务需要区分当前的运行环境,判断是否运行在客户端里我们是根据浏览器`userAgent`字符串 ```javascript export function isElectron() { return typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.toLowerCase().indexOf('electron') >= 0 } ``` 同时,也是通过`userAgent`来判断是否当前操作系统是Windows还是Mac OS。 ### 通信 集成之后,智管有方客户端相当于有了三个宿主环境 1. 主进程 node.js 2. 渲染进程 chromium 3. 渲染进程中的webview chromium 除了常见的Electron里主进程和渲染进程的通信,还涉及到了webview与主进程之间的通信,我们有两种方案: 方案一是Electron 12之前,可以通过`nodeIntegration`属性将`Node.js`主进程注入进去,但这样子安全性得不到保障 ```javascript new BrowserWindow({ webPreferences: { nodeIntegration: true,// electron 12 后 会废弃此参数。 contextIsolation:false,//是否在独立的环境中运行 Electron API和指定的preload 脚本。electron12后默认为true。 } }) ``` 方案二是通过`contextBridge.exposeInMainWorld`作为一个桥梁,将api暴露给渲染进程的全局变量,脚本通过preload属性注入给了`<webview>` ```javascript // 方案2 new BrowserWindow({ webPreferences: { nodeIntegration: false // electron 12 后 会废弃此参数 contextIsolation:true,//是否在独立的环境中运行 Electron API和指定的preload 脚本。electron12后默认为true。 preload:"demo.js" //预加载脚本 } }) ------------------------------------------------------------------------------ // demo.js const {contextBridge, ipcRenderer, remote} = require('electron') const version = remote.app.getVersion() // 使用contextBridge 将 api暴露给渲染进程。 渲染进程就可以直接使用window.moduleName来调用暴露出的api contextBridge.exposeInMainWorld('electronIpc', { version, sendToHost: function (eventName, ...params) { ipcRenderer.sendToHost(eventName, ...params) } //... }) ``` 接着在网页中可以调用`window.electronIpc.sendToHost`方法了,第二种方案在安全性上更为可靠 ### 启动与登陆 登陆的方案是与Web一致的,只不过token会存于electron-store中(主进程使用),再存到渲染进程(浏览器)的cookie中(内嵌web页面使用)。 启动与登陆过程的流程图如下 ![](//img1.jcloudcs.com/developer.jdcloud.com/d5d366c9-5768-4f60-b4b8-7b2613f99a3e20210124234408.png) ### 自定义错误页面 正常的浏览器,当页面资源加载发生错误时,会有提示页面,如页面超时,URL协议错误,域名解析失败等。但遗憾的是,electron发生这些错误时,只有黑屏,因此需要自己捕捉错误后展示符合需求的错误页面。 electron的渲染进程采用chromium内核,因此资源加载错误的错误编码由chromium内核提供,具体的连接地址在下面,不需要详细的看,因为错误码有上百个,只需要将常见的特殊处理,非常见的统一处理就好。 errorCode: -118 ERR_CONNECTION_TIMED_OUT ------ 访问超时 errorCode: -105 ERR_NAME_NOT_RESOLVED ------ DNS解析失败 errorCode: -300 ERR_INVALID_URL ------ URL错误 errorCode: -3 ABORTED ------- aborted chromium加载web页面时,会把html中的各种资源全部加载,比如图片,css,js等,这些资源的加载失败也会抛出上述的错误码,因此,捕获到这些错误时,最好做个判断,即抛出错误的资源路径和地址栏中的路径一致后,再展示对应的错误页面 ```javascript <webview :src="linkConfig.url" :nodeintegration="false" preload="demo.js" webpreferences="worldSafeExecuteJavaScript, contextIsolation" @did-fail-load="windowFailLoaded" /> windowFailLoaded(e) { // 当失败的连接与webview页面连接相同 且 未被aborted if ((e.errorCode + '' !== '-3') && e.validatedURL === webView.getURL()) { if (e.errorCode + '' === '-118') { // 展示页面加载超时的错误页面 } else { // 展示其他通用的错误页面 } } } ``` # 二生三 ## 主题切换 在智管有方的网页端中,我们支持主题皮肤的亮色与暗色的切换,而在客户端里,由于直接在`<webview>`加载了业务模块网页,所以在客户端操作切换主题的时候要多做一步 —— 将变化后的主题通知给所有的渲染进程。 ## 日志 我们使用`electron-log`来进行日志记录,实际业务操作中,日志记录非常重要,有了日志基本就能推断出当时的情况。 ### 自启动 实现自启动是通过app实例的`setLoginItemSettings`方法,它在不同平台上有各自的实现,比如在Windows上就是写入注册表,在Mac OS上是修改配置。传入是否自启动和自动路径,这个有一个坑是尽量使用electron的API —— `app.getPath('exe')` 来获取执行文件的路径,避免使用`process.cwd()`等来获取执行文件路径。因为windows开机自动启动后,`process.cwd()`等会返回`C:\Windows\System32`。 ### 与第三方客户端的通信 交易业务有一个需求是在智管有方客户端中需要检查第三方客户端是否安装、启动第三方客户端、检查第三方客户端是否启动、如果启动了需要把第三方客户端窗口前置(仅考虑Windows平台)。 - 检查客户端是否安装 当前的解决方案是约定第三方的客户端安装时会去Windows操作系统里写注册表,这样子的话我们仅需要通过regedit模块来读取注册表,进而能判断第三方的客户端是否安装。 - 启动客户端 读取出注册表的项值是第三方客户端的启动入口exe,接着就只要执行该exe就能唤起客户端。 - 检查客户端是否启动 在启动客户端之前当然要检查客户端当前是否启动,当前的做法是从进程里查找进程名,如果发现存在进程名,那就说明客户端已经启动了。 - 窗口前置 窗口前置原来的解决方案是使用`ffi-napi`与`win32-api`来调用Windows系统内置的函数`FindWindowA`、`SendMessageW`、`SetForegroundWindow`,但是后来发现有一些函数对中文窗口名支持不好,后来改用了操作窗口的大杀器——`node-window-manager`。使用它不需要了解dll原理或者Windows系统函数,而且许多细节都做了屏蔽,只需要调用语义化很好的API就行了,可以比较下面的代码 ```javascript // 得要懂Windows开发才行 const handle = myUser32.FindWindowA(null, 'windowName') // 还原窗口 user32.SendMessageW(handle, 0x0112, 0xF120, 0) // 前置窗口 user32.SetForegroundWindow(handle) // 一眼能看出表达什么含义 window.restore() window.bringToTop() ``` ## 打包 虽然已经有`electron-builder`来实现打包,但它确实也是一个力气活。首先尽量在对应的操作系统上打包对应的版本,在Windows系统上打包Windows的客户端版本,在Mac OS系统上打包Mac OS的客户端版本,减少发生问题的可能性。 打包的时候记得停止开发模式,因为开发模式会占住文件,而打包时可能要去操作文件的,其他可能会占用住文件的操作也全部退出,否则会让你查错查到怀疑人生。 打包时会询问是否自动上传和是否强制更新,尽量做到自动化。 ### 自定义条件编译 `Electron`是没有条件编译支持的,当时有些场景下,需要使用到条件编译,比如调用C++的动态链接库,因Mac OS和windows系统的差异性,若没有条件编译进行区分,编译过程中就会出错。 条件编译的中间件网上已有很多,此处列出一种简单的实现方式 ```javascript // webpack 配置截取 module: { rules: [ { test: /\.(js)$/, exclude: /node_modules/, use: [ {loader: path.resolve(__dirname,'./conditional-compile-loader.js')} ] } ] } // ./conditional-compile-loader.js const REG = /\/\*\s*IF(TRUE_\w+)(?:\s*\*\/)([\s\S]+?)(?:\/\*\s*)END\1\s*\*\//g; module.exports = function (source) { const options = { 'darwin':process.platform === 'darwin', //mac平台 'win32':process.platform === 'win32', // windows平台 } return source.replace(REG, (match, $1, $2) => options[$1.slice(5)] !== false ? $2 : '') }; // 可执行条件编译的代码段 /* IFTRUE_win32 */ var anyJsHere = '任何 只有windows平台生效的js' /* ENDTRUE_win32 */ /* IFTRUE_darwin */ var anyJsHere = '任何 只有mac平台生效的js' /* ENDTRUE_darwin */ ``` ### MAC的签名与公证 若不打算把应用上架到APP Stroe, 就需要对应用进行签名和公证后才能进行正常的使用,否则会被Mac的门禁系统拦截。 可以通过在访达中,按住control建点按应用,从带单中选取“打开” ,来打开未被公证的应用。公证流程大概耗时10分钟,因此测试包尽量不选择公证。 # 三生万物 ## 当前还存在的问题 ### 用户体验 跟一些使用`Electron`开发的大神级应用相比,智管有方客户端的用户体验还有待提高。 ### 卸载时的清理 在客户端使用过程中可能修改了系统的设置,比如写入注册表等,但目前没有卸载程序时的清理工作,将会影响下一次安装的客户端的运行状态。 ### Windows环境下的窗口漂移 windows在屏幕缩放比大于100%时,`setPosition`,`setBounds`,`setSize` 会出现1-2px的偏差,所以,当频繁调用`a.setPosition(getPosition())`,会出现漂移效果。 ### 其他各种奇怪的问题 由于Electron及其生态圈的高速迭代,还有很多奇奇怪怪的问题没人踩过,等着我们一个个去解(zhe)决(mo)。 ## 未来 ### 更小 智管有方当前的客户端安装包大约在60多M,安装后达到了300~400M,总体来说还是偏大。 ### 更快 无论何时都应该追求更快的速度和更好的性能,考虑到浏览器内核本身的限制,如果想在这个方向走得更远,需要更多的结合底层技术。 ### 更强 当前客户端网页化的痕迹还很重,应该将其打磨得更像一个原生应用,一些用户操作习惯向原生应用去靠拢,比如右键、拖拽等,带给用户更好的体验。 对于流行的技术比如PWA、WebAssembly等我们也应该开放的去拥抱它们,将客户端产品质量、体验不断提高,更好服务于客户。
原创文章,需联系作者,授权转载
上一篇:喜报丨京东科技主导的开源项目ShardingSphere荣登报告榜单国人主导开源项目中活跃度第五名!
下一篇:Agile Alliance 服务监控-系统的私人医生
智管有方前端研发组
文章数
2
阅读量
1881
作者其他文章
01
智管有方客户端诞生记
本文简述了智管有方客户端开发的要点以及各种踩过的坑
01
你的“智能缩放”够智能吗?
做界面设计的老铁们都希望把时间用在一些真正有意义的事上去,不希望做一些无脑的重复性工作。在整个设计流程中的很多便利性工具应运而生。 就连界面搭建的核心环节,也有了很多组件库+模版的资源,供我们把更多的重心放在体验环节。但是无论是组件库还是模版的资源,都在尺寸的调节上不够灵活,对设计师的效率提升帮助有限,所以,今天跟大家分享一个我在工作中用到的方法。希望能对大家更快的调节搭建出一个界面有帮助。
最新回复
丨
点赞排行
共0条评论
智管有方前端研发组
文章数
2
阅读量
1881
作者其他文章
01
你的“智能缩放”够智能吗?
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号