准备模块化
前面讲解了Native与JS通信
的知识,不过前面的代码没有大项目的应用价值,而这里的目标是在于设计
一个框架能够用于大项目的生产环境。
之后的文章会将Native与JS通信
称为JSBridge
微信小程序 JSBridge
打开 微信小程序 JSBridge API 文档, 我们可以看出微信提供了数以百计的 API,设计这些 API 不管是 Native 还是 JS 端,都需要以模块化的方式组织。
以模块化的方式组织之后,可以提供方便的引入方式和定制化需求,加上编写稳定的文档以供长期维护。一个大型 App 在框架的开发和维护上是核心之一。
如何模块化
一个大型的 App 通常会具备以下 JSBridge 模块:
- System: 包含系统和设备的相关一些接口,比如其中:wx.getSystemInfo
- App: 包含本 app 相关的一些接口,比如:getUserAgent 返回 app 的 UA
- UI:提供 native 级别一些 UI 接口,比如:toast,alert,confirm
- Camera: 提供 camera 业务相关的接口:比如:拍照、录像、扫码
- KVDB: key/value 数据库,通常我们需要比 localStorage 还要强的本地存储
- Image: 系统图片视频模块,比如:选择系统相册图片
- Sqlite: 提供 sqlite 功能
其实还有很多,包括真实业务模块等等,目前我们使用以上比较通用的模块来做下一步讲解。
我们可以通过伪代码引导我们模块化设计,比如:
JS 端,业务方调用的方式如下:
JSBridge.System.getSystemInfo()
JSBridge.UI.toast('Error: 404')
JSBridge.Camera.takePicture()
JSBridge.KVDB.setString('some key', 'some value')
...
Native 端,以 kotlin 为例:
class JSBridgeUI {
fun toast(message: String) { ... }
fun alert(title: String, essage: String, buttons: Array<String>) { ... }
fun confirm(title: String, message: String) { ... }
}
class JSBridgeCamera {
fun takePicture() { ... }
fun takeVideo() { ... }
}
以上伪代码可以看出,初步确定了 JS 端的调用和 Native 的模块(class)对于的设计关系,接下来要做的是怎么把他们关联起来。
模块化架构
从架构上看,是如下图的机构:
- js 部分组织各个模块,提供业务方调用
- JS 与 Native 通信部分
- Native 组织各个模块,提供 Native 的功能和连接客户端业务
到这里完成了大体的结构设计,接下来是实践部分,将分成 JS、iOS 和 Android 三个端分别讲解。
重构代码之JS端
前面我们讲了模块化的结构设计,这篇我们会将之前的代码进行重构
重构HTML为远程的web项目
我们将App内置的test.html
变成一个外部的真实web项目,并且WebView使用http协议去访问
重构后的结构:
web/
index.html // 页面
jsbridge.js // 提取调用native的js代码
start_server.sh // 启动服务器的shell
以上结构可以重这里看到:
sh start_server.sh
,我们用python的服务器模块来轻松启动服务器,
然后访问:http://localhost:8000/ 可以浏览页面
所有Native WebView加载时改为加载这个远程地址就行,后面重构Native的会讲到
重构提出jsbridge.js
(function() {
var currentCallbackId = 0
function callNative(method, data, callback) {
// 生成一个唯一callbackId
var callbackId = "nativeCallback_" + (currentCallbackId++)
if (callback) {
// 给window添加callback
window[callbackId] = (result) => {
delete window[callbackId]
callback(result)
}
}
var stringData = JSON.stringify(data)
if (window.androidBridge) {
// android端传递三个参数
window.androidBridge.callNative(callbackId, method, stringData)
} else {
// iOS不支持多参数,我们传递json对象
window.webkit.messageHandlers.iOSBridge.postMessage({
callbackId: callbackId,
method: method,
data: stringData
})
}
}
// 将方法挂到window.JSBridge下
var JSBridge = window.JSBridge = {}
JSBridge.callNative = callNative
})()
说明:
- callNative增加了method参数,代表调用的模块方法,比如:UI.toast
- window.androidBridge.callNative也增加了method参数传递
- window.webkit.messageHandlers.iOSBridge.postMessage也增加了method参数传递
调用:
JSBridge.callNative('UI.toast', {
message: 'This is toast!'
})
可以在jsbridge.js
里添加一些代码,改进一下更优雅:
...
var JSBridge = window.JSBridge = {}
JSBridge.callNative = callNative
JSBridge.UI = {}
JSBridge.UI.toast = function(message) {
// 这个方法不需要callback
callNative('UI.toast', { message: message })
}
调用:
JSBridge.UI.toast('This is toast!')
其他模块陆续慢慢加进来,目前使用UI.toast方法继续讲。
重构代码之Android端
创建另外的kotlin包来容纳新代码
对app/java/com.example.tobebigfe
点右键,进行如下图操作:
注意你的android路径有可能是这样的,如图第1点中,不是扁平的包名,那么可以勾上第2点中的两项:
输入jsbridge
,创建:
再在com.example.tobebigfe.jsbridge
下创建kotlin文件,文件名WebActivity
WebActivity.kt
abstract class WebActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WebView.setWebContentsDebuggingEnabled(true)
webView.settings.javaScriptEnabled = true
webView.settings.cacheMode = LOAD_NO_CACHE
webView.webViewClient = WebViewClient()
webView.addJavascriptInterface(BridgeObject(webView), "androidBridge")
webView.loadUrl(getLoadUrl())
}
abstract fun getLoadUrl(): String
interface BridgeModule {
fun callFunc(func: String, arg: JSONObject)
}
inner class BridgeObject(val webView: WebView) {
private val bridgeModuleMap = mutableMapOf<String, BridgeModule>()
init {
bridgeModuleMap["UI"] = JSBridgeUI()
}
@JavascriptInterface
fun callNative(callbackId: String, method: String, arg: String) {
Log.e("WebView", "callNative ok. args is $arg")
val jsonArg = JSONObject(arg)
val split = method.split(".")
val moduleName = split[0]
val funcName = split[1]
val module = bridgeModuleMap[moduleName]
module?.callFunc(funcName, jsonArg)
}
}
inner class JSBridgeUI : BridgeModule {
override fun callFunc(func: String, arg: JSONObject) {
when (func) {
"toast" -> toast(arg)
}
}
private fun toast(arg: JSONObject) {
val message = arg.getString("message")
Toast.makeText(this@WebActivity, message, Toast.LENGTH_SHORT).show()
}
}
}
一步步讲解
首先,提取WebView相关代码到WebActivity中:
abstract class WebActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WebView.setWebContentsDebuggingEnabled(true)
webView.settings.javaScriptEnabled = true
webView.settings.cacheMode = LOAD_NO_CACHE
webView.webViewClient = WebViewClient()
webView.addJavascriptInterface(BridgeObject(webView), "androidBridge")
// 调用getLoadUrl获取加载的链接
webView.loadUrl(getLoadUrl())
}
// 抽象方法,让子类实现,后面MainActivity中会体现作用
abstract fun getLoadUrl(): String
}
声明一个接口,这个接口定义callFunc方法,所有的JSBridge模块需要实现这个接口
interface BridgeModule {
fun callFunc(func: String, arg: JSONObject)
}
声明UI模块:
inner class JSBridgeUI : BridgeModule {
override fun callFunc(func: String, arg: JSONObject) {
when (func) {
// toast方法
"toast" -> toast(arg)
}
}
private fun toast(arg: JSONObject) {
val message = arg.getString("message")
Toast.makeText(this@WebActivity, message, Toast.LENGTH_SHORT).show()
}
}
改动BridgeObject:
inner class BridgeObject(val webView: WebView) {
// 声明存放模块的地方
// 比如:
// UI -> JSBridgeUI
// System -> JSBridgeSystem
private val bridgeModuleMap = mutableMapOf<String, BridgeModule>()
init {
// 添加模块
bridgeModuleMap["UI"] = JSBridgeUI()
}
@JavascriptInterface
fun callNative(callbackId: String, method: String, arg: String) {
Log.e("WebView", "callNative ok. args is $arg")
// 解析arg
val jsonArg = JSONObject(arg)
// 解析method,比如UI.toast,解析后 moduleName=UI funcName=toast
val split = method.split(".")
val moduleName = split[0]
val funcName = split[1]
// 拿到模块
val module = bridgeModuleMap[moduleName]
// 调用callFunc
module?.callFunc(funcName, jsonArg)
}
}
MainActivity.kt
class MainActivity : WebActivity() {
// 实现getLoadUrl,返回具体的链接,是我们用start_server.sh启动的服务器链接,
// 注意使用服务器的ip
override fun getLoadUrl(): String {
return "http://192.168.31.101:8000"
}
}
运行效果
运行前,需要在AndroidManifest.xml
增加网络设置:
效果:
重构代码之iOS端
创建新的Group来容纳新代码
创建WebViewController.swift
WebViewController.swift
import UIKit
import WebKit
class WebViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let birdge = BridgeHandler()
config.userContentController.add(birdge, name: "iOSBridge")
webView = WKWebView(frame: self.view.frame, configuration: config)
webView.uiDelegate = self
self.view.addSubview(webView)
birdge.webView = webView
birdge.viewController = self
birdge.initModules()
let url = URL(string: getLoadUrl())!
let request = URLRequest(url: url)
webView.load(request)
}
func getLoadUrl() -> String {
return ""
}
}
protocol BridgeModule : class {
func callFunc(_ funcName: String, arg: [String: Any?])
}
class BridgeHandler : NSObject, WKScriptMessageHandler {
var webView: WKWebView!
var viewController: WebViewController!
var moduleDict = [String:BridgeModule]()
func initModules() {
moduleDict["UI"] = JSBridgeUI(viewController: viewController)
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage)
{
let body = message.body as! [String: Any]
print("WebView callNative ok. body is \(body)")
let callbackId = body["callbackId"] as! String
let method = body["method"] as! String
let data = body["data"] as! String
guard let arg = try? JSONSerialization.jsonObject(with: data.data(using: .utf8)!, options: []) else {
return
}
let split = method.split(separator: ".")
let moduleName = String(split[0])
let funcName = String(split[1])
moduleDict[moduleName]?.callFunc(funcName, arg: arg as! [String:Any])
}
}
class JSBridgeUI : BridgeModule {
let viewController: WebViewController
init(viewController: WebViewController) {
self.viewController = viewController
}
func callFunc(_ funcName: String, arg: [String : Any?]) {
switch funcName {
case "toast":
toast(arg)
default: break
}
}
func toast(_ arg: [String : Any?]) {
let message = arg["message"] as! String
viewController.view.makeToast(message)
}
}
代码解析
声明protocol BridgeModule,所有的module将实现这个protocol
protocol BridgeModule : class {
func callFunc(_ funcName: String, arg: [String: Any?])
}
JSBridgeUI即UI模块的代码:
class JSBridgeUI : BridgeModule {
let viewController: WebViewController
init(viewController: WebViewController) {
self.viewController = viewController
}
func callFunc(_ funcName: String, arg: [String : Any?]) {
switch funcName {
case "toast":
toast(arg)
default: break
}
}
func toast(_ arg: [String : Any?]) {
let message = arg["message"] as! String
viewController.view.makeToast(message)
}
}
BridgeHandler代码:
class BridgeHandler : NSObject, WKScriptMessageHandler {
var webView: WKWebView!
// 声明VC,因为有些模块操作UI时需要用到,比如: UI.toast
var viewController: WebViewController!
// 声明一个dict用于添加模块
// 比如:
// UI -> JSBridgeUI
// System -> JSBridgeSystem
var moduleDict = [String:BridgeModule]()
// 初始化所有模块
func initModules() {
moduleDict["UI"] = JSBridgeUI(viewController: viewController)
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage)
{
let body = message.body as! [String: Any]
print("WebView callNative ok. body is \(body)")
let callbackId = body["callbackId"] as! String
// 获取method,比如:UI.toast
let method = body["method"] as! String
// 解析data
let data = body["data"] as! String
guard let arg = try? JSONSerialization.jsonObject(with: data.data(using: .utf8)!, options: []) else {
return
}
// 将UI.tast split
let split = method.split(separator: ".")
let moduleName = String(split[0]) // UI
let funcName = String(split[1]) // toast
// 调用相关模块
moduleDict[moduleName]?.callFunc(funcName, arg: arg as! [String:Any])
}
}
WebViewController:
// 改为getLoadUrl来得到链接地址,getLoadUrl由具体的子类实现
let url = URL(string: getLoadUrl())!
let request = URLRequest(url: url)
webView.load(request)
ViewController.swift
import UIKit
class ViewController : WebViewController {
// 覆盖getLoadUrl,返回具体的链接,是我们用start_server.sh启动的服务器链接,
// 注意使用服务器的ip
override func getLoadUrl() -> String {
return "http://192.168.31.101:8000"
}
}
iOS Toast
iOS内置没有toast功能,我们引入一个Toast.swift代码到文件夹ToBeBigFE
运行效果
WebView缓存问题
在开发过程会遇到webView有缓存,自己明明改了代码,但是打开的页面还是之前的样子, 那么在开发时我们加入如下代码:
在WebViewController的viewDidLoad中加入
let dataStore = WKWebsiteDataStore.default()
let dateFrom: Date = Date(timeIntervalSince1970: 0)
dataStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: dateFrom) {}
结语
到这里就完成了模块化重构,当然代码组织方式可以优化成多个文件,目前先放在一个文件中,等后面实现更多模块时再优化。