一、引言
本文主要针对H5
与原生混合开发中的交互问题进行讨论,当然,这仅仅是鄙人的见解,求同存异。
本文主要针对以下问题进行总结:
如何实现JS
与Andriod
的交互?
针对WebView
启动慢问题,如何优化?
如果存在多个H5
模块包,如何实现模块包的完全更新与部分更新?
针对以上问题的,如何建立一个公用的工具集(框架?)?
遇到的问题及解决办法。
OK, 开始吧!
二、交互
关于如何实现JS
与Android
交互,其实看官方的 Building web apps in WebView 这篇文章就够了,如果你觉得英文不好理解,那也没关系,因为接下来的内容会覆盖这些技术点。
其实这里可以进一步将Webview
抽象化,那么就得到了如下图关系:
显然这里的问题就是如何实现JsExecutor
和JsInterfaces
了。
对于JsExecutor
而言(Android
调用JS
) ,其实是比较固定的写法,比如,如果我们想要动态获取网页中某个标签的html
,那么会这么写:
Stirng elementId = "content" ;
String jsCode = "javascript:document.getElementById(\" + elementId +\").innerHtml" ;
webView. evaluateJavascript ( jsCode, new ValueCallback < String > () {
@Override
public void onReceiveValue ( String html ) {
}
} );
这种写法是固定的,但是方法参数比较多时就比较蛋疼了,拼凑方法名和多个参数是很烦人的,且容易出错,因而我们可以抽象出以下工具类:
public final class JsExecutor {
private static final String TAG = "JsExecutor" ;
private JsExecutor ( ) {
}
public static void executeJsRaw ( @NonNull WebView webView, @NonNull String jsCode) {
executeJsRaw ( webView, jsCode, null ) ;
}
public static void executeJsRaw ( @NonNull WebView webView, @NonNull String jsCode, @Nullable ValueCallback< String> callback) {
if ( Build. VERSION. SDK_INT >= 19 ) {
webView. evaluateJavascript ( jsCode, callback) ;
} else {
webView. loadUrl ( jsCode) ;
}
}
public static void executeJs ( @NonNull WebView webView, @NonNull CharSequence methodName, @Nullable ValueCallback< String> callback, @NonNull CharSequence. . . params) {
StringBuilder sb = new StringBuilder ( ) ;
sb. append ( "javascript:" )
. append ( methodName)
. append ( "(" ) ;
if ( params != null && params. length > 0 ) {
for ( int i = 0 ; i < params. length; i++ ) {
sb. append ( "\"" )
. append ( params[ i] )
. append ( "\"" ) ;
if ( i < params. length - 1 )
sb. append ( "," ) ;
}
}
sb. append ( ");" ) ;
Log. i ( TAG, "executeJs: " + sb) ;
executeJsRaw ( webView, sb. toString ( ) , callback) ;
}
public static void executeJs ( @NonNull WebView webView, @NonNull CharSequence methodName, @NonNull CharSequence. . . params) {
executeJs ( webView, methodName, null , params) ;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
这里直接将WebView
视为我们执行JS
代码的工具,如下示例是给H5
传递当前网络类型,由于整合了JS
代码的拼接过程,因此只需要传入具体方法名称和方法的字符串参数即可。
JsExecutor.executeJs(webView, "onNetStatusChanged", netType);
对于JsInterfaces
(JS
调用Android
) , 我们需要在我们需要注入的方法前加上注解@JavascriptInterface
才能将方法暴露出去,然后将包含此方法的类对象注入进去,如下一个实际场景, H5
需要从Android
原生中获取用户的账号信息,那么可以这么写:
先注入包含对应方法的H5JsStorage
类对象:
H5JsStorage h5JsStorage = new H5JsStorage ( this , mUser) ;
webView. addJavascriptInterface ( h5JsStorage, "h5JsStorage" ) ;
其中getUserAccountInfo
的声明如下:
public class H5JsStorage implements IH5JsStorage {
@JavascriptInterface
public String getUserAccountInfo ( ) {
return String . format ( "{\"userAccount\":\"%s\", \"password\":\"%s\", \"userIncrId\":\"%s\", \"orgId\":\"%s\"}" , mUser. getUserAccount ( ) , mUser. getPassword ( ) , mUser. getUserIncrId ( ) , mUser. getOrgId ( ) ) ;
}
}
以上便是H5
与原生交互的交互过程,具体代码在文章末尾会给出GitHub
地址。
三、WebView 启动速度优化、多模块包自动更新
1. WebView
启动速度优化
我们先来做个实验,测试一下包含WebView
的Activity
在优化前后的启动速度 ,可以这么做:根据Activity
的生命周期,在onCreate
的第一行处记录下初始时间,在onStart
最后一行记录下结束时间,然后计算时间差,作为衡量启动速度的参照,多次测试,记录时间差。结果如下:
根据以上结果可以看出,优化后要比优化前的启动速度快个10~20
秒,且抖动较小。可以注意到其中包含一个叫做prepareWebView
的时间差,据此,聪明的你肯定能想到我所谓的优化是做了什么操作。嗯~,其实就是使用WebView
之前,在合适的地方和时机先将其初始化,之后复用这个创建好的实例,这里我是这么写的:
public final class MyWebViewHolder {
private static final String TAG = "MyWebViewHolder" ;
private MyWebView mWebView;
private static MyWebViewHolder sMyWebViewHolder;
private View pageNoneNet;
private boolean mShouldClearHistory = false ;
public boolean shouldClearHistory ( ) {
return mShouldClearHistory;
}
public void shouldClearHistory ( boolean shouldClearHistory) {
this . mShouldClearHistory = shouldClearHistory;
}
private MyWebViewHolder ( ) {
}
public static MyWebViewHolder getHolder ( ) {
if ( sMyWebViewHolder != null ) return sMyWebViewHolder;
synchronized ( MyWebViewHolder . class ) {
if ( sMyWebViewHolder == null ) {
sMyWebViewHolder = new MyWebViewHolder ( ) ;
}
}
return sMyWebViewHolder;
}
public void prepareWebView ( Context context) {
long start = System . currentTimeMillis ( ) ;
if ( mWebView != null ) return ;
synchronized ( this ) {
if ( mWebView == null ) {
mWebView = new MyWebView ( context) ;
}
}
Log . i ( TAG, "prepareWebView: total cost: " + ( System . currentTimeMillis ( ) - start) + " ms" ) ;
Log . d ( TAG, "prepare MyWebView OK..." ) ;
}
public MyWebView getMyWebView ( ) {
return mWebView;
}
public void detach ( ) {
if ( mWebView != null ) {
Log . d ( TAG, "detach MyWebView, but not destroy..." ) ;
( ( ViewGroup ) mWebView. getParent ( ) ) . removeView ( mWebView) ;
mWebView. removeAllViews ( ) ;
mWebView. clearAnimation ( ) ;
mWebView. clearFormData ( ) ;
mShouldClearHistory = true ;
mWebView. getSettings ( ) . setJavaScriptEnabled ( false ) ;
}
}
public void attach ( ViewGroup parent, int index) {
if ( mWebView != null ) {
Log . d ( TAG, "attach MyWebView, index of ViewGroup is " + index) ;
WebSettings settings = mWebView. getSettings ( ) ;
settings. setDomStorageEnabled ( true ) ;
settings. setSupportZoom ( false ) ;
settings. setJavaScriptEnabled ( true ) ;
settings. setUseWideViewPort ( true ) ;
mWebView. setLayoutParams ( new ViewGroup . LayoutParams ( ViewGroup . LayoutParams . MATCH_PARENT, ViewGroup . LayoutParams . MATCH_PARENT) ) ;
mWebView. setVerticalScrollBarEnabled ( false ) ;
mWebView. setHorizontalScrollBarEnabled ( false ) ;
FrameLayout frameLayout = new FrameLayout ( parent. getContext ( ) ) ;
frameLayout. setLayoutParams ( new FrameLayout . LayoutParams ( ViewGroup . LayoutParams . MATCH_PARENT, ViewGroup . LayoutParams . MATCH_PARENT) ) ;
frameLayout. addView ( mWebView, new FrameLayout . LayoutParams ( ViewGroup . LayoutParams . MATCH_PARENT, ViewGroup . LayoutParams . MATCH_PARENT) ) ;
pageNoneNet = LayoutInflater . from ( parent. getContext ( ) ) . inflate ( R . layout. layout_null_net, frameLayout, false ) ;
frameLayout. addView ( pageNoneNet) ;
pageNoneNet. setVisibility ( View . GONE) ;
pageNoneNet. findViewById ( R . id. btn_try) . setOnClickListener ( new View . OnClickListener ( ) {
@Override
public void onClick ( View v) {
pageNoneNet. setVisibility ( View . GONE) ;
mWebView. reload ( ) ;
}
} ) ;
parent. addView ( frameLayout, index) ;
}
}
public void showNoneNetPage ( ) {
if ( pageNoneNet != null )
pageNoneNet. setVisibility ( View . VISIBLE) ;
}
public void hideNoneNetPage ( ) {
if ( pageNoneNet != null )
pageNoneNet. setVisibility ( View . GONE) ;
}
public void attach ( ViewGroup parent) {
attach ( parent, parent. getChildCount ( ) ) ;
}
public void destroy ( ) {
if ( mWebView != null ) {
Log . d ( TAG, "destroy MyWebView..." ) ;
mWebView. destroy ( ) ;
}
}
public void pause ( ) {
if ( mWebView != null ) {
Log . d ( TAG, "pause MyWebView..." ) ;
mWebView. onPause ( ) ;
}
}
public void resume ( ) {
if ( mWebView != null ) {
Log . d ( TAG, "resume MyWebView..." ) ;
mWebView. onResume ( ) ;
}
}
public void removeJSInterfaces ( String . . . names) {
if ( names == null || names. length == 0 ) return ;
for ( String name : names) {
Log . d ( TAG, String . format ( "removeJSInterfaces:: %s .." , name) ) ;
mWebView. removeJavascriptInterface ( name) ;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
然后在合适的地方初始化:
@Override
protected void onCreate ( Bundle savedInstanceState) {
MyWebViewHolder . getHolder ( ) . prepareWebView ( this ) ;
}
添加到布局中:
LinearLayout parent = findViewById ( R . id. parent ) ;
MyWebViewHolder. getHolder ( ) . attach ( parent ) ;
在onDestroy
时从界面中解除绑定:
@Override
protected void onDestroy ( ) {
MyWebViewHolder . getHolder ( ) . detach ( ) ;
}
2. 多模块包自动更新
支持多模块自动更新的目的是方便更新维护,减少用户升级所带来的流量开支,每个模块包之间可以是相互独立的,也方便于团队开发,仅需要和前端约定好文件目录即可。
先来看看H5
模块的自动更新流程(完整更新 ):
上面是模块包的完整更新过程,还可以进行补丁更新 ,而所谓补丁更新就是,下载的更新包中仅仅包含需要更新的文件,因而对应于上面流程而言,就是少了删除本地旧版本文件 的过程,而直接解压替换对应文件。这种更新方式有以下优缺点:
可以极大的减少更新时对用户的流量消耗,且速度极快。
但是需要前端明确抽取所更新的文件,否则会出现问题,可能这个过程会繁琐点。
如果使用类似于VueJs
这种模板框架编写的界面,因为需要编译为JS
代码,然后仅剩一个index.html
入口,导致抽取定位繁琐,且每次编译出来的文件名可能不一样,因此不能使用补丁更新 这种方式,只能分包,然后进行完整更新。
具体代码比较多,就补贴了,请看 github这里 , 其中H5ManagerSettings
是H5Manager
配置信息与无关逻辑的抽离类。
四、建立公用工具集
上面已经逐个介绍了混合开发中交互与更新的逻辑,工具集已经放到 github
的H5MixDevelopTools ,感兴趣的童鞋可以看看,虽然这里我并没有把JS
接口和html
界面放上去。
遇到的实际问题与解决办法: (以项目中使用VueJs
作为模板引擎来编写H5
页面为例)
1. 界面加载不出来,显示空白,怎么办?
解决办法 :给WebView
加上下面配置即可
mWebView.getSettings ( ) .setDomStorageEnabled ( true) ;
2. 联调时发现总是找不到定义的交互接口方法,怎么办?
原因与解决办法 :首先,默认情况下,VueJs
在对代码进行混淆处理,因此如果你遇到了这个问题,那么请手动配置以关闭混淆(具体做法请自行查找吧)。如果已经不混淆了,但是依然找不到对应的方法,怎么办?我和我的小伙伴是将接口文件放到components
中将其视作一个组件来使用的,然后具体到接口方法的话,将方法挂到window
对象下,如下示例:
window. showToast = function ( msg ) {
UI . showToast ( msg) ;
}
window. userInfo = { name: "horseLai" }
3. 图片选择问题,怎么选择和预览图片?
先来个具体场景 :比如说我们项目中有个评论功能,这个功能是用H5
写的,然后每次评论时可以选择数量小于3张的评论图片,附带文字上传至服务器。
此时你会发现直接使用<input type="file">
没法调用起系统相片图库和相机,更没法在旁边显示预览图,这时你可能需要这些配置:
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
settings.setAllowContentAccess(true);
接着就是选择图片有两种方案 :
通过复写WebChromeClientc#onShowFileChooser
和WebChromeClient#openFileChooser
,但是openFileChooser
方法已经变为系统Api
了,所以没法直观的找到它,但是,即使找到了,你也会发现去适配不同的机型也是坑的很。可以先看看 android-4-4-webview-file-chooser-not-opening , 而因为我不是直接调用图库选择,而是先开启一个BottomSheetDialog
来选择是通过相机还是图库取图,这样带来的问题就是,如果我仅仅是开启了BottomSheetDialog
,然后不做任何选择地关闭掉它,不调用ValueCallback#onReceiveValue
传值的话,那么<input>
只能启当一次弹窗,之后再点就没反应了,而如果我每次关闭BottomSheetDialog
时通过ValueCallback#onReceiveValue
传个null
,那么连续启动两次后又会异常闪退 ,嗯,这坑我就不跳了,我选择第二种方案。
第二种方案就是直接建立JS
交互接口,点击图片选择控件后调用建立好的原生图片选择接口取图,当我们选好图之后在onActivityResult
方法中执行JS
方法将图片的本地路径传给JS
处理,嗯,到这里的话好说,这个流程咱都熟悉。那么来说说如何在<img>
上预览,以及如何将这个路径的图片作为文件上传。
下面是选完图片后我们将图片路径回调到JS
的方法。
window. selectedImgFile = [ ] ;
window. selectedImgFileUrls = [ ] ;
window. onPictureResult = function ( type , imgFilePath) {
selectedImgFile. push ( new File ( [ "" ] , imgFilePath, { type : "image/*" } ) ) ;
selectedImgFileUrls. push ( {
imgUrl: "file://" + imgFilePath
} ) ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面selectedImgFile
,selectedImgFileUrls
这两个挂载到window
的变量,这两个数组可以直接在全局引用了,记得在使用后清空,不然会影响到下次使用。
嗯,看起来很完美,选图、预览很完美,但很快你就会发现这实际是个BUG
,BUG
在哪里呢?注意到上面的new File([""], imgFilePath, {type:"image/*"})
,这么使用会导致上传到服务器的图片大小为 0kb
, 为啥呢?因为第一个参数[""]
实际是图片的实际数据(字节数组),它的长度代表着文件的大小,因此,上面这样做虽然能够预览,但是无法仅仅直接通过一个本地路径就读取到文件流数据,也就不能上传成功了。
怎么办呢?思考了很久,发现自己一直困在JS
如何通过一个本地路径建立File
并上传的思维当中,于是找前端和后台的小伙伴交流,最终确定的方案是:选择图片后先将图片编码成Base64
字符串再注入到JS
处理,JS
端收到数据后进行图片数据绑定,以及上传到服务器,服务器端进行Base64
解码处理,然后保存成本地图片。
于是可以稍微修改成这样:
window. selectedImgFile = [ ] ;
window. selectedImgFileUrls = [ ] ;
window. onPictureResult = function ( type, imgFilePath, base64Data ) {
selectedImgFile. push ( base64Data ) ;
selectedImgFileUrls. push ( {
imgUrl: "data:image/jpg;base64," + base64Data
} ) ;
}
不过这里依然可能存在一些问题,比如内存溢出,因为图片本身可能很大,尤其是使用相机直接拍照取图的情况,一张图片可能会有3~10M
,直接编码为图片本身会比较耗时,而编码出来的字符串会存在于内存中,因此很有可能会导致Android
端出现内存溢出的情况,因此这里可以考虑先压缩后编码,这样可以降低内存耗尽的几率。
五、总结
本文基于实际项目,介绍了混合开发中JS
与原生交互的实现,然后以一个小实验测试了含WebView
的Activity
的启动速度,优化,然后测试优化后的启动速度,接着介绍了H5
分模块更新的逻辑,最后整理了一套工具集,感兴趣的童鞋可以看看 H5MixDevelopTools ,欢迎指正。
使用H5
混合开发确实能够提升开发速度,但是实际体验确实一般,适合非常追求开发速度的场景。
文章来源: jiangwenxin.blog.csdn.net,作者:前端江太公,版权归原作者所有,如需转载,请联系作者。
原文链接:jiangwenxin.blog.csdn.net/article/details/121494956
评论(0)