搭建一个极简混合开发架构
自移动互联网普及之后,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>
然后定义一个 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;
}
}
在主 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");
}
在 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>
如何读取本地的 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");
}
解析 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;
}
h5 层面的调用
window.open(
"failte://android?code=toast&data=" + JSON.stringify({ data: "toast" })
);
挟持 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");
}
const res = window.prompt(
"cordova://android?code=plugin&data=" + JSON.stringify({ data: "value" })
);
console.log(res);
总结
至此,一个搭建一个极简混合开发架构就搭好了,接下来可以按照需求去扩展功能了
相关源码
https://github.com/hn-failte/JsBridge
关于
欢迎关注作者个人博客:https://failte.cn/