Hybrid 混合App

87 阅读16分钟

0.WebApp、原生App、混合App的区别

企业要开发一个App,那么对于前端开发者来说有三种流行开发技术,分别是WebApp、原生App和混合App。

原生App在Android、iOS等移动平台上利用官方提供的开发语言、类库进行App开发。

  • 优势:
    • 具有高的稳定性和流畅性,应用性能和交互体验好。
    • 原生应用大多数据都在本地,响应速度快。
  • 劣势:
    • 原生应用可移植性较差,一款原生App, Android和iOS都要各自开发,同样的逻辑、界面要写两套。
    • 开发成本高,维护成本高。

WebApp利用Web技术进行App开发。需要浏览器的支持才能进行展示和用户交互。

  • 优势:
    • 支持设备范围广,可以跨平台,编写的代码可以同时在Android、iOS、Windows上运行
    • 开发成本低、周期短
    • 用户可以直接使用最新版本(自动更新,不需要用户手动更新)。
  • 劣势:
    • H5移动应用不能直接访问设备硬件,所以体验和性能上有很大的局限性
    • 对互联网要求高,离线不能做任何操作
    • App反应速度慢,页面切换流畅性较差(访问页面需要下载对应的html/css/js)

混合App介于WebApp和原生App两者之间的App。混合App(Hybrid App)开发就是一部分采用原生开发,一部分使用web开发,如此一来就兼具原生App开发良好用户体验的优势和WebApp开发跨平台的优势。

  • 优势:
    • 开发效率高节约时间。同一套代码,Android和iOS基本都可以使用
    • 更新和部署比较方便,每次升级版本只需要在服务器端升级即可,不需要上传到App Store进行审核
    • 代码维护方便,版本更新快,节约产品成本
    • 比Web版实现功能多
  • 劣势:
    • 加载缓慢/网络要求高:混合App数据需要从服务器调取,每个页面都需要重新下载,因此打开速度慢,网络占用高,缓冲时间长,容易让用户反感。

1.比较流行的混合方案

Hybrid App,俗称混合应用,即混合了Native技术与Web技术,进行开发的移动应用。现在比较流行的混合方案主要有三种, 主要是UI渲染机制上的不同

  • 基于WebView UI的基础方案,市面上大部分主流App都有采用,例如微信JS-SDK,通过JSBridge完成H5与Native的双向通讯,从而赋予H5一定程度的原生能力。
  • 基于Native UI的方案,例如React-Native。在赋予H5原生API能力的基础上,进一步通过JSBridge将js解析成的虚拟节点树(Virtual DOM)传递到Native并使用原生渲染。
  • 另外还有比较流行的小程序方案,也是通过更加定制化的JSBridge,并使用双WebView双线程的模式隔离了JS逻辑与UI渲染,形成了特殊的开发模式。加强了H5与Native混合程度,提高了页面性能及开发体验。

以上三种方案,其实同样都是基于JSBridge完成的通讯层,第二三种方案,其实可看作是在方案一的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge也是整个混合应用最关键的部分,例如我们在设置微信分享时用的JS-SDK,wx对象便是我们最常见的JSBridge

基本原理

运行在原生应用内部的H5页面,通过原生提供的通信方式,跟原生互相调用。

image.png 包括H5调用原生和原生调用H5

Hybrid App工作原理

虽然看上去是一个Native App,但只有一个UI WebView,里面访问的是一个Web App,比如百度App最开始的应用就是包了个客户端的壳,其实里面是HTML5的网页,后来才推出真正的原生应用。

image.png

与原生App/WebApp比较:

image.png

2.JS怎么调用Native方法

有两种方式

  • URL Schema URL Schema 是一种类似URL的链接,是为了方便app直接互相调用设计的,形式和普通的url近似,主要区别是protocol和host一般是自定义的,例如:qunarhy://hy/url?url=ymfe.tech,protocol是qunarhy,host则是hy。拦截URL Schema的主要流程是:Web端通过某种方式(例如iframe.src)发送URL Schema请求,之后Native拦截到请求并根据URL Schema(包括所带的参数)进行相关操作。在实现过程中这种方式有一定的缺陷:
    • 使用iframe.src发送URL Scheme会有url长度的隐患
    • 创建请求,需要一定的耗时,比注入API的方式调用同样的功能,耗时会较长

示例代码

