《成为大前端》系列 5.3 JSBridge模块化 - KVDB/Camera/Image模块

597 阅读6分钟

前面我们已经完成了模块化的重构和优化,这一篇我们利用这点快速编写新的模块

  • KVDB: 本地KV数据库
  • Camera: 调用摄像头
  • Image: 调用系统图片

KVDB模块Android篇

KVDB模块由Native提供比LocalStorage更稳定的本地存储,暂时提供三种数据类型存储的实现:

  • Int
  • Bool
  • String

Native

Android将使用SharedPreferences作为底层,具体也可以根据App的情况替换成其他数据库等等

新建JSBridgeKVDB.kt文件,代码如下:

class JSBridgeKVDB(val activity: WebActivity, webView: WebView) : BridgeModuleBase(webView) {

    val preferences = activity.getSharedPreferences("KVDB", Context.MODE_PRIVATE)

    override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
        when (func) {
            "getInt" -> {
                callback(callbackId, getInt(arg.getString("key")))
            }
            "setInt" -> {
                setInt(arg.getString("key"), arg.getInt("value"))
            }
            "getBool" -> {
                callback(callbackId, getBool(arg.getString("key")))
            }
            "setBool" -> {
                setBool(arg.getString("key"), arg.getBoolean("value"))
            }
            "getString" -> {
                callback(callbackId, arg.getString(arg.getString("key")))
            }
            "setString" -> {
                setString(arg.getString("key"), arg.getString("value"))
            }
        }
    }

    fun getInt(key: String): Int {
        return preferences.getInt(key, 0)
    }

    fun setInt(key: String, value: Int) {
        preferences.edit().putInt(key, value).apply()
    }

    fun getBool(key: String): Boolean {
        return preferences.getBoolean(key, false)
    }

    fun setBool(key: String, value: Boolean) {
        preferences.edit().putBoolean(key, value).apply()
    }

    fun getString(key: String): String? {
        return preferences.getString(key, null)
    }

    fun setString(key: String, value: String?) {
        preferences.edit().putString(key, value).apply()
    }

}

BridgeObject里增加JSBridgeKVDB的初始化:

init {
    bridgeModuleMap["UI"] = JSBridgeUI(activity, webView)
    bridgeModuleMap["KVDB"] = JSBridgeKVDB(activity, webView)
}

KVDB模块iOS篇

KVDB模块由Native提供比LocalStorage更稳定的本地存储,暂时提供三种数据类型存储的实现:

  • Int
  • Bool
  • String

Native

iOS将使用UserDefaults作为底层,具体也可以根据App的情况替换成其他数据库等等

新建JSBridgeKVDB.swift文件,代码如下:

class JSBridgeKVDB : BridgeModuelBase {
    
    weak var viewController: WebViewController?
    
    var userDefaults = UserDefaults(suiteName: "KVDB")!
    
    init(viewController: WebViewController) {
        self.viewController = viewController
    }
    
    override func callFunc(_ funcName: String, callbackId: String, arg: [String : Any?]) {
        let key = arg["key"] as! String
        switch funcName {
        case "getInt":
            callback(callbackId: callbackId, value: getInt(key: key))
        case "setInt":
            setInt(key: key, value: arg["value"] as? Int ?? 0)
        case "getBool":
            callback(callbackId: callbackId, value: getBool(key: key))
        case "setBool":
            setBool(key: key, value: arg["value"] as? Bool ?? false)
        case "getString":
            callback(callbackId: callbackId, value: getString(key: key))
        case "setString":
            setString(key: key, value: arg["value"] as? String)
        default: break
        }
    }
    
    func getInt(key: String) -> Int {
        return userDefaults.integer(forKey: key)
    }
    
    func setInt(key: String, value: Int) {
        userDefaults.set(value, forKey: key)
    }
    
    func getBool(key: String) -> Bool {
        return userDefaults.bool(forKey: key)
    }
    
    func setBool(key: String, value: Bool) {
        userDefaults.set(value, forKey: key)
    }
    
    func getString(key: String) -> String? {
        return userDefaults.string(forKey: key)
    }
    
    func setString(key: String, value: String?) {
        userDefaults.set(value, forKey: key)
    }
}

BridgeHandler的initModules加上KVDB模块的初始化:

