混合app开发:js-native-bridge进行原生 iOS,Android 客户端的交互
iOS 端支持最低 iOS7 以上的设备,但是 demo 中的 js 因为使用 es6 语法,所以 iOS10 以下会出现语法错误,请使用 Babel 库来做兼容。
Android 端支持最低 sdk19 4.4 以上设备,测试过 Android 7.0 的设备没问题,如果出现低版本不兼容 es6 问题,同样使用 Babel 库来做下兼容。
运行 demo:
Mac 电脑自带 web 服务器,将 js 项目拖入 /Library/WebServer/Documents 目录下,使用终端敲击如下命令 sudo apachectl start 便起来一个 web
服务,浏览器输入 http://127.0.0.1 便能访问 webServer 的 Documents 目录,iOS,Android Demo 的 WebView 访问 js demo 下 index.html 文件,iOS,Android demo 分别使用 Xcode 和 Android Studio 运行。
基础用法
iOS,Android 客户端的混合开发,避免不了 js 和 native 之间的交互,一些常用的 js-bridge 库实现都是只支持一种系统。
js 调用 native 端的接口要简单,且一个函数就能调用 iOS 和 Android 两个系统,并且尽量模块化,在存在大量 native 接口的情况下便于维护这些函数。
使用方法,以 js 端调用系统的相机或相册获取一张图片为例,其它功能大同小异。
js 端的调用代码如下:
// index.html <script type="module"> // 导入对应的 native 功能模块,其中核心模块 native-core.js 必须导入 import NCore from './native-bridge/native-core.js' import NKit from './native-bridge/native-kit.js' var nCore = NCore() var nKit = NKit(nCore) ... var self = this nKit.selectPhoto(function (photo) { // 图片为 base64 数据 self.imageBytes = photo.image return '获取图片成功' }) ... </script>
selectPhoto 方法的定义如下:
// native-kit.js // 导入 native-core 核心模块 export default core => { return { selectPhoto (picker) { // 全局记录回调函数 this.selectPhoto.picker = picker core.loadWidget('kit', this) // NativeKit 是 native 端注册的全局对象,camera 是对应的方法名,如此就能调用到原生客户端的方法 core.evaluateNative('NativeKit', 'camera', function (photo) { // 调用之前的回调函数 return $nativeBridgeWidget.kit.selectPhoto.picker(photo) }) } } }
native 系统相关的接口可以定义到 native-kit.js 中,或者模块分的粒度更细。
iOS 端使用 JavaScriptCore 实现交互,如何获取 JSContext 等不赘述,参考 iOS demo 即可。
先定义 JSExport 协议:
// HCKitJSExport.h @protocol HCKitJSExport <JSExport> // camera 即为 js 端调用的方法别名 JSExportAs(camera, - (void)cameraWithResult:(JSValue *)result ); @end
实现该协议:
头文件
// HCKitJSExportImpl.h @interface HCKitJSExportImpl : NSObject <HCKitJSExport> + (instancetype)instance:(HCJSCoreBaseViewController *)vcContext; @end
实现文件:
@interface HCKitJSExportImpl ()<UIImagePickerControllerDelegate, UINavigationControllerDelegate> @property (nonatomic, weak) HCJSCoreBaseViewController *vcContext; @property (nonatomic, strong) JSValue *imageValue; @end @implementation HCKitJSExportImpl + (instancetype)instance:(HCJSCoreBaseViewController *)vcContext { HCKitJSExportImpl *impl = [HCKitJSExportImpl new]; impl.vcContext = vcContext; return impl; } - (void)cameraWithResult:(JSValue *)result { // 保障 oc 调 js 的回调函数和 js 在同一线程 self.vcContext.jsThread = [NSThread currentThread]; // result 该 JSValue 即为 js 的回调函数 _imageValue = result; // ui 在主线程 dispatch_async(dispatch_get_main_queue(), ^{ UIImagePickerController * imagePicker = [[UIImagePickerController alloc] init]; imagePicker.editing = YES; imagePicker.delegate = self; imagePicker.allowsEditing = YES; UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"请选择打开方式" message:nil preferredStyle:UIAlertControllerStyleActionSheet]; UIAlertAction * camera = [UIAlertAction actionWithTitle:@"相机" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; imagePicker.modalPresentationStyle = UIModalPresentationFullScreen; imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto; [self.vcContext presentViewController:imagePicker animated:YES completion:nil]; }]; UIAlertAction * photo = [UIAlertAction actionWithTitle:@"相册" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; [self.vcContext presentViewController:imagePicker animated:YES completion:nil]; }]; UIAlertAction * cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [self.vcContext dismissViewControllerAnimated:YES completion:nil]; }]; [alert addAction:camera]; [alert addAction:photo]; [alert addAction:cancel]; [self.vcContext presentViewController:alert animated:YES completion:nil]; }); } #pragma mark - imagePickerController delegate - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info { [picker dismissViewControllerAnimated:YES completion:nil]; UIImage * image = [info valueForKey:UIImagePickerControllerEditedImage]; NSData *imageData = UIImagePNGRepresentation(image); NSString *imageBase64 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; NSDictionary *dict = @{@"image": imageBase64}; if (self.imageValue) { [self.vcContext executeJSValueThreadSafe:self.imageValue args:@[dict]]; } } @end
Android 端使用的是 JavaScriptInterface 实现的交互。
实现类如下
public class NativeKitJSImpl { private static final String TAG = "NativeKitJSImpl"; private MainActivity mActivity; public NativeKitJSImpl(MainActivity activity) { this.mActivity = activity; } @JavascriptInterface public void camera(final String picker) { mActivity.tempCallback = picker; new AlertDialog.Builder(mActivity) .setTitle("提示") .setMessage("选择相机或者相册") .setPositiveButton("相机", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { mActivity.takePhoto(new ObtainPhoto() { @Override public void getPhotoBase64(String image) { final JSONObject jsonObject = new JSONObject(); try { jsonObject.put("image", image); JsInterfaceUtils.evaluateJs(mActivity.mMainWebView, picker, new ValueCallback<String>() { @Override public void onReceiveValue(String s) { Log.d(TAG, s); } }, jsonObject); } catch (JSONException e) { e.printStackTrace(); } } }); } }) .setNegativeButton("相册", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { mActivity.selectPhoto(new ObtainPhoto() { @Override public void getPhotoBase64(String image) { final JSONObject jsonObject = new JSONObject(); try { jsonObject.put("image", image); JsInterfaceUtils.evaluateJs(mActivity.mMainWebView, picker, new ValueCallback<String>() { @Override public void onReceiveValue(String s) { Log.d(TAG, s); } }, jsonObject); } catch (JSONException e) { e.printStackTrace(); } } }); } }).show(); } }
在 MainActivity 中添加此 JavaScriptInterface :
mMainWebView.addJavascriptInterface(new NativeKitJSImpl(this), "NativeKit");
如此就实现 js 与 native 端(iOS,Android)的交互。
NativeKitJSImpl 类中,可以不引用具体的 Activity,如,MainActivity 。这样耦合比较紧,可以引用接口,接口中定义要调用的方法,这样只要我的 Activity 实现了接口方法,就可以传入 jsImpl 类中了。
如这样定义接口:
// 定义基础接口 public interface NativeBaseInterface { WebView getMainWebView(); // 提供 Activity 上下文 AppCompatActivity getActivityContext(); } public interface NativeSelectPhotoInterface extends NativeBaseInterface { // 拍照获取图片 public void takePhoto(ObtainPhoto obtainPhoto); // 相册获取图片 public void selectPhoto(ObtainPhoto obtainPhoto); }
NativeKitJSImpl 类中引入上下文环境,使用 WeakReference 避免循环引用。
如:
private NativeSelectPhotoInterface mActivity; public BotsNativeKitJSImpl(WeakReference<NativeSelectPhotoInterface> weakReference) { this.mActivity = weakReference.get(); }
native 调用 js
原生调用的 js 方法,需要 js 端将被调用的函数注册进来。
var test = function (param) { self.$nativeUi.alert('test js', JSON.stringify(param), function affirm () { console.log('点击了确认ok') }, function cancel () { console.log('点击了取消cancel') }) return 'finished' } // 调用 core 核心模块的 registerJs 函数,test 是要被原生调用的函数 this.$nativeCore.registerJs('testJs', test)
iOS 端调用 js 函数的示例:
- (IBAction)testJs:(id)sender { NSDictionary *dict = @{@"foo":@"hello", @"bar":@YES}; JSValue *value = [self callJsBridge:@"testJs" args:@[dict]]; NSLog(@"测试返回值:%@", [value toString]); } - (JSValue *)callJsBridge:(NSString *)methodName args:(NSArray *)args { JSValue * jsBridge = self.appJSContext[@"$jsBridge"]; JSValue *jsFunction = [jsBridge valueForProperty:methodName]; return [jsFunction callWithArguments:args]; }
Android 端调用 js函数的示例:
JSONObject jsonObject = new JSONObject(); try { jsonObject.put("bar", "hello"); jsonObject.put("foo", true); String script = "$jsBridge.testJs"; JsInterfaceUtils.evaluateJs(mMainWebView, script, new ValueCallback<String>() { @Override public void onReceiveValue(String s) { Log.d(TAG, s); } }, jsonObject); } catch (JSONException exception) { exception.printStackTrace(); }
vue 插件
实现 Vue 插件,在 Vue 框架中使用更加方便。
插件实现如下:
// native-vue.js import NCore from './native-bridge/native-core.js' import NUI from './native-bridge/native-ui.js' import NStore from './native-bridge/native-store.js' import NKit from './native-bridge/native-kit.js' import NRequest from './native-bridge/native-request.js' var jsBridge = {} jsBridge.install = function (Vue, options) { var nCore = NCore() var nUi = NUI(nCore) var nStore = NStore(nCore) var nKit = NKit(nCore) var nRequest = NRequest(nCore) Vue.prototype.$nativeCore = nCore Vue.prototype.$nativeUi = nUi Vue.prototype.$nativeStore = nStore Vue.prototype.$nativeKit = nKit Vue.prototype.$nativeRequest = nRequest } export default jsBridge;
使用插件:
// 使用前引入插件 import nativeVue from './native-vue.js' Vue.use(nativeVue); var self = this var params = {'id':2, 'pageNum':3, 'pageSize':10, 'keyword':'xx'} this.$nativeRequest.get('https://api.github.com/', params, function success(response) { console.log(response) self.resultMsg = response }, function fail(error) { console.log(error) })