Summary
- How does
Native
provide native methods forWeb
pages that can be called byWeb
- How does
Web
know the result after executing the method provided byNative
, and how to pass the callback data toWeb
- How to elegantly use the method provided by
Native
on theWeb
side
background
In the mixed development mode of native and web pages, mobile terminals will inevitably have business scenarios that call native capabilities on web pages, such as operating photo albums, local files, and accessing cameras. If the native and front-end students do not understand each other's execution mechanism of the methods provided by each other, it is easy to appear similar to the following situations:
The original said that he provided it, the front end said no, and I couldn’t adjust your method 😖
The front end said that there was a problem with your method. You didn't call me back after you finished executing it. The native said I called you back😠
Native or front-end will say: why did you give me a string, I need objects 😭
Then I debugged again and wrote all kinds of compatible codes that I couldn’t bear to watch. Finally, I was able to take off the pain mask. Let’s finish the test and go online...
So the reason is that the two sides don't understand each other, so let's explain the doorway to everyone!
How does Native
provide the Web
page with native methods that can be called by Web
The methods of Android
and iOS
that can be called by web pages are different. Only Android
of webkit.WebView - addJavascriptInterface
and iOS
of WKWebView - evaluateJavaScript
are analyzed here. The students at the front end of this section have to move a small bench and take a small notebook to write it down~
Android
:webkit.WebView
- addJavascriptInterface
First, take Android
as an example. In fact, the web page written by the front-end classmates is a WebView
at runtime in App
. Usually, the JS
method that is natively provided to the front-end will maintain a class specially provided for the front-end with many different methods. It will define a namespace string, put all the methods in this class under this namespace, and then mount this namespace to the window
object of the web page, which is the global object, here is a simple example code :
// ... 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()
}
}
When this WebviewActivity
is created, all the methods annotated with WebAppFunctions
in @JavascriptInterface
will be injected into the window.AppInterface
object of the web page. This namespace AppInterface
is the second parameter of our addJavascriptInterface
method above, which should be the native and web page conventions. A good namespace string, at this time we can call the showToast
method provided to us natively on the web page like this:
window.AppInterface.showToast("Hi, I'm a Native's Toast!")
iOS:WKWebView
- evaluateJavaScript
Similarly, the front-end students should take a good look at iOS
. Compared with WKUserContentController
, which can inject methods into webpages, evaluateJavaScript
can not only inject methods into webpages, but also execute callbacks of webpages. Therefore, evaluateJavaScript
is generally used to deal with the interaction with webpages. Here is a simple 🌰:
let userContent = WKUserContentController.init()
// 推荐约定一个命名空间,在这个命名空间下,通过解析Web端传递过来的参数中的方法名、数据和回调来处理不同的逻辑
userContent.add(self, name: "AppInterface")
let config = WKWebViewConfiguration.init()
config.userContentController = userContent
let 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操作
}
}
}
This injection method in iOS
is different from Android
, which requires the front end to call:
window.webkit.messageHandlers.AppInterface.postMessage({ functionName: "showToast" })
That is to say, the previous part of window.webkit.messageHandlers.AppInterface.
is the same. The method name, data parameters and method name provided to the native callback are all passed through the agreed parameters in postMessage
.
How does Web
know the result after executing the method provided by Native
, and how does the callback data pass to Web
In addition to the simple and direct interaction between the web page and the native, there are other situations, such as selecting one or more photos in the local album. At this time, the problem becomes complicated. First of all, I may the need to select photos type , for example, I select only 1
Zhang photos and selected multiple photos are different, but in the case of multiple photos should have a limit, such as similar to the micro-channel select up to 9
Zhang , and after the selection is successful, these photos need to be displayed on the web page. At this time, it is necessary to tell the web page which photos are selected after selecting the photos.
For a simple example: determines whether an object has the attribute or not 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 = JSONObject(obj)
// 判断对象是否有name属性
val result = data.has("name")
webview.post {
// 执行JavaScript中的回调方法并将回调数据传过去,执行成功后打印日志
webview.evaluateJavascript("javascript:$cbName(${result})") {
Log.i("callbackExec", "success")
}
}
}
}
How to call this in the web page, how to get the callback:
// 首先定义一个回调方法
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
The native code has the same logic as Android
, which is relatively simple and ignored here.
How to call this in the web page, how to get the callback:
// 同样的先定义回调方法,并将数据序列化
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'
}
})
At this point, the students of the native and the webpage must have a general understanding of each other's situation, especially the front-end students should know how to call the native method, but the writing method of calling the same method on Android
and iOS
is still different. It is too troublesome to execute different codes through UA
judgment, and the callbacks are all hanging on the global window, and there is a risk of naming conflicts and memory leaks. So let's finally talk about how to smooth out the difference between the method calls of Android
and iOS
, so that front-end students can call native methods more elegantly!
How to elegantly use the method provided by Native
on the Web
end
According to our previous specification, all natively provided methods fall into the following four types
- without any parameters
- data only
- callback parameter only
- Both data parameters and callback parameters
We need to do the underlying encapsulation for the above four types. First of all, what problems do we need to solve:
- Different end types call in different ways, how to smooth this difference through encapsulation
- Every time you call a native method with callback, you need to declare a function globally in for native call , there will be a risk of naming conflict and memory leak
- The callback method declared in the global and needs to be processed internally. How do we the content of the callback and it in different methods
- When we are debugging, how can we see what method is calling , and what are the parameters passed? Is there any problem? How to design a call log
First we heat the pot (bushi
First we define an enumeration to maintain all natively provided methods
export const enum NativeMethods { /** 展示toast */ SHOW_TOAST: 'showToast', /** 是否有name属性 */ HAS_NAME: 'hasName', // .... }
Maintain a native method and data-related type declaration file native.d.ts, and declare a parameter type on
iOS
that needs to be passed to thepostMessage
methoddeclare name NATIVE { type SimpleDataType = string | number | boolean | symbol | null | undefined | bigint /** iOS原生方法参数接口 */ interface PostiOSNativeDataInterface { functionName: NativeMethods args?: { arg0?: SimpleDataType arg1?: string } } }
- Define a
nativeFunctionWrapper
method. This method has three parameters. The first parameterfuncionName
is the method name, the secondparams
is the data parameter, and the third is whetherhasCallback
has a callback. We use this method to smooth out the difference in method calls at different ends:
export function nativeFunctionWrapper(functionName: NativeMethods, params?: unknown, hasCallback?: boolean) {
const iOS = Boolean(navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/))
// 如果有数据切数据是引用类型就将其序列化为字符串
let data = params
if (params && typeof params === 'object') data = 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]()
}
}
}
In the previous step, we solved our first problem through
nativeFunctionWrapper
, and smoothed out the calling differences of the same solution on different ends. You can directly callnativeFunctionWrapper
to specify the method name, parameters and whether there is a callback to call methods on different ends. In fact, in the second step, we still write the native callback method to death, which must be problematic. Let's solve the following problems now:// 我们通过动态的设置我们的回调函数的方法名来解决这个问题,最后跟上时间戳拼接是为了防止有些方法可能调用的很频繁,导致后面的回调数据还是走到第一个回调里面 const callbackName = `NativeFun_${functionName}_callback_${Date.now()}`
But if we do this, there will be memory leaks, because calling a native method requires adding a function to
window
, let's modify the content of the callback function bodyconst callbackName = `NativeFun_${functionName}_callback_${Date.now()}` if (hasCallback) { window[callbackName] = (res) => { console.log(res) // 释放挂载的临时函数 window[callbackName] = null // 删除临时函数全局对象并返回undefined void delete window[callbackName] } }
Next, let's solve the third problem, extracting the logic after the callback, because in our current way, we still need to judge within
window[callbackName]
to get data for different callbacks, which is very inelegant. Let's passPromise
OurnativeFunctionWrapper
for retrofit: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 = params if (params && typeof params === 'object') data = 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) } } }) }
Through the above transformation, we have extracted the callback logic into Promise, and we can directly call back our data in
.then
. At this point, we have almost completed all the encapsulation work, and finally we will add it to him. A function that calls log printing:/** 原生方法调用日志 */ 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))
In this way, when you call the native method, you can see the detailed call information, isn't it nice~
After the above transformation, let's see how we can call it now
// 最终一步封装后直接提供给各业务代码调用
export function hasNameAtNative(params: unknown) {
return nativeFunctionWrapper(NativeMethods.HAS_NAME, params, true): Promise<boolean>
}
// 调用
const data = { age: 18, name: 'ldl' }
hasNameAtNative(data).then(res => {
console.log(`data is or not has name attr: `, res)
})
If the data types you interact with natives are complex, you can also maintain the data types that interact with natives in the native.d.ts
file we maintained before
Summarize
In fact, there is nothing particularly difficult about the interaction between native and web pages, but to standardize and engineer this part of the content, a lot of work still needs to be done. I also hope that the original web page is a family, everyone core live in peace! If you have other better solutions for standardizing this part, you can also say it in the comments. If it is helpful to you, please don’t be stingy with your three companies. Finally, if it is useful, please like it. If you like it, please pay attention. I am Senar
(the same name as the official account), thank you!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。