1、背景
公司研发的一款服务软件App(姑且称为“大地”),提供了包涵消息、待办、工作台、同事圈和通讯录五大功能模块,其中,工作台里集成了包括公司的移动客户端、PC端以及第三方平台的部分功能/服务(统称为“应用”)。
我今天要讲的是这个集成平台以什么方式展现“应用”,答案是:借鉴了微信的架构,自研了“小程序”接入“应用”。
我司小程序具有一种相对开放能力(面向全公司),赋能业务快速数字化、场景敏捷迭代,并且可在“大地”上便捷的获取和使用,同时具有完善的使用体验(这就是严格的接入审核标准带来的好处)。
在“大地”开发者平台,创建小程序会自动创建配套的公众号(公众号是为了推送消息使用,可订阅)。小程序开发不限制技术选型,开发完成之后按照小程序接入规范打包上架小程序,审核发布。
简单来说,可以把“大地”看成是一个“钉钉”,我现在要把我们的业务功能投放到“大地”上,就需要接入“大地”小程序,以小程序的方式在“大地”上为用户提供服务。
小程序架构:Cordova
框架做的WebView
,运行我开发的前端程序,通过Nginx
帮我把请求代理到微服务网关,由网关转发到目的主机处理请求。它虽然看上去是一个Native App
,但只有一个UI WebView
,里面访问的是一个Web App
,对我来说就是开发一个H5
应用调用一些所需的JSBridge
,也就是所谓的Hybrid App
。
下面看一下本地开发中的一些问题,以及我是怎么处理的
2、问题
Hybrid App
本地开发过程中没有真实的Native
环境的,同样也无法使用JSBridge
,这就会带来一个问题:跟原生交互的行为只能发布小程序才可以调试,本地玩不了,这...,相当fuck。
目的是想让本地开发同小程序测试环境具有相同的体验,我的想法是在本地模拟JSBridge
的方法,尽管不能带来真实的效果,至少触发了某个行为之后要有个反应,不至于让操作流程看起来像是“脱节”的(实际跟原生的交互行为并不多,比如:拍照、弹窗提示、定位等等)。
因此,我要做的就是本地模拟JSBridge
的一些方法,开发时触发了这些原生交互行为之后提示一些信息,等到上架小程序测试环境时,在手机上会用真实的JSBridge
方法自动替换掉我模拟实现的方法。
于是我就开始了下面的准备工作。
- 搞清楚
JSBridge
运行的原理 - 本地模拟
JSBridge
的方法 - 上架小程序是自动使用真实的
JSBridge
3、了解JSBridge
JSBridge:望文生义就是js
和Native
之前的桥梁,而实际上JSBridge
确实是JS
和Native
之前的一种通信方式。
简单的说,JSBridge
就是定义Native
和JS
的通信,Native
只通过一个固定的桥对象调用JS
,JS
也只通过固定的桥对象调用Native
。JSBridge
另一个叫法及大家熟知的Hybrid app
技术。
了解即可,更多的请参考
下图展示了JSBridge
的工作流程👇
上图中左侧部分正式我要做的,具体请看下文
看累了,三连一下,回看不迷路哟😉
3.1、我们的JSBridge
推测“大地”那边的JSBridge
应该是自己写的,没有初始化JSBridge
的操作
当调用JSBridge
时,必须在页面完全加载完成之后才能够拿到全局的JSBridge
,Cordova
框架提供deviceready
事件,该事件触发的时候表示全局的JSBridge
挂载成功。(注意:这就是我接下来操作的切入点,嘻嘻)
简单写下如下:
document.addEventListener('deviceready', function () { console.log('deviceready OK!'); JSAPI.showToast(0, '提示信息') }, false)
需要注意的是,在开发环境,是没有 deviceready
事件的,所以上面的代码并不会执行,只有在app
里面运行的时候才会执行。
思考:
JSBridge
必须是在deviceready
事件触发后方能使用的,因此首先要做的就是自定义deviceready
事件,本地环境可以在load
事件里触发自定义deviceready
事件,生产环境下监听deviceready
事件即可
4、JS发起自定义事件
我是用 CustomEvent
构造函数,继承至 Event
,文档看这里
- 用法
new CustomEvent(eventName, params);
- 示例
创建一个自定义事件
const event=new CustomEvent('mock-event');
- 传递参数
这里值得注意,需要把想要传递的参数包裹在一个包含detail
属性的对象,否则传递的参数不会被挂载
function createEvent(params, eventName = 'mock-event') { return new CustomEvent(eventName, { detail: params }); } const event = createEvent({ id: '0010' });
- 发起事件
调用dispatchEvent
方法发起事件,传入你刚才创建的方法
window.dispatchEvent(event);
- 监听事件
window.addEventListener('mock-event', ({ detail: { id } }) => { console.log('id',id) // 会在控制台打印0010 });
- 示例:
document.body.addEventListener('show', (event) => { console.log(event.detail); }); // 触发 let myEvent = new CustomEvent('show', { detail: { username: 'xixi', userid: '2022' } }); document.body.dispatchEvent(myEvent);
了解了自定义事件之后,通过自定义事件模拟触发
deviceready
事件,这样上面的deviceready
事件监听就可以执行了。注意:这里还要确定一个问题,在什么时候触发自定义事件
deviceready
呢?
5、确定 deviceready
事件执行时机
- 只需要编写如下代码,查看输出结果即可
window.addEventListener('load', function () { console.log('load OK!'); }, false); document.addEventListener('deviceready', function () { console.log('deviceready OK!'); }, false);
- 结果输出
load OK! deviceready OK!
由此可知,执行顺序:load
--> deviceready
6、自定义事件模拟Cordova
deviceready
事件
- 自定义
deviceready
事件 - 根据上面测试执行顺序得出的结论,我在
load
事件里触发自定义事件 - 在开发环境下模拟一些用到的
JSBridge-API
,比如下面写到的JSAPI.showToast()
方法
- mockEvent.js
if (process.env.NODE_ENV === 'development') { // 自定义事件 let myEvent = new CustomEvent('deviceready'); // 模拟JSAPI事件 window['JSAPI'] = { showToast(type, desc) { console.log(type, desc); } } // ... // 开发环境下,在 原生 load 方法之后 触发自定义事件 window.addEventListener('load', function () { console.log('load OK!'); setTimeout(() => { document.body.dispatchEvent(myEvent); }, 100) }, false); }
7、封装deviceReady
方法
实现在Cordova
框架触发deviceready
事件的时候感知到,以便于在deviceReady
事件触发后执行JS-API
。
可用于开发环境和非开发环境
7.1、方式一
这里采用链式调用的方式,
以下这种借助 Promise
的实现,在这种场景下其实是不合理的👀 只是形式上类似,其实并不是
- 定义
- mixin.js
deviceReady() { return new Promise((resolve) => { window.addEventListener('deviceready', function () { resolve("ready go!"); }, false); }) }
- 组件内使用
JS-API
使用JSAPI
可以如下这么写
this.deviceReady().then((res) => { console.log(res); // ready go! JSAPI.showToast(0, '提示') }) this.deviceReady().then((res) => { JSAPI.getUserInfo((res) => { console.log(res); }, (err) => { console.log(err); }); })
- 开发环境执行效果如下
7.2、方式二(推荐)
改写成通用的事件监听函数,支持链式调用
- 开发环境下,由
mockEvent.js
文件里的dispatchEvent
触发自定义的deviceready
事件;- 小程序里运行,则由真实的
deviceready
事件触发
- 定义
- mixin.js
receiver(type) { let callbacks = { fns: [], then: function(cb){ this.fns.push(cb); return this; } }; document.addEventListener(type, function(ev) { let fns = callbacks.fns.slice(); for(let i = 0, l = fns.length; i < l; i++){ fns[i].call(this, ev); } }); return callbacks; }
- 使用
this.receiver('deviceready').then((ev) => { console.log(ev); JSAPI.getUserInfo( (res) => { console.log(res); }, (err) => { console.log(err); } ) }) this.receiver("click") .then(() => console.log("hi")) .then(()=> console.log(22));
最后
当应用发布到app
上,就是监听的真实的 Cordova
框架的 deviceready
事件了,之后也就可以拿到真实的JSAPI
了,以上只是为了在开发环境的时候模拟使用JSAPI
。防止在开发环境下直接调用JSAPI
飘红的情况,当然也是可以加try catch
处理的,只不过个人感觉模拟事件使得代码看起来更加优雅别致一点,使用更加丝滑,酌情食用😁。
软件架构非常有意思,感兴趣的可以交流探索,嘻嘻。