“我报名参加1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情”
目录
前言
下面接着上篇博客的 Demo 继续完善
桥接
WebView 的桥接,也就是 App 可以调用 Web 中的 js 方法,js 也可以调用 App 中的方法
普通调用
js 调用 App 方法
以实现 js 调用 App 实现展示 toast 为例,Demo 为了方便直接在 BaseWebView 中增加以下代码:
open class BaseWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : WebView(context, attrs), LifecycleEventObserver {
init {
// 省略其他代码...
// 添加桥接
addJavascriptInterface(this, "bridge")
}
// 添加注解 表示 js 可以调用该方法
@JavascriptInterface
fun showToast(message: String){
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
新建 test_default_bridge.html 文件
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<script src='../js/bridge.js'></script>
<style type="text/css">
.bn {
padding: 8px 20px;
width: 100%;
height: auto;
margin: 0 auto;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="detail-content" id="app-vote">
<div style="display: flex; flex-direction: column;">
<button class="bn" onclick="showToast()">调用原生 App 展示 Toast</button>
</div>
</div>
</body>
<script type='text/javascript'>
function showToast() {
// bridge 要和 BaseWebView 中 addJavascriptInterface 第二个参数对应
window.bridge.showToast('hello world')
}
</script>
让 WebView 加载 test_default_bridge.html 后测试,效果图:
App 调用 js 方法
App 调用 js 就比较简单了直接利用 WebView 的 evaluateJavascript 方法即可:
// 写正确 方法名 和 参数 即可
mWebView.evaluateJavascript("javascript:showToast('hello world')") {}
命令模式
作为开发者,肯定不希望写好的基类(BaseWebView)被频繁改动,如果采用上述 Demo 中的方式实现桥接通信,那每增加一个桥接都需要修改 BaseWebView 类,这里就需要用设计模式来进行重构。 这里为什么用命令模式?
- 通信双发都符合 请求方发出请求,要求执行某个操作;接收方收到请求,执行对应操作
- 并且都符合单命令单接收者
- 请求方和接收方可以独立开来,不用知道对方的命令接口(经过封装后达成,调用同一方法实现不同命令)
- 降低耦合,新命令(桥接方法)很容易加入到项目中(下面会结合 APT 实现)
实现过程主要考虑几个方面:
- 一般桥接肯定要 Android iOS 同时实现,要考虑易用性。
- 新增桥接方法时不能频繁改动已有代码,低耦合。
- 支持回调,WebView 的 evaluateJavascript 方法支持回调,那么 js 调用 Android 方法也要支持回调。
- 桥接参数传递选择了 json 格式,增删参数方便。
桥接实现
流程图
App 提供发送命令桥接
首先新增桥接参数实体类:
data class JsBridgeMessage(
@SerializedName("command")
val command: String?, // 命令
@SerializedName("params")
val params: JsonObject?, // 参数
)
新增命令接口:
interface IBridgeCommand {
fun exec(params: JsonObject?)
}
上述示例中展示 toast 的桥接改为命令,新增 ToastCommand 类:
class ToastCommand : IBridgeCommand {
override fun exec(params: JsonObject?) {
if (params != null && params["message"] != null) {
ToastUtils.showShort(params["message"].asString)
}
}
}
BaseWebView 中添加 sendCommond 桥接方法:
class BaseWebView{
// 省略其他代码...
init{
addJavascriptInterface(this, "bridge")
}
@JavascriptInterface
fun sendCommand(json: String?) {
if (json.isNullOrEmpty()) {
// 异常调用处理
return
}
try {
val message = GsonUtils.fromJson(json, JsBridgeMessage::class.java)
// 成功拿到参数后 待会在这里分发命令
// ...
} catch (e: JsonSyntaxException) {
e.printStackTrace()
}
}
}
命令分发器
分发器主要负责命令参数合法性校验、执行命令,这里用单例模式实现:
class JsBridgeInvokeDispatcher {
companion object {
// 省略不必要的代码...
// 单例
fun getInstance(): JsBridgeInvokeDispatcher {
// ...
}
}
// 暴露给外部方法 分发调用
fun sendCommand(view: BaseWebView, message: JsBridgeMessage?) {
LogUtils.d(TAG, "sendCommand()", "message: $message")
if (checkMessage(message)){
// 校验命令通过后 执行命令
excuteCommand(view, message)
}
}
// 校验命令、参数 合法性
private fun checkMessage(message: JsBridgeMessage?): Boolean{
if (message == null) {
return false
}
// ...
return true
}
//执行命令
private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
//...
}
}
执行命令再抽出一个类来,BridgeCommandHandler 主要负责命令的注册、命令的逻辑执行:
class BridgeCommandHandler {
companion object {
// 省略不必要的代码...
// 单例
fun getInstance(): BridgeCommandHandler {
// ...
}
}
// 用于切线程
private val mHandle = Handler(Looper.getMainLooper())
// 命令注册 暂时用 map 手动添加 后续修改
private val mCommandMap by lazy {
val map = ArrayMap<String, IBridgeCommand>().apply {
put("showToast", ToastCommand())
}
return@lazy map
}
// 暴露给外部方法 分发调用
fun handleBridgeInvoke(command: String?, params: String?) {
// map 中存在命令 则执行
if (mCommandMap.contains(command)) {
mHandle.post { // 切换到主线程 获取命令 执行
mCommandMap[command]!!.exec(
GsonUtils.fromJson(params, JsonObject::class.java)
)
}
}
}
}
再回到 JsBridgeInvokeDispatcher 的 excuteCommand 执行方法里调用一下 BridgeCommandHandler :
private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
BridgeCommandHandler.getInstance().handleBridgeInvoke(message.command, message.params)
}
到这里 App 端已经为 js 端做好了调用准备,这里毕竟是个 Demo 下面我们自己来搞一下 js 那边的封装。
js 文件封装
还是在 assets 的 js 目录下,新建 bridge.js 文件:
下面是 js 文件内容,算是一个工具类吧,Android 开发其实不用特别关注 js 代码,就不详细解释了,代码中都有注释,直接贴代码:
var jsBridge = {};
// 系统判断
jsBridge.os = {
'isAndroid': Boolean(navigator.userAgent.match(/android/ig)),
'isIOS': Boolean(navigator.userAgent.match(/iphone|ipod|iOS/ig))
};
// 发送命令 参数解释:command 命令;params 参数json格式
jsBridge.sendCommand = function(command, params) {
// 构建 message 对象
var message = {
'command': command
}
if (params && typeof params === 'object') { // 支持传参
message['params'] = params // 参数
}
if (jsBridge.os.isAndroid) { // android 桥接调用
window.bridge.sendCommand(JSON.stringify(message))
} else if (jsBridge.os.isIOS) { // ios 桥接调用 偷来的代码 不用太关注
window.webkit.messageHandlers.bridge.sendCommand(JSON.stringify(message))
}
}
window.jsBridge = jsBridge;
修改最初事例中展示 toast 的 html 文件:
// 省略了无关代码 只展示了引入 bridge.js 和 方法调用
// ...
<head>
<script src='./js/bridge.js'></script>
</head>
// ...
<script type='text/javascript'>
function showToast() {
// window.bridge.showToast('hello world')
// 上面的桥接调用修改为
var params = {
'message': 'hello world'
}
window.jsBridge.sendCommand('showToast', params)
}
</script>
测试效果和普通调用是一样的,这里就不放效果图了,为什么呢?因为这条流程还没搞定,还有一个很重要的细节 ———— 回调
回调支持
流程图
Web 端实现
这次先来修改 js 文件部分,在 bridge.js 中维护一个回调 map,并且桥接调用支持传递回调方法:
// 省略其他代码 只贴 新增 修改的代码
// 回调方法 map
jsBridge.mapCallbacks = {}
// 回调处理 提供给 App 命令执行完成后调用此方法 根据 key 从 map 中取出 触发回调
jsBridge.postBridgeCallback = function(key, data){
var obj = jsBridge.mapCallbacks[key]; // 从 map 中拿出 function
if(obj.callback){ // 存在则调用
obj.callback(data); // 调用 有参数则传递参数 这里回调参数也设计为 JSON 格式
delete jsBridge.mapCallbacks[key]; // 从 map 中移除
}else{ // 不存在 异常处理
console.log('jsBridge postBridgeCallback', '回调不存在: ' + key)
}
}
// 生成回调map key 的方法 采用 固定前缀+时间戳+随机码 的方式
// 防止短时间内并发调用出现重复的key
function generateCallbackKey(){
return "jsBridgeCallback_" + new Date().getTime() + "_" + randomCode();
}
// 生成随机码 防止并发重复
function randomCode(){
var code = ""
for(var i = 0; i < 6; i++){
code += Math.floor(Math.random() * 10)
}
return code;
}
// 发送命令 方法修改 增加 callback 回调方法参数
jsBridge.sendCommand = function(command, params, callback) {
var message = {
'command': command
}
if (params && typeof params === 'object') { // 支持传参
message['params'] = params
}
// sendCommand 方法主要增加了这个 if 判断
// 回调 key 固定放在 bridgeCallback 字段中,app 客户端判断 callback 是否有值即可
if (callback && typeof callback === 'function') { // 支持回调 判断是否是回调方法
var key = generateCallbackKey() // 生成回调key
jsBridge.mapCallbacks[key] = { 'callback': callback } // 回调方法放入 map 中
message['params']['bridgeCallback'] = key // 参数新增字段 bridgeCallback
}
if (jsBridge.os.isAndroid) { // android 桥接调用
window.bridge.sendCommand(JSON.stringify(message))
} else if (jsBridge.os.isIOS) { // ios 桥接调用 偷来的代码 不用太关注
window.webkit.messageHandlers.bridge.sendCommand(JSON.stringify(message))
}
}
App 端实现
首先 JsBridgeMessage 添加 bridgeCallback 字段:
data class JsBridgeMessage(
//省略...
@SerializedName("bridgeCallback")
val bridgeCallback: String? // 回调 key
)
新增回调接口 IBridgeCallbackInterface :
interface IBridgeCallbackInterface {
/**
* callback 回调key
* params 参数 json 格式
*/
fun handleBridgeCallback(callback: String, params: String)
// 从参数中获取回调 key 的方法
fun getCallbackKey(params: JsonObject?): String? {
if (params == null) {
return null
}
if (params["bridgeCallback"] == null) {
return null
}
return params["bridgeCallback"].asString
}
}
修改 IBridgeCommand 中 exec 方法参数,增加回调接口参数
interface IBridgeCommand {
fun exec(params: JsonObject?, callback: IBridgeCallbackInterface?)
}
修改 BridgeCommandHandler 的 handleBridgeInvoke 方法参数,增加回调接口参数:
fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeCallbackInterface?) {
// ...
mCommandMap[command]!!.exec(
GsonUtils.fromJson(params, JsonObject::class.java),
bridgeCallback // 回调传递给 Command
)
}
最后修改 JsBridgeInvokeDispatcher 的 excuteCommand 方法:
class JsBridgeInvokeDispatcher {
// ...
private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?){
// 实现 IBridgeCallbackInterface
val callback = object : IBridgeCallbackInterface{
override fun handleBridgeCallback(callback: String, params: String) {
view.postBridgeCallback(callback, params)
}
}
BridgeCommandHandler.getInstance().handleBridgeInvoke(
message?.command,
GsonUtils.toJson(message?.params),
callback
)
}
}
BaseWebView 中提供触发回调方法:
class BaseWebView{
// 省略其他代码
fun postBridgeCallback(key: String?, data: String?) {
post {
evaluateJavascript("javascript:window.jsBridge.postBridgeCallback(`$key`, `$data`)") {}
}
}
}
最后修改一下 ToastCommand 中的代码:
class ToastCommand : IBridgeCommand {
override fun exec(params: JsonObject?) {
if (params != null && params["message"] != null) {
ToastUtils.showShort(params["message"].asString)
//回调 测试 返回一个 message 给 web 端
val key = getCallbackKey(params)
if (!key.isNullOrEmpty()) {
val data = mapOf("message" to "showToast is success!!")
callback?.handleBridgeCallback(key, GsonUtils.toJson(data))
}
}
}
}
为了测试回调效果 js 中新增一个带回调的调用:
<script type='text/javascript'>
// ...
function showToastWithCallback() {
var params = {
'message': 'hello world'
}
window.jsBridge.sendCommand('showToast', params, function(data){
// 打印一下 回调中接受的参数
console.log('触发回调成功! data:', data)
})
}
</script>
效果图:
toast 一加成功展示,看一下回调输出的日志:
到这里为止用命令模式封装的桥接设计就大功告成了!
利用 apt 自动注册桥接
目前的实现后续每个命令的增删都需要在 BridgeCommandHandler 类中手动修改 mCommandMap 的初始化代码,这肯定是不合理的。网上也有很多博客利用 @AutoService 实现自动注册,这里分享一个我个人的方案,我是利用 APT 去扫描自定义注解生成一个工具类,BridgeCommandHandler 调用工具类方法实现自动注册的,下面简单说一下实现过程。
注意
Demo 中 APT 相关代码也是用 Kotlin 写的,Demo 整体也是个完完全全的 Kotlin 项目,那么 APT 生成的文件也自然是 Kotlin 文件最好
新建 Moudle
首先新建两个 Java or Kotlin Library
apt-annotations : 用来写自定义注解
apt-processor :用来写自定义注解处理器
apt-processor 添加依赖:
implementation project(path: ':apt-annotations')
// kotlin 文件生成库
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
implementation "com.squareup:kotlinpoet:1.8.0"
implementation "com.google.auto.service:auto-service:1.0"
kapt "com.google.auto.service:auto-service:1.0"
moudle_web 引入两个 apt 模块:
implementation project(path: ':apt-annotations')
kapt project(path: ':apt-processor')
注解
apt-annotations 中新建自定义注解:
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class JsBridgeCommand(
val name: String // 命令名
)
注解处理器
apt-processor 中新建自定义注解处理器 JsBridgeCommandProcessor, APT 不是重点,就简单贴一下部分代码,详情可以去 Demo 中查看:
@AutoService(Processor::class)
class JsBridgeCommandProcessor : AbstractProcessor() {
// 省略其他代码...
// 生成代码 路径
val packageName = "com.sunhy.demo.apt"
// 生成类方法
val registerMethodBuilder = FunSpec.builder("autoRegist")
.addComment("web jsbridge command auto load")
// 定义局部变量
val arrayMap = ClassName("android.util", "ArrayMap")
val iBridgeCommand = ClassName("com.sunhy.demo.web.bridge", "IBridgeCommand")
val arrayMapCommand = arrayMap.parameterizedBy(String::class.asTypeName(), iBridgeCommand)
registerMethodBuilder.addStatement("val commandMap = %L()", arrayMapCommand)
commandMap.forEach { (key, value) ->
registerMethodBuilder.addStatement("commandMap[%S] = $value()", key)
}
// 方法返回类型
registerMethodBuilder.returns(arrayMapCommand)
registerMethodBuilder.addStatement("return commandMap")
// 生成伴生对象
val companionObject = TypeSpec.companionObjectBuilder()
.addFunction(registerMethodBuilder.build())
.build()
// 生成类
val clazzBuilder = TypeSpec.classBuilder("JsBridgeUtil")
.addType(companionObject)
//输出到文件...
}
}
注解使用
给 ToastCommand 添加注解:
@JsBridgeCommand(name = "showToast")
class ToastCommand : IBridgeCommand {
// ...
}
到这里先编译一下项目,让 APT 生成 JsBridgeUtil 文件:
文件生成成功后修改 BridgeCommandHandler 中 mCommandMap 初始化代码:
class BridgeCommandHandler {
// private val mCommandMap by lazy {
// val map = ArrayMap<String, IBridgeCommand>().apply {
// put("showToast", ToastCommand())
// }
// return@lazy map
// }
// 之前初始化代码 替换为 自动注册
private val mCommandMap: ArrayMap<String, IBridgeCommand> by lazy { JsBridgeUtil.autoRegist() }
}
运行测试:
后续在注册新桥接新建 Command 时只需要加上注解给予 name 命令名即可。
独立进程
WebView 独立进程,就是让 Web 相关功能单独使用一个进程。
优点:
- 进程隔离,Web 进程发生异常不会导致主进程闪退
- 分担主进程内存压力
缺点:
- Application 每个进程启动都会初始化,造成多次初始化
- 跨进程通信要注意的细节很多(静态成员变量问题、sharedpreferences操作等等)
虽然有一定的缺点,当 App 越做业务越繁杂,内存占用变高,让其中一些功能模块独立进程运行能够有效解决内存占用高的问题。
查看进程
命令:
adb shell ps -A |grep com.sunhy.demo
可以看出目前 Demo 只有一个进程。
实现 Web 进程
让 WebView 相关页面在一个新的进程运行非常简单,只需要在 AndroidManifest.xml 给对应的 Activity 添加 process :
<application>
<activity
android:name=".activity.NewsDetailActivity"
android:exported="false"
android:process=":web"/> // 进程名
<activity
android:name=".activity.WebActivity"
android:exported="false"
android:process=":web"/> // 所有 WebView 相关页面都在 :web 进程中运行
</application>
运行 App 并且启动其中的 WebActivity 再次用命令行查看进程信息:
现在 Demo 已经有两个进程了,独立进程就已经实现了!
但是注意!!!多进程带来的缺点不能忽视,Application 进行了多次初始化,也就意味着之前在 Application 中写的 WebViewPool 初始化的代码初始化了两次,WebView 相关既然已经独立到新进程,那么主进程的 Application 还需要对 WebViewPool 初始化吗?
当然不需要!可以通过判断当前进程名字进行不同的初始化操作:
class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
when(ProcessUtils.getCurrentProcessName()){
"com.sunhy.demo" -> {
// 主进程初始化...
}
"com.sunhy.demo:web" -> {
// :web 进程程初始化...
initWebViewPool()
}
}
}
}
Application 设计
上面这种写法呢并不友好,当进程变多,或者因业务逻辑修改需要修改各个进程初始化逻辑时,又是会发生频繁修改 BaseApplication 文件的问题,尤其多人协作大家都负责不同的模块同时修改一个文件难免冲突。
这里分享两个个人时间过的思路:
- 利用抽象工厂模式,BaseApplication 中仅做生产调用,各个进程具体初始化逻辑写在各自的“孵化器”中。
- 和上面自动注册桥接方法一样利用 APT 实现,无非是把 map 中的桥接名替换为进程名,根据进程名取出“孵化器”进行初始化操作。
这不是关于 WebView 的重点就不贴代码了,仅分享下思路,下面是最后的重头戏了。
跨进程通信
为什么要跨进程通信
思考一个问题,就以当前 Demo 来说,有一个登陆工具类:
object LoginUtils {
private var userInfo: UserInfo? = null
fun getUserInfo(): String{
return GsonUtils.toJson(userInfo)
}
// 模拟登陆
fun login(){
this.userInfo = UserInfo("孙先森@", "ASDJKLQJDKL12KLDKL3KLJ1234KL12KLLDA")
}
}
App 主进程初始化时进行登陆,那么在 web进程中通过调用 LoginUtils.getUserInfo()
能够获取到用户信息吗?
并不能,原因很简单,在上面 webview 独立进程后,用户登陆只在主进程进行,web 进程中对 LoginUtils 的调用属于一个全新的对象。
需要解决类似这种问题就要动手实现进程间通信,将主进程的 userInfo 传递给 web 进程。
实现
增加登陆命令
@JsBridgeCommand(name = "getUserInfo")
class UserInfoCommand : IBridgeCommand{
override fun exec(params: JsonObject?, callback: IBridgeCallbackInterface?) {
val userInfoJson = LoginUtils.getUserInfo()
val key = getCallbackKey(params)
if (!key.isNullOrEmpty()) {
callback?.handleBridgeCallback(key, userInfoJson)
}
}
}
js 调用
function getUserInfo() {
var params = {}
window.jsBridge.sendCommand('getUserInfo', params, function(data){
console.log('用户信息:', data)
if(data){
$('.user').text(data);
}
})
}
分析
以上述 getUserInfo 从 web 进程调用为例,目前的调用流程图:
黑色框代表 web 进程调用,红色框代表 main 进程调用。可以看出分发器执行分发命令后就要进行跨进程通信。
跨进程通信
基础工作整完,下一步就是实现跨进程通信。这里采用 Android 中的 Binder 来实现跨进程通信。首先新建 AIDL 文件:
// web 进程调用 主进程
// 也就是 js 调用 原生 桥接调用
// 表示web进程调用主进程
interface IBridgeInvokeMainProcess {
void handleBridgeInvoke(String command, String params, IBridgeCallbackInterface bridgeCallback);
}
IBridgeCallbackInterface 之前已经定义为普通接口,主要用于处理桥接回调,那么同样改写为 AIDL 文件,Demo 没有删除 IBridgeCallbackInterface 文件为了避免命名冲突 AIDL 文件命名为 IBridgeInvokeWebProcess :
// 方法没有变化,由 IBridgeCallbackInterface 改为了 IBridgeInvokeWebProcess
interface IBridgeInvokeWebProcess {
void handleBridgeCallback(String callback, String params);
}
先编译一下,没问题后接着修改 BridgeCommandHandler,它主要负责处理 js 调用原生方法,所以首先继承 IBridgeInvokeMainProcess.Stub()
,对应修改其handleBridgeInvoke
方法参数:
class BridgeCommandHandler: IBridgeInvokeMainProcess.Stub() {
// 省略代码...
// 修改最后一参数类型
override fun handleBridgeInvoke(command: String?, params: String?, bridgeCallback: IBridgeInvokeWebProcess?) {
// 省略代码...
}
}
接着修改 IBridgeCommand 中 exec 方法回调参数:
interface IBridgeCommand {
// IBridgeCallbackInterface 改为 IBridgeInvokeMainProcess
fun exec(params: JsonObject?, callback: IBridgeInvokeMainProcess?)
}
相关命令子类一并修改,这里就不贴了。
根据上述分析 BridgeCommandHandler 的调用在主进程,那么主进程需要一个 service 向外暴露:
class BridgeCommandService: Service() {
override fun onBind(intent: Intent?): IBinder {
return BridgeCommandHandler.getInstance()
}
}
// AndroidManifest.xml 中注册
<service android:name=".service.BridgeCommandService"/>
下一步就轮到修改命令分发器JsBridgeInvokeDispatcher
,首先继承 ServiceConnection 并且提供绑定 service 方法:
class JsBridgeInvokeDispatcher : ServiceConnection{
private var iBridgeInvokeMainProcess: IBridgeInvokeMainProcess? = null
//省略其他代码...
// 获取 IBinder 对象
fun bindService() {
LogUtils.d(TAG, "bindService()")
if (iBridgeInvokeMainProcess == null) {
val i = Intent(BaseApplication.getInstance(), BridgeCommandService::class.java)
BaseApplication.getInstance().bindService(i, this, Context.BIND_AUTO_CREATE)
}
}
fun unbindService() {
LogUtils.d(TAG, "unbindService()")
iBridgeInvokeMainProcess = null
BaseApplication.getInstance().unbindService(this)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
iBridgeInvokeMainProcess = IBridgeInvokeMainProcess.Stub.asInterface(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
iBridgeInvokeMainProcess = null
}
// excuteCommand 方法修改
// callback 改为 IBridgeInvokeWebProcess.Stub
// 通过 iBridgeInvokeMainProcess 跨进程调用 handleBridgeInvoke
private fun excuteCommand(view: BaseWebView, message: JsBridgeMessage?) {
val callback = object : IBridgeInvokeWebProcess.Stub() {
override fun handleBridgeCallback(callback: String, params: String) {
LogUtils.e(TAG, "当前进程: ${ProcessUtils.getCurrentProcessName()}")
view.postBridgeCallback(callback, params)
}
}
if (iBridgeInvokeMainProcess != null){
iBridgeInvokeMainProcess?.handleBridgeInvoke(message?.command, parseParams(message?.params), callback)
}
}
}
效果图
最后
Demo源码
源码地址:WebViewSimpleDemo
Demo源码均为手写,参考文献已在第一篇文末说明。
THE END
如果我的博客分享对你有点帮助,不妨点个赞支持下!
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!