优雅的使用native jsapi
什么是混合开发
顾名思义,混合开发就是多种技术融为一体,共同构建一个产物。那么客户端的hyrid混合开发指的是什么呢?指的就是通过 指通过 Web 技术(如 HTML、CSS、JavaScript) 开发移动应用,并通过一个原生容器(Native Container)将 Web 页面嵌入到移动应用中运行。Hybrid 应用结合了 Web 应用的跨平台性和原生应用的功能性。具有开发效率高(一次开发,多平台运行);无需发版即可实现热更新的特点。 这里特意强调下微信,微信极大的推动了这种技术的普及,比如图文时代的公众号,以及现在小程序都是混合开发的产物,一处开发,运营两端。

问题在哪
从工作一来,一共呆过三家公司,都有混合开发影子,特别是第二家公司,但是不约而同的都出现过如下的问题。
方法散落各处,管理麻烦,缺乏版本控制
方法调用层级过深,在非客户端内报错问题
方法缺少说明和调试页面,无法实现前后分离

如何解决以上两个问题
- 封装nativejsapi的仓库,所以api统一调度,针对每个方法做版本控制
- 在仓库中做ua头判定,对非目标环境下,直接return false
- 开发调试页面和文档
技术方案
文件目录:
获取App信息
ts
const isClient = typeof window !== "undefined";
const userAgent = isClient ? window.navigator.userAgent : "";
const IsProd = isClient ? location.host.indexOf(".com") > 0 : true;
const UA = window.navigator.userAgent;
let appType = "NATIVE_APP";
const regex = new RegExp(`${appType}\/\\d{1}\\.\\d{1,2}\\.\\d{1,2}`, "ig");
const matchResult = (UA.match(regex)?.[0] as unknown as string)?.split("/");
const isAndroid = UA.indexOf("Android") > -1 || UA.indexOf("Adr") > -1;
const lowerUserAgent = userAgent.toLowerCase();
const appUas: [string?] = [];
const appVersionRegExp = new RegExp(`(${appUas.join("|")})\\/([^\s]*)\s?`, "i");
const appVersionResult = lowerUserAgent.match(appVersionRegExp);
export const appInfo = {
appName: matchResult?.[0] || "NATIVE_APP",
isXXXAPP: userAgent.includes("NATIVE_APP"),
AppVersion: appVersionResult ? appVersionResult[2] : "",
IOS: !!userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
Andriod: userAgent.includes("Android") || userAgent.includes("Adr"),
version: matchResult?.[1] || "1.0.0",
system: isAndroid ? "android" : "ios",
IsProd: IsProd,
isAppWebview: !!matchResult?.length,
};
export default appInfo;事件监听
ts
const events: {
[key: string]: Array<any>;
} = {};
/**
* 绑定事件
* @param name
* @param self
* @param callback
*/
function on(name: string, self: any = null, callback: Function) {
const tuple = [self, callback];
const callbacks = events[name];
if (Array.isArray(callbacks)) {
callbacks.push(tuple);
} else {
events[name] = [tuple];
}
}
function _remove(name: string, self: any = null) {
const callbacks = events[name];
if (Array.isArray(callbacks)) {
if (!self) {
events[name] = [];
} else {
events[name] = callbacks.filter((tuple) => tuple[0] !== self);
}
}
}
/**
* 移除事件
* @param name
* @param self
*/
function remove(name: string, self: any = null) {
if (!name) {
Object.keys(events).forEach((_name) => {
_remove(_name, self);
});
} else {
_remove(name, self);
}
}
/**
* 触发事件
* @param name
* @param data
*/
function emit(name: string, data = {}) {
const callbacks = events[name];
if (Array.isArray(callbacks)) {
callbacks.map((tuple) => {
const self = tuple[0];
const callback = tuple[1];
callback.call(self, name, data);
});
}
}
const BACKED_SHOW = "onBackedShow"; // webview离开
const LEAVE = "onLeave"; // webview返回
const KEYBOARD_FRAME_CHANGED = "onKeyboardChangeFrame"; // 键盘变化
const STATUS_BAR_TAPPED = "onStatusBarTapped"; // 点击状态栏,目前只有ios, deprecated,直接使用 js方法 setScrollsToTop
const PULL_TO_REFRESH = "onPullToRefresh"; // 下拉刷新
export default {
on,
remove,
emit,
types: {
BACKED_SHOW,
LEAVE,
KEYBOARD_FRAME_CHANGED,
STATUS_BAR_TAPPED,
PULL_TO_REFRESH,
},
};方法汇总
ts
import EventBus from "./event-bus";
import {
osProxy,
iosFactoryFn,
createCallBack,
encodeData,
isClient,
} from "./utils";
const VOIDFN = (res: any) => {}; // 默认空函数
const VOID_EVENT_FN = (event: string, data: any) => {}; // 事件空函数
const ANDROIDPREFIX: any = isClient ? (window as any).toolbox || {} : {};
const customWindow: any = isClient ? (window as any) : {};
interface CommOptions {
[propName: string]: any;
}
/**
* 分享类型
* wechat_friend-微信好友
* wechat_moment-朋友圈
* qq-qq好友
* qqzone-qq空间
* sinaweibo-新浪微博
*/
type ShareType =
| "wechat_friend"
| "wechat_moment"
| "qq"
| "qqzone"
| "sinaweibo";
type SingleImg = {
base64?: string; // 安卓base64
method?: string; // ios base64取方法名称
};
interface ShareOptions extends CommOptions {
copy_writing?: string; // 原生底部的title
share_string?: string; // 一级标题
share_desc?: string; // 二级标题
share_url?: string; // 分享地址
share_img_url?: string; // 分享图片
share_type?: ShareType; // 分享类型
single_img?: SingleImg; // base64图片
}
class NativeApi {
/**
* 分享
* @param shareOptions
* @param callback
* @description 单图分享只需要传 single_img | SingleImg
*/
doShare(shareOptions: ShareOptions, callback = VOIDFN) {
const callBackName = createCallBack(callback);
osProxy({
android: () => {
if (shareOptions.single_img) {
delete shareOptions.single_img.method;
}
ANDROIDPREFIX.notifyAndroid_doShare(
JSON.stringify(shareOptions),
callBackName,
);
},
ios: () => {
if (shareOptions.single_img) {
delete shareOptions.single_img.base64;
}
iosFactoryFn(
`share¶m=${encodeData(shareOptions)}&callback=${callBackName}`,
);
},
});
}
/**
* 绑定事件
* @param event 事件名
* @param func 回调
* @param target 事件目标,默认为当前 nativeApi 实例
*/
addEventListener(event: string, func = VOID_EVENT_FN, target: any = this) {
let sFunc = func;
EventBus.on(event, target, sFunc);
}
/**
* 移除事件
* @param event 事件名
* @param target 事件目标,默认为当前 nativeApi 实例
*/
removeEventListener(event: string, target: any = this) {
EventBus.remove(event, target);
}
/**
* 触发事件
* @param event 事件名
* @param data 事件参数
*/
raiseEvent(event: string, data: any = {}) {
EventBus.emit(event, data);
}
}
const instance = new NativeApi();
// 提供类供app调用
customWindow.NativeApi = function () {
return instance;
};
customWindow.NativeApi = instance;
export const Events = EventBus.types;
export default instance;公共方法
ts
import appInfo from "./appInfo";
export const isClient = typeof window !== "undefined";
export const IOSPREFIX: string = "http://callclient?method=";
const ANDROIDPREFIX: any = isClient ? (window as any).toolbox : {};
interface ProxyF {
android: Function;
ios: Function;
}
interface CallbackOption {
count: number; // 回调次数
}
export interface CallbackFunction extends Function {
(res?: any, option?: CallbackOption): void;
}
export function versionComparison(appVersion: string) {
if (appInfo.AppVersion) {
const v1 = appInfo.AppVersion;
const v2 = appVersion;
const arr1 = v1.split(".");
const arr2 = v2.split(".");
const len = Math.max(arr1.length, arr2.length);
while (arr1.length < len) {
arr1.push("0");
}
while (arr2.length < len) {
arr2.push("0");
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(arr1[i]);
const num2 = parseInt(arr2[i]);
if (num1 < num2) {
return false;
} else if (num1 > num2) {
return true;
}
}
return true;
} else {
return false;
}
}
/**
* 代理,根据不同设备执行不同方法
* @param f
*/
export function osProxy(
f: ProxyF,
version: string = "1.0.0",
callback?: Function,
) {
let appVersion = "";
let versionMatch = false;
if (appInfo.AppVersion) {
versionMatch = versionComparison(appVersion);
}
if (versionMatch) {
if (appInfo.Andriod && f.android) {
return f.android();
}
if (appInfo.IOS && f.ios) {
return f.ios();
}
} else {
callback &&
callback({
success: false,
data: {
message: "暂不支持",
},
});
return;
}
}
/**
* ios 工厂函数
* @param url
*/
export function iosFactoryFn(url: string): void {
const _window: any = window;
const messageHandlers =
_window.webkit && _window.webkit.messageHandlers
? _window.webkit.messageHandlers
: {};
const nativeApi = messageHandlers.nativeApi || null;
if (nativeApi && nativeApi.postMessage) {
// 新版通过 postMessage 通信,优化性能
const arr = url.split("&");
const method: string = arr.shift() as string;
let params = {};
let callback = "";
for (const item of arr) {
const arr1 = item.split("=");
const key = arr1[0];
const value = arr1[1] || "";
if (key === "callback") {
callback = value;
} else if (key === "param") {
const val = decodeURIComponent(value);
params = val[0] === "{" ? JSON.parse(val) : val;
}
}
nativeApi.postMessage({
method,
params,
callback,
});
} else {
const iframeDom: HTMLIFrameElement = document.createElement("iframe");
iframeDom.src = `${IOSPREFIX}${url}`;
iframeDom.style.display = "none";
document.documentElement.appendChild(iframeDom);
setTimeout(() => {
document.documentElement.removeChild(iframeDom);
}, 0);
}
}
/**
* 发送消息到ios
* @param method
* @param paramStr
* @param callback
*/
export function postMessageToIOS(
method: string,
paramStr: string = "",
callback: string = "",
) {
iosFactoryFn(
`${method}¶m=${encodeURIComponent(paramStr)}&callback=${callback}`,
);
}
/**
* 发送消息到ios
* @param method
* @param paramStr
* @param callback
*/
export function postMessageToAndroid(
method: string,
paramStr: string = "",
callback: string = "",
) {
ANDROIDPREFIX[`notifyAndroid_${method}`](paramStr, callback);
}
/**
* 本地回调函数
* @param res
*/
export function formatNativeParam(res: any) {
const data = osProxy({
android: () => {
if (typeof res === "object") {
return res;
}
if (!res) {
return { success: true };
}
if (res.indexOf("{") === 0) {
try {
return JSON.parse(res);
} catch (err) {
return { success: true, data: res };
}
}
return { success: true, data: res };
},
ios: () => {
if (res && res.hasOwnProperty("success")) {
if (res.success === "1") {
res.success = true;
} else if (res.success === "0") {
res.success = false;
} else {
res.success = !!res.success;
}
}
return res;
},
});
return data;
}
/**
* 生成随机回调函数
* @param fn 回调函数,第二个参数为系统参数 count:当前是第几次回调
* @param callbackNum 最大回调次数,某些特定场景可以指定多次回调
*/
export function createCallBack(
fn: CallbackFunction,
maxCallbackNum: number = 1,
): string {
const mathRandom = Math.ceil(Math.random() * 1000);
const callBackName = `nativeCallBack${new Date().getTime()}${mathRandom}`;
const _window: any = window;
if (!_window.nativeCallback) {
_window.nativeCallback = {};
}
_window.nativeCallback[callBackName] = {
max: maxCallbackNum,
current: 0,
};
_window[callBackName] = (res: any) => {
_window.nativeCallback[callBackName].current++;
fn(formatNativeParam(res), {
count: _window.nativeCallback[callBackName].current,
});
if (
_window.nativeCallback[callBackName].current >=
_window.nativeCallback[callBackName].max
) {
removeCallback(callBackName);
}
};
return callBackName;
}
/**
* 清除回调函数
* @param callBackName
*/
export function removeCallback(callBackName: string) {
const _window: any = window;
if (_window.nativeCallback) {
delete _window.nativeCallback[callBackName];
}
delete _window[callBackName];
}
/**
* encodeURIComponent 数据
* @param data
*/
export function encodeData(data: object): string {
return encodeURIComponent(JSON.stringify(data));
}
/**
* 判定数组所在值
* @param {val} number
* @param {arr} Array
*/
export function inArray(val: number, arr: any[]): number {
for (let x = 0, len = arr.length; x < len; x++) {
if (arr[x] == val) {
return x;
}
}
return -1;
}这样就可以规避掉以上的所存在的问题了。
原本想封装一个NPM包,但适用性并不高,仍然还是需要手动改造,因此建了一个模板,放在了git上,有需要的可以自取。 传送门