混合开发是指开发Hybrid App,Hybrid App是一种将Native App和WebApp组合在一起的应用。
- Hybrid开发效率高、跨平台
- 学习成本低
- Hybrid从业务开发上讲,没有版本问题,有BUG能及时修复
- 体验没有native好。
- WebApp开发和发布成本最低。
- 但是在移动端,运行在移动端浏览器里的WebApp体验并不好。
- 而且几乎每次打开WebApp都需要下载所有代码,好费流量。
- 无法调用手机原生的能力
开发前需要后台、IOS、安卓、前端要一起商量规划好的细节问题
有两种方法:
- webview里通过Url Schema通知Native调用方法
- Native监听Url Schema的变化,兼容性好,不存在等待webview加载的问题
- webview里直接调用Native注入的方法
- 因为是注入的方法,有一个生命周期,可能会出现js代码已经都实例化完毕,而方法还没被注入的问题。
Url Schema方法指在WebView里发起请求,这些请求都可以被Native拦截到,查看Url,并根据事先约定执行逻辑。
- Native跳H5
- H5跳Native
- H5跳H5(这里还要分内嵌的场景)
- H5新开Webview打开H5
header组件需用Native组件,因为:
- 其它主流容器都是这么做的,比如微信、手机百度、携程
- 没有header一旦网络出错出现白屏,APP将陷入假死状态,这是不可接受的,而一般的解决方案都太业务了
- 使用Native的Header组件,就是为了防止假死
- 通过检测网络情况,webview里js代码执行情况,给Header的后退按钮注册不同的方法,来避免假死。
- webview打开在线站点时,用ajax发请求
- webview打开本地网站时
- 调用native注入的方法发请求
- 使用url schema,native监测到url schema,然后发请求,并执行js的回调
- 为简化逻辑,请求需要都通过native发出
- 登陆和登出,也需要通过调用统一的组件。
- 给每个模块一个版本号
- app每次启动时军询问后台是否有更新
- 有更新则下载新的资源完成更新
- 可以封装一些非常通用的NativeUI以供调用
因为开发时宿主环境是浏览器,上线后是webview,静态资源的位置可能会变化
- 静态资源都读取线上资源,Native可拦截webview请求,实现一些缓存的逻辑。
- H5调用Native
- Native通知H5执行回调
- 能够注入方法
- 封装后的api支持promise式调用
- H5调用Native时,用Url Schema还是直接调用Native注入的方法?
我们公司之前选择了注入,原因我也不知道。
- Native暴露一个方法共H5调用,H5暴露一个方法共Native调用
- 互相调用显然是异步的,于是需要有一个机制来管理回调函数
- 创建一个类,取名News Center.
- 在add方法中,可以看到回调函数有resolve, reject两个属性,而通常做法里回调函数仅仅是一个函数。这么做是为了支持promise
class NewsCenter {
constructor() {
/* 存放callback */
this.callBacks = {};
}
add(id, cb) {
const { resolve, reject } = cb;
const CALLBACKS = this.callBacks;
CALLBACKS[id] = {
resolve: (...params) => {
resolve(...params);
this._remove(id);
},
reject: (...params) => {
reject(...params);
this._remove(id);
},
};
}
get(id) {
return this.callBacks[id];
}
_remove(id) {
delete this.callBacks[id];
}
}
- IOS和安卓分别注入了函数webkit.messageHandlers.jstouseoc.postMessage和android.sendCommond
- 将该函数封装一下,作为类的一个方法_sendCommand
- 还要一个H5调用Native的方法取名为invokeNative
- 还要一个Native调用H5的方法取名为informH5
- 还要一个wrapAPI方法,参数是一个命名空间字符串和一个函数。
期待hybrid的使用方式:
$.wrapAPI('music.play', param => {
return $.invokeNative('paly', param)
});
$.music.play('some Param')
.then(r => console.log('success', r))
.catch(j => console.log('fail', j))
- 在constructor中实例化NewsCenter,以供使用
- 在invokeNative方法中,返回了一个promise,并把resolve和reject添加到了newsCenter中。没有执行resolve或reject,promise将一直处于pending状态
- 又在informH5方法中取出保存在newsCenter里的resolve和reject,执行resolve或reject,使invokeNative方法中的promise被resolved或被rejected。这样就支持了promise风格的api
function idGenerater() {
return CALL_BACK_ID_PREFIX + Date.now()
}
class Hybrid {
constructor() {
this.newsCenter = new NewsCenter();
this.isInApp = brand !== OTHER_BRAND;
}
/* 向native发送命令 */
_sendCommand(cmd, id, params) {
if (brand === IPHONE) {
window.webkit.messageHandlers.jstouseoc.postMessage({ cmd, id, params });
} else if (brand === ANDROID) {
android.sendCommond(cmd, id, params);
} else if (brand === OTHER_BRAND) {
/* 以下为测试代码 */
console.log(`cmd:${cmd} \n id:${id} \n params:${params}`)
setTimeout(() => {
this.informH5(id, JSON.stringify({
callbackType: 'fail',
data: 'okok123',
params
}))
}, 2000)
} else {
throw 'Unkonwn platform isn\'t supported';
}
}
/* h5调用native */
invokeNative(method, params) {
return new Promise((resolve, reject) => {
const id = idGenerater();
this._sendCommand(method, id, params)
this.newsCenter.add(id, { resolve, reject });
})
}
/* native通知h5执行回调 */
informH5(id, content) {
let rtn;
try {
rtn = JSON.parse(content);
} catch (err) {
console.warn(err)
}
const cb = this.newsCenter.get(id);
if (cb == null) return;
const { callbackType } = rtn;
const { resolve, reject } = cb;
if (callbackType === 'success') {
resolve(rtn)
} else {
reject(rtn)
}
}
/* 创建一个api */
wrapAPI(nameSpace, wrapAPI) {
const names = nameSpace.split('.');
const fnName = names.pop();
let result = this;
while (1) {
if (names.length === 0) break;
let name = names.shift();
if (result[name] == null) {
result[name] = {}
}
result = result[name]
}
if (result[fnName] != null) {
throw `${nameSpace} has already been defined!`
}
result[fnName] = wrapAPI;
}
}
前端社区有好几种模块化方案,采用UMD的写法让代码能在各种环境中都能运行。 更多知识可以看这里
通过检测是否有特定的全局变量,来判断当前处于什么环境
(function (root, factory) {
/* AMD */
if (typeof define === 'function' && define.amd) {
define([], factory);
/* CMD */
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
/* 没有采用模块化,script标签引入 */
} else {
Object.assign(root, factory())
}
}(typeof self !== 'undefined' ? self : this, function () {
class NewsCenter {
/* 省略 */
}
class Hybrid {
/* 省略 */
}
/* 暴露hybrid实例和informH5方法到全局 */
const $ = Hybrid = new Hybrid();
const inform = $.informH5.bind($);
/* 上面factory()返回的就是这个对象 */
return {
Hybrid,
inform
};
}));
相比在hybrid类中用一个对象保存id和回调函数,为news center的逻辑创建一个类有很多好处
- 高内聚低耦合
- 代码更易读
- 需求更改时需要被修改的代码更少
- 例子中news center类在constructor中被实例化。稍加修改,在外部实例化,以参数形势传入constructor方法,便可实现依赖注入
js里无法直接设置私有方法,导致我们创建的hybrid类导出后每个方法都能被调用到。 如果不用ES6的类,可以这样做
var hybrid = global.hybrid = {};
var privateFunc = ()=>{};
var publicFunc = ()=>{
privateFunc()
};
hybrid.privateFunc = privateFunc;
使用ES6的类,方便许多。且私有方法和公用方法,视觉上就泾渭分明。
var privateFunc = ()=>{};
class hybrid {
publicFunc(){
privateFunc();
};
}