搭建一个极简混合开发架构
# 搭建一个极简混合开发架构
自移动互联网普及之后,h5 开发与原生 APP 开发便迎来了高速的发展,而这两者之间也各有优缺点。而这两者之前也有融合,形成了一种新的开发模式:混合开发。
# 原生、H5 与混合开发 的优缺点
# 原生 APP
优点:
可访问众多手机底层所有提供的功能
运行速度快、性能高,用户体验好
安全性较高
缺点:
开发时间长,快则 3 个月左右完成,慢则五个月左右,直接导致成本较高
可移植性比较差,不同平台都要各自开发,同样的逻辑、界面要写两套
发布要受到平台限制
获得新版本时需重新下载应用更新
# H5
优点:
支持设备范围广,可以跨平台,编写的代码可以同时在多端执行
开发成本低、周期短
适合展示有段落文章等格式比较丰富的页面
用户可以直接使用最新版本(不需用户手动更新)
缺点:
技术限制,无法直接访问设备硬件和离线存储,体验和性能局限
对联网要求高,离线不能做任何操作
APP 反应速度慢,页面切换流畅性较差
用户体验感较原生 APP 有差距
# 混合开发
优点:
开发效率高,节约时间。
代码跨平台
更新和部署比较方便,升级小版本只需要在服务器端升级即可,不再需要上传到应用商店进行审核;
代码维护方便、版本更新快,节省产品成本
比 web 版实现功能多;
可离线运行。
缺点:
功能、界面有限
加载缓慢、网络要求高
安全性比较低
需要原生和 H5 都懂
# 混合开发的形式
而由于以上的优缺点,原生 APP 和 H5 交叉,根据主导程度,划分一下几类
1、以原生做主导,H5 为辅,这种市面上其实还是挺多的,各家的 APP 在不同程度上都有集成
2、以 H5 做主导,原生为辅,这种比较有名的有:uni-app、cordova
3、以 H5 的形式,开发原生,这种主要有:React-Native、Flutter
# JsBrigde
什么是 JsBridge?
在一些原生与 H5 混合开发的应用中,由于 H5 的功能有限或是不够完美,通常会在原生应用中提供一些独有的方式,然后暴露到 WebView 中,供 H5 页面使用,而这些 api 一般就被称为 JsBridge。
JsBridge 主要是使用原生安卓 Webview 类的 addJavaScriptInterface 方法提供 API,挂在到 h5 的全局作用域 window 上。
首先,我们创建了一个简单的安卓项目,并给首页添加一个 WebView 组件,设置其 id 为 view_webview
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fullscreenBackgroundColor"
android:theme="@style/ThemeOverlay.JsBridge.FullscreenContainer"
tools:context=".FullscreenActivity">
<!-- This FrameLayout insets its children based on system windows using
android:fitsSystemWindows. -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/fullscreen_content_controls"
style="@style/Widget.Theme.JsBridge.ButtonBar.Fullscreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:orientation="horizontal"
tools:ignore="UselessParent"/>
<WebView
android:id="@+id/view_webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</FrameLayout>
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
然后定义一个 JS 方法类,并添加需要暴露的 JS 方法
public class JavaScriptMethod {
private Context mContext;
private WebView mWebView;
// 需要挂在在 webview 的接口,在 h5 中表现为某个全局对象
public static final String JAVASCRIPTINTERFACE = "JsBridge";
// andorid 4.2(包括android4.2)以上,如果不写该注解,js无法调用android方法
@JavascriptInterface
public void showToast(String json){
Toast.makeText(mContext, json, Toast.LENGTH_SHORT).show();
}
public JavaScriptMethod(Context context, WebView webView) {
mContext = context;
mWebView = webView;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在主 Activity 的 onCreate 钩子里,获取页面上的 webview,并添加 js 方法,暴露给 h5 使用
@SuppressLint("JavascriptInterface")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fullscreen);
// 获取页面上的 webview 组件引用
webView = findViewById(R.id.view_webview);
// 获取 webview 的设置类
WebSettings settings = webView.getSettings();
// 允许在 WebView 中使用 js
settings.setJavaScriptEnabled(true);
// 实例化方法类
JavaScriptMethod method = new JavaScriptMethod(this, webView);
// 添加 JS 接口
webView.addJavascriptInterface(method, JavaScriptMethod.JAVASCRIPTINTERFACE);
// 指定 webview 加载哪个页面
webView.loadUrl("file:///android_asset/index.html");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在 H5 中的调用
window.JsBridge.showToast(JSON.stringify({ code: "toast", data: "abc" }));
注意:由于安卓 9.0 以上在 webview 中默认限制了必须有 https,所有的 http 请求都会被拦截,需要修改配置
加入<uses-permission android:name="android.permission.INTERNET"></uses-permission>
与 android:usesCleartextTraffic="true"
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.failte.jsbridge">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.JsBridge">
<activity
android:name=".FullscreenActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/app_name"
android:theme="@style/Theme.JsBridge.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>
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
如何读取本地的 html 文件?
读取本地文件和线上文件有点区别,这里需要使用 file 协议头去加载,同时,在项目的 app/src/main
路径下新创建了一个 assets 文件夹,这样就可以通过 file:///android_asset
读取到下面的文件了(这种情况下前端文件是随着 APP 一起打包的)
联想:APP 的热更新怎么做?
步骤:
- 在 APP 初始化时生成前端资源文件夹
- 下载远程提供的资源包,然后读取该包
- 打热更新包,并上传到远程,指定可以接收到更新的版本
- APP 触发更新后,下载远程资源包,并替换本地的资源包,再重新读取资源
# Scheme 跳转协议
该方式主要是通过安卓拦截 h5 端请求的 url 地址,并对 url 进行解析,返回结果,从而完成交互。
核心代码
@SuppressLint("JavascriptInterface")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fullscreen);
// 获取页面上的 webview 组件引用
webView = findViewById(R.id.view_webview);
// 获取 webview 的设置类
WebSettings settings = webView.getSettings();
// 允许在 WebView 中使用 js
settings.setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
// 返回 true,即根据代码逻辑执行相应操作,webview 不加载该url
// 返回 false,除执行相应代码外,webview 加载该url
// 返回 super.shouldOverrideUrlLoading(),在父类中,返回的其实还是 false
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 通过判断拦截到的url是否含有pre,来辨别是http请求还是调用android方法的请求
String pre = "failte://android";
if (url.contains(pre)) {
// 该url是调用 android 方法的请求
Map map = getParamsMap(url, pre);
// 解析 url 中的参数来执行相应方法
String code = (String) map.get("code");
String data = (String) map.get("data");
if(code.equals("toast")) {
try {
JSONObject json = new JSONObject(data);
String toast = (String)json.optString("data");
Log.v("toast", toast);
Toast.makeText(context, toast, Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
e.printStackTrace();
}
}
return true;
}
// 放行其他请求,用 webview 加载 url
return false;
}
});
// 指定 webview 加载哪个页面
webView.loadUrl("file:///android_asset/index.html");
}
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
解析 url 的方法,将 url 的 query 参数解析成对象
private Map getParamsMap(String url, String pre) {
ArrayMap qsMap = new ArrayMap<>();
if (url.contains(pre)) {
int index = url.indexOf(pre);
int end = index + pre.length();
String queryString = url.substring(end + 1);
String[] queryStringSplit = queryString.split("&");
String[] queryStringParam;
for (String qs : queryStringSplit) {
if (qs.toLowerCase().startsWith("data=")) {
//单独处理 data 项,避免 data 内部的 & 被拆分
int dataIndex = queryString.indexOf("data=");
String dataValue = queryString.substring(dataIndex + 5);
qsMap.put("data", dataValue);
} else {
queryStringParam = qs.split("=");
String value = "";
if (queryStringParam.length > 1) {
//避免后台有时候不传值,如 key= 这种
value = queryStringParam[1];
}
qsMap.put(queryStringParam[0].toLowerCase(), value);
}
}
}
return qsMap;
}
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
h5 层面的调用
window.open(
"failte://android?code=toast&data=" + JSON.stringify({ data: "toast" })
);
2
3
# 挟持 WebView 的原生 js 方法
Webview 的 WebChromeClient 对象上存在 onJsAlert、onJsConfirm、onJsPrompt 方法,主要对应了浏览器端的 window.alert、window.confirm、window.prompt 方法,而由于 window.prompt 方法可以返回数据,因此可以利用该方法来进行通信。
@SuppressLint("JavascriptInterface")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fullscreen);
// 获取页面上的 webview 组件引用
webView = findViewById(R.id.view_webview);
// 获取 webview 的设置类
WebSettings settings = webView.getSettings();
// 允许在 WebView 中使用 js
settings.setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Log.v("url", url);
Log.v("message", message);
Log.v("defaultValue", defaultValue);
String pre = "cordova://android";
if (message.contains(pre)) {
Map map = getParamsMap(message, pre);
String code = (String) map.get("code");
String data = (String) map.get("data");
if(code.equals("plugin")) {
try {
JSONObject json = new JSONObject(data);
String toast = (String)json.optString("data");
Log.v("plugin", toast);
result.confirm("\"{\"code\": 0}\", \"data\": {}");
} catch (JSONException e) {
e.printStackTrace();
}
} else {
result.cancel();
}
}
return true;
}
});
// 指定 webview 加载哪个页面
webView.loadUrl("file:///android_asset/index.html");
}
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
const res = window.prompt(
"cordova://android?code=plugin&data=" + JSON.stringify({ data: "value" })
);
console.log(res);
2
3
4
# 总结
至此,一个搭建一个极简混合开发架构就搭好了,接下来可以按照需求去扩展功能了