住手!你们不要再打了啦!Native和Web应该和平相处啊
摘要
Native
是如何给Web
页面提供可供Web
调用的原生方法的Web
在执行完Native
提供的方法之后如何知道结果,回调数据怎么传给Web
Web
端如何优雅的使用Native
提供的方法
原生说他提供了,前端说没有,调不到你的方法然后再一通调试,写了各种看不下去的兼容代码,终于能摘下痛苦面具了,赶紧测试完上线吧……
前端说你的方法有问题,你执行完了都没回调我,原生说我回调你了啊
原生或前端都会说:你怎么给了我一个字符串啊,我需要对象啊
所以原因还是在双方对彼此不了解导致的,下面就给大家伙儿把这里面的门道给说明白!
Native
是如何给Web
页面提供可供Web
调用的原生方法的
Android
和iOS
的可供网页调用的方法的方式是不一样的,这里只对Android
的webkit.WebView - addJavascriptInterface
和iOS
的WKWebView - evaluateJavaScript
进行剖析。这一段前端的同学可得搬个小板凳,拿个小本本好好记下来~Android
:webkit.WebView
- addJavascriptInterface
首先拿Android
上举例吧,其实前端同学写的网页在App
里面的运行时就是一个WebView
,通常情况下原生提供给前端的JS
方法会维护一个专门给前端提供的有很多不同方法的一个类,端上会定义一个命名空间的字符串,把所有的这个类里面的方法都放到这个命名空间下面,然后把这个命名空间挂载到网页的window
对象也就是全局对象上,来段简单的例子代码:// ... import pageage// webview的Activity
class WebviewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_webview)
WebView.setWebContentsDebuggingEnabled(true)
val webview = WebView(this)
val context = this
setContentView(webview)
// 指定webview都要干什么
webview.run {
// 设置开启JavaScript能力
settings.javaScriptEnabled = true
// 添加提供给网页的js方法,并把这些方法注入到AppInterface这个全局对象里面
addJavascriptInterface(WebAppFunctions(context, webview), "AppInterface")
// 指定URI,加载网页
loadUrl("https://www.baidu.com")
}
}
}// 一个提供可供网页调用js方法的类
class WebAppFunctions(private val mContext: Context) {/**带有这个@JavascriptInterface注解的方法都是提供给网页调用的方法 *//** 展示Toast */
@JavascriptInterface
fun showToast(toast: String) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
}
}
当这个
WebviewActivity
被创建之后,就会将所有的WebAppFunctions
里面的有@JavascriptInterface
注解的方法注入到网页的window.AppInterface
对象上,这个命名空间AppInterface
就是上面我们addJavascriptInterface
方法的第二个参数,这个应该是原生和网页约定好的一个命名空间字符串,这个时候我们在网页上就可以通过这样来调用原生提供给我们的showToast
方法了:window.AppInterface.showToast("Hi, I'm a Native's Toast!")
iOS:
WKWebView
- evaluateJavaScript
同样的,前端的同学也要好好看下iOS
的。相对于WKUserContentController
可以给网页注入方法,evaluateJavaScript
既可以给网页注入方法,也可以执行网页的回调,所以一般使用evaluateJavaScript
来处理和网页的交互,举个简单的:let userContent = WKUserContentController.init()
// 推荐约定一个命名空间,在这个命名空间下,通过解析Web端传递过来的参数中的方法名、数据和回调来处理不同的逻辑
userContent.add(self, name: "AppInterface")
let config = WKWebViewConfiguration.init()
config.userContentController = userContentlet wkWebView: WKWebView = WKWebView.init(frame: UIScreen.main.bounds, configuration: config)
wkWebView.navigationDelegate = self
wkWebView.uiDelegate = self
view.addSubview(wkWebView)
view.insertSubview(wkWebView, at: 0)
wkWebView.load(URLRequest.init(url: URL.init(string: "https://www.baidu.com")!))...// 代理方法,window.webkit.messageHandlers.AppInterface.postMessage(xxx)实现发送到这里
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// WKScriptMessage有两个属性,一个是name一个是bady,name就是我们之前约定的AppInterface, body里面就是方法名(必选)、数据、回调网页的方法名
if message.name == "AppInterface" {
let params = message.body
// 这里推荐约定args里面有两个参数,arg0、arg1,分别是参数和回调网页的方法名(可选)
if (params["functionName"] == "showToast") {
// 执行showToast操作
}
}
}
iOS
中这种注入的方式提供给网页上调用跟Android
不同,需要前端这么来调用:window.webkit.messageHandlers.AppInterface.postMessage({ functionName: "showToast" })
也就是说前面的这部分
window.webkit.messageHandlers.AppInterface.
都是一样的,调用的方法名、数据参数还有提供给原生回调我们的方法名都通过约定的postMessage
中的参数进行传递。Web
在执行完Native
提供的方法之后如何知道结果,回调数据怎么传给Web
网页和原生的交互除了这种简单直接的告诉原生你要干什么之外,还有其他的一些情况,比如选取本地相册中的一个或者多个照片,这个时候问题就变得复杂了,首先我可能需要有选取照片的类型,比如我只选1
张照片和选多张照片是不同的,而且多张照片的情况下应该有个上限,比如类似微信的最多选取9
张这种,并且选取成功之后,网页上还需要展示出来这些照片,这个时候就需要原生在选完照片之后告诉网页选的都是哪些照片了。举个简单的例子:判断一个对象中有没有
name
这个属性Android:
// 同上面的...class WebAppFunctions(private val mContext: Context, private val webview: WebView) {/**
* 是否有name属性
* @param obj: 传进来的序列化后的对象
* @param cbName: 执行完成后回调js的方法名
* @return Boolean
*/
@JavascriptInterface
fun hasName(obj: String, cbName: String) {
// 将序列化后的对象反序列化为JSON对象
val data = https://www.it610.com/article/JSONObject(obj)
// 判断对象是否有name属性
val result = data.has("name")
webview.post {
// 执行JavaScript中的回调方法并将回调数据传过去,执行成功后打印日志
webview.evaluateJavascript("javascript:$cbName(${result})") {
Log.i("callbackExec", "success")
}
}
}
}
在网页中的怎么调用这个,怎么拿到回调:
// 首先定义一个回调方法
window.nativeCallback = (res) => console.log(typeof res, res)
// 然后调用`AppInterface`上的`hasName`方法并按照约定将判断的数据序列化后和回调方法名一并传给原生
const params = JSON.stringify({ age: 18, name: 'ldl' })
window.AppInterface.hasName(params, 'nativeCallback')
// 执行成功之后,回调就会回调我们的回调并打印相应的结果
boolean true
iOS 原生代码跟
Android
逻辑相同,比较简单的这里就忽略了。在网页中的怎么调用这个,怎么拿到回调:
// 同样的先定义回调方法,并将数据序列化
window.nativeCallback = (res) => console.log(typeof res, res)
const params = JSON.stringify({ age: 18, name: 'ldl' })
window.webkit.messageHandlers.AppInterface.postMessage({
functionName: 'hasName',
args: {
arg0: params,
arg1: 'nativeCallback'
}
})
到这里,想必原生和网页的同学都大致了解了对方的情况了,尤其是前端的同学应该知道怎么调用原生的方法了,但是
Android
和iOS
上调用同一个方法的写法还不同,如果每次都要通过UA
判断再执行不同的代码也太麻烦了,而且回调都是挂在全局的window上的还有命名冲突和内存泄漏的风险。所以我们最后聊一下如何在将调用Android
、iOS
的方法调用差异抹平,让前端同学可以更加优雅的调用原生方法!Web
端如何优雅的使用Native
提供的方法
根据我们之前的规范,所有原生提供的方法都属于以下四种类型
- 无任何参数
- 仅有数据参
- 仅有回调参
- 既有数据参,也有回调参
- 不同端类型调用方式不同,如何通过封装抹平这个差异
- 每次调用有回调的原生方法都需要在全局声明一个函数供原生调用,会有命名冲突和内存泄漏风险
- 回调我们的方法声明在全局,需要在内部处理很多判断,我们如何把回调的内容抽离出来在不同的方法中处理
- 我们在调试的时候怎么看到我调用的是什么方法,传的参数是什么有没有问题,如何设计一个调用日志
- 首先我们定义一个枚举维护所有的原生提供的方法
export const enum NativeMethods { /** 展示toast */ SHOW_TOAST: 'showToast', /** 是否有name属性 */ HAS_NAME: 'hasName', // .... }
- 维护一个原生方法和数据相关的类型声明文件native.d.ts, 并声明一个
iOS
上的需要传递给postMessage
方法的参数类型
declare name NATIVE { type SimpleDataType = string | number | boolean | symbol | null | undefined | bigint /** iOS原生方法参数接口 */ interface PostiOSNativeDataInterface { functionName: NativeMethods args?: { arg0?: SimpleDataType arg1?: string } } }
- 定义一个
nativeFunctionWrapper
方法,这个方法有三个参数,第一个参数funcionName
是方法名,第二个params
是数据参数,第三个是hasCallback
是否有回调,我们通过这个方法将不同端的方法调用差异抹平:
export function nativeFunctionWrapper(functionName: NativeMethods, params?: unknown, hasCallback?: boolean) {
const iOS = Boolean(navigator.userAgent.match(/\(i[^;
]+;
( U;
)? CPU.+Mac OS X/))
// 如果有数据切数据是引用类型就将其序列化为字符串
let data = https://www.it610.com/article/params
if (params && typeof params ==='object') data = https://www.it610.com/article/JSON.stringify(params)
// 如果data不是undefined就是有参数,void 0是为了得到安全的undefined, callbackName是提供给原生回调我们的方法名
const hasParams = data !== void 0,
callbackName ='nativeCallback'
if (hasCallback) {
window[callbackName] = (res) => console.log(res)
}if (isiOS) {
const postData: NATIVE.PostiOSNativeDataInterface = { functionName }
// 根据不同的情况构建不同的参数
if (hasParams) {
postData.args = { arg0: data }
if (hasCallback) postData.args.arg1 = callbackName
} else if (hasCallback) postData.args = { arg0: callbackName }
// 判断只有在真机上才执行,我们在电脑上的Chrome中调试的时候就不必调用执行原生方法了
if (window.webkit) {
window.webkit.messageHandlers.AppInterface.postMessage(postData)
}
} else {
// 同样的如果宿主环境没有AppInterface就return
if (!window.AppInterface) return
// 根据不同的参数情况 走不同的执行调用逻辑
if (hasData) {
hasCallback ? window.AppInterface[functionName](data, callbackName) : window.AppInterface[functionName](data)
} else if (hasCallback) {
window.AppInterface[functionName](callbackName)
} else {
window.AppInterface[functionName]()
}
}
}
- 上一步我们通过
nativeFunctionWrapper
解决了我们的第一个问题,抹平了不同端同个方案的调用差异,直接可以通过调用nativeFunctionWrapper
指定方法名、参数和是否有回调即可调用不同端的方法。其实第二步里面我们还是将原生回调我们的方法写死了,这样肯定是有问题的,我们现在来解决后面的问题:
// 我们通过动态的设置我们的回调函数的方法名来解决这个问题,最后跟上时间戳拼接是为了防止有些方法可能调用的很频繁,导致后面的回调数据还是走到第一个回调里面 const callbackName = `NativeFun_${functionName}_callback_${Date.now()}`
- 但是我们这么做又会有内存泄漏,因为调用一次原生方法,就要往
window
上添加一个函数,我们来改造下回调函数体的内容
const callbackName = `NativeFun_${functionName}_callback_${Date.now()}` if (hasCallback) { window[callbackName] = (res) => { console.log(res) // 释放挂载的临时函数 window[callbackName] = null // 删除临时函数全局对象并返回undefined void delete window[callbackName] } }
- 接下来我们来解决第三个问题,把回调之后的逻辑抽离出来,因为我们现在的方式,针对不同的回调拿到数据还是需要在
window[callbackName]
内部进行判断,这样很不优雅,我们来通过Promise
对我们的nativeFunctionWrapper
进行改造:
export function nativeFunctionWrapper(functionName: NativeMethods, params?: unknown, hasCallback?: boolean) { const iOS = Boolean(navigator.userAgent.match(/\(i[^; ]+; ( U; )? CPU.+Mac OS X/)), const errInfo = `当前环境不支持!` return new Promise((resolve, reject) => { // 如果有数据切数据是引用类型就将其序列化为字符串 let data = https://www.it610.com/article/params if (params && typeof params ==='object') data = https://www.it610.com/article/JSON.stringify(params) // 如果data不是undefined就是有参数,void 0是为了得到安全的undefined, callbackName是提供给原生回调我们的方法名 const hasParams = data !== void 0, callbackName = `NativeFun_${functionName}_callback_${Date.now()}` if (hasCallback) { window[callbackName] = (res: string) => { resolve(res) window[callbackName] = null void delete window[callbackName] } } if (isiOS) { const postData: NATIVE.PostiOSNativeDataInterface = { functionName } // 根据不同的情况构建不同的参数 if (hasParams) { postData.args = { arg0: data } if (hasCallback) postData.args.arg1 = callbackName } else if (hasCallback) postData.args = { arg0: callbackName } // 判断只有在真机上才执行,我们在电脑上的Chrome中调试的时候就不必调用执行原生方法了 if (window.webkit) { window.webkit.messageHandlers.AppInterface.postMessage(postData) if (!hasCallback) resolve(null) } else reject(errInfo) } else { // 同样的如果宿主环境没有AppInterface就return if (!window.AppInterface) return // 根据不同的参数情况 走不同的执行调用逻辑 if (hasData) { hasCallback ? window.AppInterface[functionName](data, callbackName) : window.AppInterface[functionName](data) } else if (hasCallback) { window.AppInterface[functionName](callbackName) } else { window.AppInterface[functionName]() resolve(null) } } }) }
- 【住手!你们不要再打了啦!Native和Web应该和平相处啊】通过上面的这步改造,我们就将回调的逻辑抽离到Promise里面了,直接在
.then
中拿原生回调我们的数据即可,到这里我们就几乎完成所有的封装工作了,最后我们给他添加一个调用日志打印的功能:
/** 原生方法调用日志 */ function NativeMethodInvokedLog(clientType: unknown, functionName: unknown, params: unknown, callbackName: unknown) { this.clientType = clientType this.functionName = functionName this.params = params this.calllbackName = callbackName }// 在`nativeFunctionWrapper`中判断是否是`iOS`的前面加上下面这句代码 console.table(new NativeMethodInvokedLog(`${isiOS ? 'iOS' : 'Android'}`, functionName, data, callbackName))
这样在你调用原生的方法的时候就可以看到详细的调用信息了,是不是很nice~
// 最终一步封装后直接提供给各业务代码调用
export function hasNameAtNative(params: unknown) {
return nativeFunctionWrapper(NativeMethods.HAS_NAME, params, true): Promise
}
// 调用
const data = https://www.it610.com/article/{ age: 18, name:'ldl' }
hasNameAtNative(data).then(res => {
console.log(`data is or not has name attr: `, res)
})
如果你和原生交互的数据类型比较复杂也可以在我们之前维护的
native.d.ts
文件中维护与原生交互的数据类型总结 其实原生和网页之间的交互没有什么特别难搞的东西,但是想要把这部分内容给规范化,工程化,还是要做不少工作的。也希望原生网页一家亲,大家
Senar
(公号同名),谢谢各位!推荐阅读
- 非必要千万不要改C盘用户名!!!
- JAVA人生|春招来了,这波神仙打架你要不要参战
- 可不要忽视了TypeScript中函数和类的重要性
- java|3月,劝你不要跳槽!
- 不要再用where|不要再用where 1=1了,有更好的写法
- python|用 Pandas 做 ETL,不要太快
- 《奔跑吧》之龙舟特辑~谢谢你们,让我们为综艺人感动!
- YC015不要让任何事偷走你的梦想
- 我想你们了
- 不要担心女朋友花你的钱