func initModules() {
    moduleDict["UI"] = JSBridgeUI(viewController: viewController!)
    moduleDict["KVDB"] = JSBridgeKVDB(viewController: viewController!)
}

KVDB模块JS篇

jsbridge.js

针对js的特性,我们可以利用JSON.stringify/JSON.parse,提供比原生多一种数据类型json

JSBridge.KVDB = {}
JSBridge.KVDB.getInt = function(key, callback) {
    callNative("KVDB.getInt", { key: key }, callback);
}
JSBridge.KVDB.setInt = function(key, value) {
        callNative("KVDB.setInt", { key: key, value: value });
}
JSBridge.KVDB.getBool = function(key, callback) {
    callNative("KVDB.getBool", { key: key }, callback);
}
JSBridge.KVDB.setBool = function(key, value) {
    callNative("KVDB.setBool", { key: key, value: value });
}
JSBridge.KVDB.getString = function(key, callback) {
    callNative("KVDB.getString", { key: key }, callback);
}
JSBridge.KVDB.setString = function(key, value) {
    callNative("KVDB.setString", { key: key, value: value });
}
JSBridge.KVDB.getJSON = function(key, callback) {
    callNative("KVDB.getString", { key: key }, function(result){ 
        callback(JSON.parse(result)) 
    })
}
JSBridge.KVDB.setJSON = function(key, value) {
    callNative("KVDB.setString", { key: key, value: JSON.stringify(value) });
}

index.html

<button onclick="onClickButton(this)">KVDB.getInt</button>
<button onclick="onClickButton(this)">KVDB.setInt</button>
<button onclick="onClickButton(this)">KVDB.getBool</button>
<button onclick="onClickButton(this)">KVDB.setBool</button>
<button onclick="onClickButton(this)">KVDB.getString</button>
<button onclick="onClickButton(this)">KVDB.setString</button>
<button onclick="onClickButton(this)">KVDB.getJSON</button>
<button onclick="onClickButton(this)">KVDB.setJSON</button>
case "KVDB.getInt":
    JSBridge.KVDB.getInt("int", (result) => {
        JSBridge.UI.toast("getInt:" + result)
    })
    break
case "KVDB.setInt":
    JSBridge.KVDB.setInt("int", parseInt(Date.now() / 1000))
    JSBridge.UI.toast('setInt')
    break
case "KVDB.getBool":
    JSBridge.KVDB.getBool("bool", (result) => {
        JSBridge.UI.toast("getBool: " + result)
    })
    break
case "KVDB.setBool":
    JSBridge.KVDB.setBool("bool", Date.now() % 2 === 0)
    JSBridge.UI.toast('setBool')
    break
case "KVDB.getString":
    JSBridge.KVDB.getString("string", (result) => {
        JSBridge.UI.toast("getString: " + result)
    })
    break
case "KVDB.setString":
    JSBridge.KVDB.setString("string", "string_value_" + Date.now())
    JSBridge.UI.toast('setString')
    break
case "KVDB.getJSON":
    JSBridge.KVDB.getJSON("json", (result) => {
        JSBridge.UI.toast("getJSON: " + JSON.stringify(result, null, '  '))
    })
    break
case "KVDB.setJSON":
    JSBridge.KVDB.setJSON("json", { title: 'This is json', message: 'time is ' + new Date().toUTCString() })
    JSBridge.UI.toast('setJSON')
    break

结合Android和iOS,自己运行看看效果,这里就不截图了。

Camera模块Android篇

这篇使用到了Native的知识,来源:

AndroidManifest.xml

manifest配置里增加使用相机的请求项

<manifest ... >
    <uses-feature android:name="android.hardware.camera"
                    android:required="true" />
    ...
</manifest>

WebActivity.kt

由于我们要调用系统相机,我们必须使用activity的Intent来唤起相机应用,在activity上 需要做如下的代码编写,代码设计了callback方式的调用,提供给bridge模块使用

abstract class WebActivity : AppCompatActivity() {

    private var activityResultCallback: ActivityResultCallback? = null

    ...

    fun startActivityForCallback(intent: Intent, requestCode: Int, callback: ActivityResultCallback) {
        activityResultCallback = callback
        startActivityForResult(intent, requestCode)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        activityResultCallback?.let {
            if (it.invoke(requestCode, resultCode, data)) {
                activityResultCallback = null
            }
        }
    }
}