JavaScript 调用:JavaScript 可以通过如下代码发起URL Scheme请求:location.href = "js-to-native://getData"

**Native 拦截:在 Android 中,需要在WebViewClientshouldOverrideUrlLoading方法中进行拦截和处理,示例代码如下:

myWebView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 解析参数并执行相应逻辑
        return true;
    }
});

JS内容:

1.准备页面

2.准备元素

3.请求特殊的url

<input class="ios" type="button" value="使用iframe加载url">

// 加载url 通过iframe设置URL,目的是让ios拦截
function loadUrl(url){
    //创建iframe
    const iframe = document.createElement('iframe')
    //设置url
    iframe.src = url;
    //设置尺寸(不希望它被看到)
    iframe.style.height = 0;
    iframe.style.width = 0;
    //添加到页面上
    document.body.appendChild(iframe)
    //加载了url之后就没有用了
    //移除iframe
    iframe.parentNode.removeChild(iframe)
}

document.querySelector('ios').onClick = function (){
    loadUrl('js-to-native://click')
}

iOS部分内容:

1.成为代理

2.实现代理方法

//拦截url
func webView(_webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (NavigationActionPolicy) -> void){
    //获取url
    let url = navigationAction.request.url?.absoluteString;
    if(url == "js-to-native://click"){
        print("调用系统功能");
        decisionHandler(.cancel)
    }else{
        decisionHandler(.allow)
    }
}
  • 注入API注入API的主要原理是,通过WebView提供的接口,向JavaScript的Context(window)中注入对象或者方法,让JavaScript调用时,直接执行相应的Native代码逻辑,达到js调用Native的目的。 对于IOS的UIWebView,实例如下:
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame。jave.context@"postBridgeMessage"] = ^(NSArray<NSArray*>*calls){
//Native逻辑
}

前端调用方式
window.postBridgeMessage(message)

Android 平台

定义接口类:创建一个 Java 类,在类中定义提供给 JavaScript 调用的方法,并使用@JavascriptInterface注解声明这些方法。例如:

public class WebAppInterface {
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

注入对象:在WebView中通过addJavascriptInterface方法将该 Java 对象注入到 JavaScript 的window对象中。示例代码如下:

WebView myWebView = (WebView) findViewById(R.id.webview);
//允许Android执行JS脚本,必须要!!
myWebView.getSettings().setJavaScriptEnabled(true);
// 暴露一个JSBridge对象到webview的全局对象 即WebAppInterface实例化对象
myWebView.addJavascriptInterface(new WebAppInterface(this), "Android");

JavaScript 调用:在 JavaScript 中就可以通过window.别名.方法的形式来调用 Native 方法,如window.Android.showToast('Hello from JS')

iOS 平台

配置WKWebView:在初始化WKWebView时,创建WKWebViewConfiguration对象,配置各个接口对应的MessageHandler。示例代码如下:

// 1.iOS部分注册监听
wkWebView.configuration.userContentController.add(self, name: 方法名)
// 2.iOS部分遵守协议
func userContentController(_userContentController:WKUserContentController, didReceive message: WKScriptMessage){
    // message.body 就是传递过来的数据
    print("传来的数据为", message.body)
}

JavaScript 调用:在 JavaScript 中使用window.webkit.messageHandlers.接口名.postMessage(参数)来调用 Native 方法,如window.webkit.messageHandlers.接口名.postMessage({data: 'Hello from JS'})

代码实操:

image.png

3.Native怎么调用JS方法

相比于JavaScript调用Native, Native调用JavaScript较为简单,直接执行拼接好的JavaScript代码即可。 直接使用JS Context执行JavaScript代码 / evaluateJavaScript

Android 平台

4.4版本之前:

// mWebView = new WebView(this); //
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");

//也可以在UI线程中运行
runOnUiThread(new Runnable()){
    @override
    public void run(){
        // 通过loadUrl方法进行调用 参数通过字符串的方式传递
        mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
        // Android 中原生的弹框
        Toast.makeText(Activity名.this,"调用方法...", Toast.LENGTH_SHORT).show()
    }
}

4.4版本之后(包括4.4)

// 通过异步的方式执行JS代码,并获取返回值
mWebView.evaluateJavascript("javascript:方法名('参数1,参数2...')", new ValueCallback(){
   @Override
   // 这个方法会在执行完毕之后触发,其中value就是js代码执行的返回值(如果有的话)
   public void onReceiveValue(String value){
       //触发回调
   }
})

IOS

1.设计代理

2.实现代理方法

3.调用JS

class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler{
    // 加载完毕会触发(类似vue的生命周期钩子)
    func webView(_webView: WKWebView, didFinish navigation: WkNavigation!){
        // 类似console.log()
        print("触发啦");
        // wkWebView调用js代码,其中doSomething()会被当作js解析
        webView.evaluateJavaScript("doSomething()");
    }
}

代码实操:

image.png

image.png

4.什么是 WebView

WebView 是一种在移动应用开发中常用的组件,它提供了一个嵌入式的浏览器环境,允许开发者在原生应用中展示网页内容。借助 WebView,开发者能够把 HTML、CSS 和 JavaScript 代码集成到原生应用里,从而实现混合开发。这种方式兼具原生应用的高性能和 Web 技术的灵活性与快速迭代能力。

WebView 在不同平台的使用示例:

Android

在 Android 里,WebView 是 android.webkit 包中的一个类,能够在 Android 应用中嵌入网页。

import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webView);
        // 启用 JavaScript
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        // 设置 WebViewClient,使网页在 WebView 内加载,而非跳转到浏览器
        webView.setWebViewClient(new WebViewClient());

        // 加载网页
        webView.loadUrl("https://www.example.com");
    }

    @Override
    public void onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack();
        } else {
            super.onBackPressed();
        }
    }
}    
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>    

