hn-failte's blog hn-failte's blog
首页
  • 前端文章

    • JavaScript
    • Vue
    • React
    • Webpack
    • 混合开发
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《React》笔记
    • 《TypeScript 从零实现 axios》
    • 《Git》学习笔记
    • TypeScript笔记
    • JS设计模式总结笔记
  • HTML&CSS
  • HTML
  • CSS
  • CSS预处理
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 算法
  • 数据库
  • 操作系统
  • 工具
  • 学习
  • 面试
  • 心情杂货
  • 前端相关
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

hn-failte

前端cv仔
首页
  • 前端文章

    • JavaScript
    • Vue
    • React
    • Webpack
    • 混合开发
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《React》笔记
    • 《TypeScript 从零实现 axios》
    • 《Git》学习笔记
    • TypeScript笔记
    • JS设计模式总结笔记
  • HTML&CSS
  • HTML
  • CSS
  • CSS预处理
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 算法
  • 数据库
  • 操作系统
  • 工具
  • 学习
  • 面试
  • 心情杂货
  • 前端相关
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Webpack

  • React

  • JavaScript

  • Vue

  • 混合开发

    • 搭建一个极简混合开发架构
      • 原生、H5 与混合开发 的优缺点
        • 原生 APP
        • H5
        • 混合开发
      • 混合开发的形式
      • JsBrigde
      • Scheme 跳转协议
      • 挟持 WebView 的原生 js 方法
      • 总结
      • 相关源码
  • 学习笔记

  • 微信小程序

  • 前端
  • 混合开发
hn-failte
2021-08-03

搭建一个极简混合开发架构

# 搭建一个极简混合开发架构

自移动互联网普及之后,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>
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

然后定义一个 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;
    }
}
1
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");
}
1
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" }));
1

注意:由于安卓 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>
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

如何读取本地的 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");
}
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

解析 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;
}
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

h5 层面的调用

window.open(
  "failte://android?code=toast&data=" + JSON.stringify({ data: "toast" })
);
1
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");
}
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
const res = window.prompt(
  "cordova://android?code=plugin&data=" + JSON.stringify({ data: "value" })
);
console.log(res);
1
2
3
4

# 总结

至此,一个搭建一个极简混合开发架构就搭好了,接下来可以按照需求去扩展功能了

# 相关源码

https://github.com/hn-failte/JsBridge (opens new window)

编辑 (opens new window)
#混合开发
上次更新: 2021/08/07, 20:42:25
Vue项目使用mock数据的几种方式
《JavaScript教程》笔记

← Vue项目使用mock数据的几种方式 《JavaScript教程》笔记→

最近更新
01
基于 Taro 的微信小程序优化指南
02-16
02
使用State Hook
04-06
03
使用Effect Hook
04-06
更多文章>
Theme by Vdoing | Copyright © 2017-2023 hn-failte | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式