JSBridgeCamera.kt

class JSBridgeCamera(val activity: WebActivity, webView: WebView) : BridgeModuleBase(webView) {

    override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
        when (func) {
            "takePicture" -> takePicture(callbackId, arg)
        }
    }

    private fun takePicture(callbackId: String, arg: JSONObject) {
        ...
    }
}

WebViewBridge.kt

bridgeModuleMap["Camera"] = JSBridgeCamera(activity, webView)

takePicture代码

private val REQ_CODE = 200

private fun takePicture(callbackId: String, arg: JSONObject) {

    // 创建去相机的意图(Intent)
    val takePhotoIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    // 检查系统是否支持相机
    if (!takePhotoIntent.resolveActivity(activity.packageManager) != null) {
        
    } else {
        // 调用前面定义的callback方式打开摄像机
        activity.startActivityForCallback(takePhotoIntent, REQ_CODE) { requestCode: Int, resultCode: Int, result: Intent? ->
            // 不是对应的requestCode
            if (REQ_CODE != requestCode) {
                return@startActivityForCallback false
            }

            val json = JSONObject()

            if (resultCode == RESULT_OK) {
                json.put("success", true)
                val bitmap = result?.extras?.get("data") as Bitmap
                // TODO 存文件,返回路径(后面会讲到)
                
            } else {
                json.put("success", false)
                json.put("canceled", resultCode == RESULT_CANCELED)
                json.put("error", "Result code is $resultCode")
            }

            callback(callbackId, json)
            true
        }
    }
}

扩展阅读:

Android Intent: developer.android.google.cn/guide/compo…

存文件

首先我们声明了两个成员变量:

  • appCacheDir: 应用的缓存路径,使用了packageName防止名称冲突
  • cameraCacheDir:appCacheDir的子目录,作为业务区分
private val appCacheDir = File(activity.cacheDir, activity.application.packageName)
private val cameraCacheDir = File(appCacheDir, "Camera")

然后将文件存到cameraCacheDir下:

// 获取拍照的bitmap
val bitmap = result?.extras?.get("data") as Bitmap
// 使用时间戳创建一个文件
val cacheFile = File(cameraCacheDir, "takePicture_${System.currentTimeMillis()}.jpg")
// 下面是将图片写入文件
var fos: FileOutputStream? = null
try {
    fos = FileOutputStream(cacheFile)
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
} catch (error: Throwable) {
    // 出现了IO异常
    json.put("success", false)
    json.put("error", error.localizedMessage)
    return@startActivityForCallback true
} finally {
    fos?.close()
}
json.put("success", true)
// 增加文件路径返回给js端
json.put("file", cacheFile.absolutePath)

到这里就完成的takePicture部分

扩展阅读:

takeVideo

takeVideo和takePicture非常类似,只是相机返回的数据是视频,并且更加简单

private fun takeVideo(callbackId: String, arg: JSONObject) {
    // 创建去相机的意图(Intent),ACTION_VIDEO_CAPTURE(拍视频)
    val takePhotoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
    // 检查系统是否支持相机
    if (takePhotoIntent.resolveActivity(activity.packageManager) == null) {
        // 不支持返回不成功
        val json = JSONObject()
        json.put("success", false)
        json.put("error", "This device is not support camera.")
        callback(callbackId, json)
    } else {
        // 调用前面定义的callback方式打开摄像机
        activity.startActivityForCallback(takePhotoIntent, REQ_CODE) { requestCode: Int, resultCode: Int, result: Intent? ->
            // 不是对应的requestCode
            if (REQ_CODE != requestCode) {
                return@startActivityForCallback false
            }


            val json = JSONObject()

            if (resultCode == RESULT_OK) {
                // 获取视频地址,这里是和takePicture不同的地方
                val videoUri = result?.data
                json.put("success", true)
                json.put("file", videoUri.toString())
            } else {
                json.put("success", false)
                json.put("canceled", resultCode == RESULT_CANCELED)
                json.put("error", "Result code is $resultCode")
            }

            callback(callbackId, json)
            true
        }
    }
}
override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
    when (func) {
        "takePicture" -> takePicture(callbackId, arg)
        "takeVideo" -> takeVideo(callbackId, arg) // 注意加上这一行
    }
}