iOS 平台

在 iOS 中,有 WKWebView 和 UIWebView(已弃用)两种选择,建议使用 WKWebView,它具备更好的性能和安全性。

import UIKit
import WebKit

class ViewController: UIViewController, WKUIDelegate {
    var webView: WKWebView!

    override func loadView() {
        let webConfig = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfig)
        webView.uiDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let myURL = URL(string: "https://www.example.com")
        let myRequest = URLRequest(url: myURL!)
        webView.load(myRequest)
    }
}    

网页端使用 iframe 模拟 WebView 效果

在网页中,可使用 <iframe> 标签来嵌入其他网页,达到类似 WebView 的效果。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IFrame Example</title>
</head>

<body>
    <h1>Embedded Web Page</h1>
    <iframe src="https://www.example.com" width="100%" height="600"></iframe>
</body>

</html>    

5.JSBridge 的设计思想

JSBridge 是一种用于在 JavaScript 与原生代码(如 Android 的 Java、iOS 的 Objective - C 或 Swift)之间进行通信的机制。其核心设计思想在于构建一个桥梁,使得 Web 页面中的 JavaScript 代码能够调用原生代码的功能,同时原生代码也能调用 JavaScript 代码,从而实现 Web 技术与原生技术的优势互补。

在混合开发中,Web 页面擅长快速迭代、跨平台展示等,而原生代码在性能、系统交互等方面具有优势。JSBridge 就像是一个翻译官,将 JavaScript 和原生代码的请求进行相互转换和传递,让它们能够无缝协作。

具体代码实现及使用:

Android 平台

以下是一个简单的 Android 端 JSBridge 实现示例,它允许 JavaScript 调用 Android 的方法,并且 Android 也能调用 JavaScript 的方法。

import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webView);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        // 添加 JavaScript 接口
        webView.addJavascriptInterface(new AndroidInterface(), "Android");

        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("file:///android_asset/index.html");
    }

    class AndroidInterface {
        @JavascriptInterface
        public void showToast(String message) {
            // 显示 Toast
            android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show();
        }

        public void callJavaScript() {
            // 调用 JavaScript 方法
            webView.loadUrl("javascript:showMessage('Hello from Android')");
        }
    }
}    
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSBridge Example</title>
</head>

<body>
    <button onclick="callAndroid()">Call Android Method</button>
    <script>
        function callAndroid() {
            // 调用 Android 方法
            Android.showToast('Hello from JavaScript');
        }

        function showMessage(message) {
            alert(message);
        }
    </script>
</body>

</html>    

iOS 平台

以下是一个使用 Swift 和 WKWebView 实现的 iOS 端 JSBridge 示例。

import UIKit
import WebKit

class ViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
    var webView: WKWebView!

    override func loadView() {
        let webConfig = WKWebViewConfiguration()
        let userContentController = WKUserContentController()
        userContentController.add(self, name: "iOSBridge")
        webConfig.userContentController = userContentController
        webView = WKWebView(frame: .zero, configuration: webConfig)
        webView.uiDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let myURL = URL(fileURLWithPath: Bundle.main.path(forResource: "index", ofType: "html")!)
        let myRequest = URLRequest(url: myURL)
        webView.load(myRequest)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "iOSBridge" {
            if let body = message.body as? String {
                // 处理 JavaScript 传来的消息
                print("Received from JavaScript: \(body)")
                // 调用 JavaScript 方法
                webView.evaluateJavaScript("showMessage('Hello from iOS')") { (result, error) in
                    if let error = error {
                        print("Error calling JavaScript: \(error)")
                    }
                }
            }
        }
    }
}    
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSBridge Example</title>
</head>

<body>
    <button onclick="calliOS()">Call iOS Method</button>
    <script>
        function calliOS() {
            // 调用 iOS 方法
            window.webkit.messageHandlers.iOSBridge.postMessage('Hello from JavaScript');
        }

        function showMessage(message) {
            alert(message);
        }
    </script>
</body>

</html>    

代码解释:

Android 端

  • 在 MainActivity 中,通过 addJavascriptInterface 方法将 AndroidInterface 类注入到 WebView 中,JavaScript 可以通过 Android 对象调用 AndroidInterface 中的方法。
  • AndroidInterface 类中的 showToast 方法用于显示 Toast,callJavaScript 方法用于调用 JavaScript 的 showMessage 方法。

iOS 端

  • 在 ViewController 中,使用 WKUserContentController 的 add 方法添加一个消息处理程序,监听名为 iOSBridge 的消息。
  • 当 JavaScript 调用 window.webkit.messageHandlers.iOSBridge.postMessage 时,userContentController(_:didReceive:) 方法会被触发,处理接收到的消息并可以调用 JavaScript 方法。

Web 页面

  • 在 HTML 文件中,通过 JavaScript 调用 Android 或 iOS 的方法,同时提供供原生代码调用的 JavaScript 方法。

6.常见的混合式App框架

混合框架的职责:

  • 提供前端运行环境
  • 实现前端和原生交互
  • 封装原生功能,提供插件机制 image.png

以下是常用的混合App框架:

打包工具:Cordova, PhoneGap

  • Cordova:实际上做的是封装的作用,它把我们原生App里面的WebView,提供给我们的前端代码,让它可以进行Html解析和js的运行,因为WebView自带有一个JS引擎。Cordova提供了一些插件,前端代码可以通过这些插件访问到底层的硬件设备
  • 安装Cordova,并基于Cordova创建项目,基于Cordova安装平台和插件。默认情况下 cordova create 会生成基于Web的骨架应用程序,其起始页是项目的www/index.html文件。

云平台:DCloud(Uni-app基于Vue), AppCan, ApiCloud, wex5

框架:

  • Ionic: 免费和开源,Ionic提供了一个移动和桌面优化的HTML,CSS和JS组件库,用于构建高度交互的应用程序。与Angular, React, Vue或纯JavaScript一起使用

image.png

  • Flutter:是一款移动应用SDK,可帮助开发人员和设计人员构建适用于iOS和Android的现代移动应用。
  • React Native:使您能够使用基于JavaScript和React一致开发人员体验在本机平台上构建世界级的应用程序体验。学习一次,到处写作。
  • Xamarin:Xamarin的基于Mono的产品使.NET开发人员能够使用它们现有的代码,库和工具(包括Visual Studio * )以及.NET和C#编程语言的技能,为业界最广泛使用的移动设备创建移动应用程序设备,包括基于Android的智能手机和平板电脑,iPhone, iPad和iPod Touch

跨端应用:Electron: 由Github出品、维护的跨平台桌面应用开发框架。

7.移动开发写meta标签和不写meta标签的区别

设备独立像素DPI。设备独立像素的出现使得即便在高清屏下,也可以让元素有正常的尺寸,让代码不受设备的影响,它是设备厂商根据屏幕特性设置的,无法更改。

设备独立像素和物理像素的关系:

  • 普通屏幕下,一个设备独立像素对应一个物理像素
  • 高清屏幕下,一个设备独立像素对应N个物理像素

设备独立像素和css像素关系:

  • 在无缩放的情况下(标准情况):1css像素=1设备独立像素

像素比(dpr)单一方向上物理像素设备独立像素的比例,即:dpr=物理像素/设备独立像素 可以通过以下方式获取:

window.devicePixelRatio

开启理想视口的方法:

// 让布局视口的宽度和设备独立像素的宽度一致
<meta name="viewport" content="width=device-width" />

不写meta标签(不符合理想视口标准):

1.描述屏幕:物理像素:750px、设备独立像素:375px、css像素:980px

2.优点:元素在不同设备上呈现效果几乎一样,因都是通过布局容器等比缩放的,例如200宽的盒子:200/980

3.缺点:元素太小,页面文字不清楚,用户体验不好。

写meta标签(符合理想视口标准):

1.描述屏幕:物理像素750px、设备独立像素375px、css像素375

2.优点:

  • 页面清晰展现,内容不再小到难以观察,用户体验较好
  • 更清晰的像素关系:布局视口=视觉视口=设备独立像素=375px
  • 更清晰的dpr,即dpr = 物理像素/设备独立像素 = 物理像素/css像素。例如:dpr=2的设备,1 * 1css像素 = 1 * 1设备独立像素 = 2 * 2物理像素

3.缺点同一个元素,在不同屏幕(设备)上,呈现效果不一样,例如375宽的盒子:375/375和375/414(不是等比显示)

4.解决缺点:做适配。

8.主流的适配方式

前提都是开启理想视口 因为开启了理想视口,布局视口宽度才等于设备独立像素值宽度。

1.viewport 适配(有些问题,比如边框变粗,比如有些浏览器不支持width=375具体的值)

<meta name="viewport" content="width=375" />
  • 方法:拿到设计稿之后,设置布局视口的宽度为设计稿宽度,然后直接按照设计稿给宽高进行布局即可。
  • 优点:不用复杂的计算,直接使用图稿上标注的px值
  • 缺点:
    • 不能使用完整的meta标签,会导致某些android手机上有兼容性的问题
    • 不希望适配的东西,如边框,也强制参与了适配
    • 图片会失真

2.rem适配(主流方式,几乎完美适配)

  • 方案一
    • 根字体 =(手机横向设备独立像素值 * 100)/ 设计稿宽度
    • 编写样式时:直接以rem为单位,值为:设计值 / 100

image.png

image.png

  • 方案二
    • 根字体 = 设备横向独立像素值 / 10

    • 编写样式时:以rem为单位,值为:设计值 / (设计稿宽度 / 10) 通过EaseLess等工具,帮忙计算: image.png

3.vw适配(一定是以后的趋势,但目前兼容性不太好)

1vw = 布局视口宽度的1%

1wh = 布局视口高度的1%

9. 1物理像素边框

image.png

image.png

10.移动端事件与事件对象

移动端事件列表

  • touchstart 元素上触摸开始时触发
  • touchmove 元素上触摸移动时触发
  • touchend 手指从元素上离开时触发
  • touchcancel 触摸被打断时触发

这几个事件最早在IOS safari中,为了向开发人员传达一些特殊的信息。

注意:

  • touchmove 事件触发后,即使手指离开了元素,touchmove 事件也会持续触发
  • 触发 touchmove 与 touchend 事件,一定要先触发touchstart
  • 事件的作用在于实现移动端的界面交互
  • 移动端touchstart 事件结束后会默认触发元素的 click事件,touchstart 和 click 之间的时间差大小跟是否开启理想视口有关,如果开启了理想视口,则时间差在50ms - 80ms之间,但如果没有开启理想视口,则时间差有300ms之多。

事件对象事件对象 事件对象里边的关键字段:

  • touches:屏幕上出现的触点数
  • targetTouches:当前元素上的触点数
  • changedTouches:同时按下几个手指

注意:点击穿透 touch事件触发后会默认的触发元素的click事件。如果touch事件隐藏了元素并且该元素底下有其他元素能承接这个click事件,则click动作将作用到新的元素,触发新元素的click事件或页面跳转,此现象称为事件穿透。

解决点击穿透方案:

  • 阻止默认行为,即调用事件的e.preventDefault()
  • 使背后元素不具备click特性,用touchXXX代替,如touchstart
  • 让背后的元素暂时失去click事件,300ms左右再复原,例如style 的 pointerEvents为none,setTimeout设置300ms后pointerEvents设置为auto
  • 让隐藏的元素延迟300ms左右后再隐藏