到这里就完成了takeVideo

Camera模块iOS篇

JSBridgeCamera.swift

添加JSBridgeCamera.swift文件,初始代码:

class JSBridgeCamera : BridgeModuelBase {

    weak var viewController: WebViewController?

    init(viewController: WebViewController) {
        self.viewController = viewController
    }

    override func callFunc(_ funcName: String, callbackId: String, arg: [String : Any?]) {
        switch funcName {
        case "takePicture": takePicture(callbackId: callbackId, arg: arg)
        case "takeVideo": takeVideo(callbackId: callbackId, arg: arg)
        default: break
        }
    }
    
    private func takePicture(callbackId: String, arg: [String : Any?]) {
      ...
    }
    
    private func takeVideo(callbackId: String, arg: [String : Any?]) {
      ...
    }
}

WebViewBridge.swift

func initModules() {
    moduleDict["UI"] = JSBridgeUI(viewController: viewController!)
    moduleDict["KVDB"] = JSBridgeKVDB(viewController: viewController!)
    // 添加Camera模块
    moduleDict["Camera"] = JSBridgeCamera(viewController: viewController!)
}

takePicture

拍照和录像都需要使用到系统提供的UIImagePickerController,使用很简单,如下:

private var cameraDelegate: CameraDelegate?

...

private func takePicture(callbackId: String, arg: [String : Any?]) {
    guard let vc = self.viewController else { return }
    // 
    if !UIImagePickerController.isSourceTypeAvailable(.camera) {
        callback(callbackId: callbackId, json: [
            "success": false,
            "error": "This device is not support camera"
        ])
        return
    }
    let cameraPicker = UIImagePickerController()
    // 配置为拍照图片
    cameraPicker.allowsEditing = false
    cameraPicker.sourceType = .camera
    cameraPicker.cameraCaptureMode = .photo
    
    // 创建delegate
    cameraDelegate = CameraDelegate()
    cameraDelegate?.jsbridgeCamera = self
    cameraDelegate?.callbackId = callbackId
    cameraPicker.delegate = cameraDelegate!
    
    // 打开拍照
    vc.present(cameraPicker, animated: true, completion: nil)
}

上面代码创建了delegate,这个delegate实现了UIImagePickerControllerDelegateUINavigationControllerDelegate 代码如下:

fileprivate class CameraDelegate : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    // 定义这两个属性,从JSBridgeCamera里传过来的,用于调用callback
    weak var jsbridgeCamera: JSBridgeCamera?
    var callbackId: String!
    
    // 实现这个protocol方法,用于拍照完成后的回调处理
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)
        
        // TODO 图片存入缓存文件

        let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": true
        ])
    }
    
    // 实现这个protocol方法,用于用户取消时的操作
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": false,
            "canceled": true
        ])
    }
    
}

前面还没有完成全部代码,但可以现在先运行看看结果。

运行后会在XCode看到如下错误提示

This app has crashed because it attempted to access privacy-sensitive data without 
a usage description.  The app's Info.plist must contain an NSCameraUsageDescription 
key with a string value explaining to the user how the app uses this data.

需要在Info.plist添加一行信息,用于申请权限时系统显示的文案,这里不细说。

完成上面的操作再运行,拍照流程应该是通的,然后继续后面的步骤。

图片存入缓存

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    picker.dismiss(animated: true, completion: nil)

    // 获取拍照的图片
    let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
    
    // 创建临时文件
    let tempDir = NSTemporaryDirectory() as NSString
    let appTempDir = tempDir.appendingPathComponent(Bundle.main.bundleIdentifier!) as NSString
    let cacheDir = appTempDir.appendingPathComponent("Camera") as NSString
    let filePath = cacheDir.appendingPathComponent("Camera_\(Int64(Date().timeIntervalSince1970)).jpg")
    let fileMgr = FileManager.default
    
    // 确定目录存在
    var isDir: ObjCBool = false
    if !fileMgr.fileExists(atPath: cacheDir as String, isDirectory: &isDir) {
        try? fileMgr.createDirectory(atPath: cacheDir as String, withIntermediateDirectories: true, attributes: nil)
    }
    
    // 转换成jpeg data
    guard let data = image.jpegData(compressionQuality: 1) else {
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": false,
            "error": "Image compress fail"
        ])
        return
    }
    
    // 写文件
    do {
        let fileURL = URL(fileURLWithPath: filePath)
        try data.write(to: fileURL)
        // callback给js
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": true,
            "file": fileURL.absoluteString
        ])
    } catch {
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": false,
            "error": error.localizedDescription
        ])
        return
    }
}

到这里就完成的takePicture部分

takeVideo

takeVideo和takePicture非常类似,只是相机返回的数据是视频,并且更加简单

private func takeVideo(callbackId: String, arg: [String : Any?]) {
    guard let vc = self.viewController else { return }
    if !UIImagePickerController.isSourceTypeAvailable(.camera) {
        callback(callbackId: callbackId, json: [
            "success": false,
            "error": "This device is not support camera"
        ])
        return
    }
    let cameraPicker = UIImagePickerController()
    cameraPicker.sourceType = .camera
    cameraPicker.cameraDevice = .rear
    cameraPicker.videoQuality = .typeHigh
    cameraPicker.mediaTypes = [kUTTypeMovie as String]
    cameraPicker.videoMaximumDuration = 60
    cameraDelegate = CameraDelegate()
    cameraDelegate?.jsbridgeCamera = self
    cameraDelegate?.callbackId = callbackId
    cameraPicker.delegate = cameraDelegate!
    vc.present(cameraPicker, animated: true, completion: nil)
}

由于拍照和录视频都回调的统一的方法,因此CameraDelegate类里需要作出判断,代码如下:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    jsbridgeCamera?.cameraDelegate = nil
    picker.dismiss(animated: true, completion: nil)
    
    // 判断是视频资源
    if let url = info[.mediaURL] as? URL {
        // callback给js
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": true,
            "file": url.absoluteString
        ])
    }
    // 判断是图片资源
    else if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
        // 创建临时文件
        let tempDir = NSTemporaryDirectory() as NSString
        let appTempDir = tempDir.appendingPathComponent(Bundle.main.bundleIdentifier!) as NSString
        let cacheDir = appTempDir.appendingPathComponent("Camera") as NSString
        let filePath = cacheDir.appendingPathComponent("Camera_\(Int64(Date().timeIntervalSince1970)).jpg")
        let fileMgr = FileManager.default
        
        // 确定目录存在
        var isDir: ObjCBool = false
        if !fileMgr.fileExists(atPath: cacheDir as String, isDirectory: &isDir) {
            try? fileMgr.createDirectory(atPath: cacheDir as String, withIntermediateDirectories: true, attributes: nil)
        }
        
        // 转换成jpeg data
        guard let data = image.jpegData(compressionQuality: 1) else {
            jsbridgeCamera?.callback(callbackId: callbackId, json: [
                "success": false,
                "error": "Image compress fail"
            ])
            return
        }
        
        // 写文件
        do {
            let fileURL = URL(fileURLWithPath: filePath)
            try data.write(to: fileURL)
            // callback给js
            jsbridgeCamera?.callback(callbackId: callbackId, json: [
                "success": true,
                "file": fileURL.absoluteString.
            ])
        } catch {
            jsbridgeCamera?.callback(callbackId: callbackId, json: [
                "success": false,
                "error": error.localizedDescription
            ])
            return
        }
    } else {
        jsbridgeCamera?.callback(callbackId: callbackId, json: [
            "success": false,
            "error": "Operation failed."
        ])
    }
}

到这里就完成了takeVideo

Camera模块JS篇

jsbridge.js

JSBridge.Camera = {}
JSBridge.Camera.takePicture = function(callback) {
    callNative("Camera.takePicture", {}, callback)
}
JSBridge.Camera.takeVideo = function(callback) {
    callNative("Camera.takeVideo", {}, callback)
}

index.html

<button onclick="onClickButton(this)">Camera.takePicture</button>
<button onclick="onClickButton(this)">Camera.takeVideo</button>
case "Camera.takePicture":
    JSBridge.Camera.takePicture((result) => {
        JSBridge.UI.toast(JSON.stringify(result))
    })
    break
case "Camera.takeVideo":
    JSBridge.Camera.takeVideo((result) => {
        JSBridge.UI.toast(JSON.stringify(result))
    })
    break

结合Android和iOS,自己运行看看效果,这里就不截图了。

Image模块Android篇

Android去系统选择图片和视频资源依然使用Intent,只是参数设置不一样,这里不细讲,直接贴代码:

JSBridgeImage.swift

class JSBridgeImage(val activity: WebActivity, webView: WebView) : BridgeModuleBase(webView) {

    private val REQ_CODE = 200

    override fun callFunc(func: String, callbackId: String, arg: JSONObject) {
        when (func) {
            "pickPhotos" -> pickPhotos(callbackId, arg)
        }
    }

    private fun pickPhotos(callbackId: String, arg: JSONObject) {
        // 创建去系统选图片的意图
        val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        activity.startActivityForCallback(intent, REQ_CODE) { requestCode: Int, resultCode: Int, result: Intent? ->
            // 不是对应的requestCode
            if (REQ_CODE != requestCode) {
                return@startActivityForCallback false
            }

            val json = JSONObject()

            if (resultCode == Activity.RESULT_OK) {
                val selectedImage = result?.data as Uri
                json.put("success", false)
                json.put("file", selectedImage.toString())
            } else {
                json.put("success", false)
                json.put("canceled", resultCode == Activity.RESULT_CANCELED)
                json.put("error", "Result code is $resultCode")
            }

            callback(callbackId, json)
            true
        }
    }

}

WebViewBridge.kt

bridgeModuleMap["Camera"] = JSBridgeCamera(activity, webView)

Image模块iOS篇

JSBridgeImage.swift

iOS去系统选择图片和视频资源和Camera拍照使用的都是UIImagePickerController,只是参数 设置不一样,这里不细讲,直接贴代码:

class JSBridgeImage : BridgeModuelBase {

    weak var viewController: WebViewController?
    fileprivate var pickerDelegate: PickerDelegate?

    init(viewController: WebViewController) {
        self.viewController = viewController
    }

    override func callFunc(_ funcName: String, callbackId: String, arg: [String : Any?]) {
        switch funcName {
        case "pickPhotos": pickPhotos(callbackId: callbackId, arg: arg)
        default: break
        }
    }
    
    private func pickPhotos(callbackId: String, arg: [String : Any?]) {
        guard let vc = self.viewController else { return }
        
        let picker = UIImagePickerController()
        picker.sourceType = .photoLibrary
        pickerDelegate = PickerDelegate()
        pickerDelegate?.jsbridgeImage = self
        pickerDelegate?.callbackId = callbackId
        picker.delegate = pickerDelegate!
        vc.present(picker, animated: true, completion: nil)
    }

}

fileprivate class PickerDelegate : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    weak var jsbridgeImage: JSBridgeImage?
    var callbackId: String!
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        jsbridgeImage?.pickerDelegate = nil
        picker.dismiss(animated: true, completion: nil)
        
    
        let url = info[UIImagePickerController.InfoKey.referenceURL] as? URL
        let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String
        let type = mediaType == "public.image" ? "image" : "video"
        // callback给js
        jsbridgeImage?.callback(callbackId: callbackId, json: [
            "success": true,
            "type": type,
            "file": url?.absoluteString
        ])
    }
    
    
    // 出来用户取消的操作
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        jsbridgeImage?.pickerDelegate = nil
        picker.dismiss(animated: true, completion: nil)
        jsbridgeImage?.callback(callbackId: callbackId, json: [
            "success": false,
            "canceled": true
        ])
    }
    
}

Image模块JS篇

jsbridge.js

JSBridge.Image = {}
JSBridge.Image.pickPhotos = function(callback) {
    callNative("Image.pickPhotos", {}, callback)
}

index.html

<button onclick="onClickButton(this)">Image.pickPhotos</button>
case "Image.pickPhotos":
    JSBridge.Image.pickPhotos((result) => {
        JSBridge.UI.toast(JSON.stringify(result))
    })
    break

结合Android和iOS,自己运行看看效果,这里就不截图了。

结语

到这里我们完成了几个JSBridge模块:

  • UI
  • KVDB
  • Camera
  • Image

完整地了解了如何模块化

其中Camera、Image模块会返回一些本地路径,协议不是http的,WebView无法识别,下一章我们会讲解