H5与原生混合开发总结

简介: H5与原生混合开发总结

一、引言

本文主要针对H5与原生混合开发中的交互问题进行讨论,当然,这仅仅是鄙人的见解,求同存异。

本文主要针对以下问题进行总结:

  1. 如何实现JSAndriod的交互?
  2. 针对WebView启动慢问题,如何优化?
  3. 如果存在多个H5模块包,如何实现模块包的完全更新与部分更新?
  4. 针对以上问题的,如何建立一个公用的工具集(框架?)?
  5. 遇到的问题及解决办法。'

OK, 开始吧!

二、交互

关于如何实现JSAndroid交互,其实看官方的 Building web apps in WebView 这篇文章就够了,如果你觉得英文不好理解,那也没关系,因为接下来的内容会覆盖这些技术点。

  • 交互模型:

image.png

其实这里可以进一步将Webview抽象化,那么就得到了如下图关系:

image.png

显然这里的问题就是如何实现JsExecutorJsInterfaces了。

对于JsExecutor而言(Android调用JS,其实是比较固定的写法,比如,如果我们想要动态获取网页中某个标签的html,那么会这么写:

// 先假设id参数为content
Stirng elementId = "content";
String jsCode = "javascript:document.getElementById(\" + elementId +\").innerHtml"; 
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String html) {
                // ...
            }
        }); 

这种写法是固定的,但是方法参数比较多时就比较蛋疼了,拼凑方法名和多个参数是很烦人的,且容易出错,因而我们可以抽象出以下工具类:

/**
 * @Author horseLai
 * CreatedAt 2018/10/22 17:42
 * Desc: JS 代码执行器,包含通过WebView执行JS代码的通用方法。
 * Update:
 */
public final class JsExecutor {
    private static final String TAG = "JsExecutor";
    private JsExecutor() {
    }
    /**
     * JS方法不带参,且无返回值时用此方法
     *
     * @param webView
     * @param jsCode
     */
    public static void executeJsRaw(@NonNull WebView webView, @NonNull String jsCode) {
        executeJsRaw(webView, jsCode, null);
    }
    /**
     * JS方法带参,且有返回值时用此方法
     *
     * @param webView
     * @param jsCode
     * @param callback
     */
    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 { 
            // 注意这里,这种方式没有直接的结果回调,不过可以迂回解决,比如我们可以
            // 执行JS的一个固定的方法,并传入类型参数,然后在JS方法中根据这个类型参
            // 数去匹配方法并执行,执行完成后再调用我们注入的相应回调方法将结果传回
            // 来,这样就可以解决结果回调问题了,如果要适配 Android 4.4 以下的版本则可以这么做。
            webView.loadUrl(jsCode);
        }
    } 
    /**
     * JS方法带参,且有返回值时用此方法
     *
     * @param webView
     * @param methodName
     * @param callback
     * @param params
     */
    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);
    }
    /**
     * JS方法带参,且无返回值时用此方法
     *
     * @param webView
     * @param methodName
     * @param params
     */
    public static void executeJs(@NonNull WebView webView, @NonNull CharSequence methodName, @NonNull CharSequence... params) {
        executeJs(webView, methodName, null, params);
    } 
}  

这里直接将WebView视为我们执行JS代码的工具,如下示例是给H5传递当前网络类型,由于整合了JS代码的拼接过程,因此只需要传入具体方法名称和方法的字符串参数即可。

image.png

  • 对于JsInterfaces(JS调用Android) , 我们需要在我们需要注入的方法前加上注解@JavascriptInterface才能将方法暴露出去,然后将包含此方法的类对象注入进去,如下一个实际场景, H5需要从Android原生中获取用户的账号信息,那么可以这么写:

先注入包含对应方法的H5JsStorage类对象:image.png

其中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最后一行记录下结束时间,然后计算时间差,作为衡量启动速度的参照,多次测试,记录时间差。结果如下:

//---------------------------------------------
// 不做任何处理, Mi6 android 8.0
I/Main2Activity: onStart: total cost:150 ms
I/Main2Activity: onStart: total cost:44 ms
I/Main2Activity: onStart: total cost:33 ms
I/Main2Activity: onStart: total cost:54 ms
I/Main2Activity: onStart: total cost:35 ms
I/Main2Activity: onStart: total cost:34 ms
I/Main2Activity: onStart: total cost:34 ms 
//  优化后  初始化耗时
I/MyWebViewHolder: prepareWebView: total cost: 131 ms
I/MyWebViewHolder: prepareWebView: total cost: 121 ms
I/MyWebViewHolder: prepareWebView: total cost: 121 ms
I/MyWebViewHolder: prepareWebView: total cost: 117 ms
I/MyWebViewHolder: prepareWebView: total cost: 110 ms
I/MyWebViewHolder: prepareWebView: total cost: 116 ms
I/MyWebViewHolder: prepareWebView: total cost: 116 ms
// 之后耗时
I/Main2Activity: onStart: total cost:26 ms
I/Main2Activity: onStart: total cost:20 ms
I/Main2Activity: onStart: total cost:22 ms
I/Main2Activity: onStart: total cost:17 ms
I/Main2Activity: onStart: total cost:19 ms
I/Main2Activity: onStart: total cost:21 ms
//---------------------------------------------
// 模拟器 android 9.0
I/Main2Activity: onStart: total cost:292 ms
I/Main2Activity: onStart: total cost:50 ms
I/Main2Activity: onStart: total cost:49 ms
I/Main2Activity: onStart: total cost:54 ms
I/Main2Activity: onStart: total cost:43 ms
I/Main2Activity: onStart: total cost:47 ms
I/Main2Activity: onStart: total cost:39 ms
I/Main2Activity: onStart: total cost:41 ms 
// 优化后  初始化耗时
I/MyWebViewHolder: prepareWebView: total cost: 177 ms
I/MyWebViewHolder: prepareWebView: total cost: 169 ms
I/MyWebViewHolder: prepareWebView: total cost: 183 ms
I/MyWebViewHolder: prepareWebView: total cost: 159 ms
// 之后 耗时
I/Main2Activity: onStart: total cost:40 ms
I/Main2Activity: onStart: total cost:27 ms
I/Main2Activity: onStart: total cost:34 ms
I/Main2Activity: onStart: total cost:34 ms
I/Main2Activity: onStart: total cost:33 ms
I/Main2Activity: onStart: total cost:30 ms
//---------------------------------------------
//  MT6592 android 4.4 不做处理
I/Main2Activity: onStart: total cost:141 ms
I/Main2Activity: onStart: total cost:46 ms
I/Main2Activity: onStart: total cost:43 ms
I/Main2Activity: onStart: total cost:42 ms
I/Main2Activity: onStart: total cost:44 ms
I/Main2Activity: onStart: total cost:46 ms  
// 优化后 初始化耗时
I/MyWebViewHolder: prepareWebView: total cost: 182 ms
I/MyWebViewHolder: prepareWebView: total cost: 50 ms
I/MyWebViewHolder: prepareWebView: total cost: 54 ms
I/MyWebViewHolder: prepareWebView: total cost: 53 ms
I/MyWebViewHolder: prepareWebView: total cost: 54 ms
I/MyWebViewHolder: prepareWebView: total cost: 56 ms
// 之后耗时
I/Main2Activity: onStart: total cost:36 ms
I/Main2Activity: onStart: total cost:34 ms
I/Main2Activity: onStart: total cost:30 ms
I/Main2Activity: onStart: total cost:31 ms
I/Main2Activity: onStart: total cost:32 ms 

根据以上结果可以看出,优化后要比优化前的启动速度快个10~20秒,且抖动较小。可以注意到其中包含一个叫做prepareWebView的时间差,据此,聪明的你肯定能想到我所谓的优化是做了什么操作。嗯~,其实就是使用WebView之前,在合适的地方和时机先将其初始化,之后复用这个创建好的实例,这里我是这么写的:

/**
 * @Author horseLai
 * CreatedAt 2018/12/10 10:11
 * Desc: 用于持有MyWebView实例,减少每次都重新创建和销毁造成的开销
 * Update:
 */
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;
    }
    /**
     * 务必在使用WebView前调用此方法进行初始化
     *
     * @param context
     */
    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();
            // mWebView.clearHistory();
            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);
            // 在WebView上层覆盖一个用于提示如错误等信息的布局层,
            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);
        }
    }
}

然后在合适的地方初始化:

image.png

添加到布局中:

image.png

onDestroy时从界面中解除绑定:

image.png

2. 多模块包自动更新

支持多模块自动更新的目的是方便更新维护,减少用户升级所带来的流量开支,每个模块包之间可以是相互独立的,也方便于团队开发,仅需要和前端约定好文件目录即可。

先来看看H5模块的自动更新流程(完整更新):

V9WH8URYJBR%A7CSQXR6}@E.png

上面是模块包的完整更新过程,还可以进行补丁更新,而所谓补丁更新就是,下载的更新包中仅仅包含需要更新的文件,因而对应于上面流程而言,就是少了删除本地旧版本文件的过程,而直接解压替换对应文件。这种更新方式有以下优缺点:

可以极大的减少更新时对用户的流量消耗,且速度极快。

但是需要前端明确抽取所更新的文件,否则会出现问题,可能这个过程会繁琐点。

如果使用类似于VueJs这种模板框架编写的界面,因为需要编译为JS代码,然后仅剩一个index.html入口,导致抽取定位繁琐,且每次编译出来的文件名可能不一样,因此不能使用补丁更新这种方式,只能分包,然后进行完整更新。

具体代码比较多,就补贴了,请看 github这里, 其中H5ManagerSettingsH5Manager配置信息与无关逻辑的抽离类。

四、建立公用工具集

上面已经逐个介绍了混合开发中交互与更新的逻辑,工具集已经放到 githubH5MixDevelopTools,感兴趣的童鞋可以看看,虽然这里我并没有把JS接口和html界面放上去。image.png

遇到的实际问题与解决办法:(以项目中使用VueJs作为模板引擎来编写H5页面为例)

1. 界面加载不出来,显示空白,怎么办?

解决办法:给WebView加上下面配置即可

image.png

2. 联调时发现总是找不到定义的交互接口方法,怎么办?

原因与解决办法:首先,默认情况下,VueJs在对代码进行混淆处理,因此如果你遇到了这个问题,那么请手动配置以关闭混淆(具体做法请自行查找吧)。如果已经不混淆了,但是依然找不到对应的方法,怎么办?我和我的小伙伴是将接口文件放到components中将其视作一个组件来使用的,然后具体到接口方法的话,将方法挂到window对象下,如下示例:

// 挂载方法
window.showToast = function(msg){
      UI.showToast(msg);
} 
// 挂载变量,挂载在window的变量可以在全局直接引用
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的方法。

/**
 *  相册中获取图片、相机拍照结果回调
 * @param {Number} type   类型: 0->图库, 1->相机
 * @param {String} imgFilePath  
 */
window.selectedImgFile = [];  // 模拟<input>选择文件后的存储形式,用于上传
window.selectedImgFileUrls = []; // 将图片路径转换成<img>能够预览的路径
window.onPictureResult = function (type, imgFilePath) {
    // 注意这里
    selectedImgFile.push(new File([""], imgFilePath, {type:"image/*"}));
    selectedImgFileUrls.push({
        imgUrl: "file://" + imgFilePath
    });
}

上面selectedImgFile,selectedImgFileUrls这两个挂载到window的变量,这两个数组可以直接在全局引用了,记得在使用后清空,不然会影响到下次使用。

嗯,看起来很完美,选图、预览很完美,但很快你就会发现这实际是个BUG,BUG在哪里呢?注意到上面的new File([""], imgFilePath, {type:"image/*"}),这么使用会导致上传到服务器的图片大小为 0kb, 为啥呢?因为第一个参数[""]实际是图片的实际数据(字节数组),它的长度代表着文件的大小,因此,上面这样做虽然能够预览,但是无法仅仅直接通过一个本地路径就读取到文件流数据,也就不能上传成功了。

怎么办呢?思考了很久,发现自己一直困在JS如何通过一个本地路径建立File并上传的思维当中,于是找前端和后台的小伙伴交流,最终确定的方案是:选择图片后先将图片编码成Base64字符串再注入到JS处理,JS端收到数据后进行图片数据绑定,以及上传到服务器,服务器端进行Base64解码处理,然后保存成本地图片。

于是可以稍微修改成这样:

上面selectedImgFile,selectedImgFileUrls这两个挂载到window的变量,这两个数组可以直接在全局引用了,记得在使用后清空,不然会影响到下次使用。
嗯,看起来很完美,选图、预览很完美,但很快你就会发现这实际是个BUG,BUG在哪里呢?注意到上面的new File([""], imgFilePath, {type:"image/*"}),这么使用会导致上传到服务器的图片大小为 0kb, 为啥呢?因为第一个参数[""]实际是图片的实际数据(字节数组),它的长度代表着文件的大小,因此,上面这样做虽然能够预览,但是无法仅仅直接通过一个本地路径就读取到文件流数据,也就不能上传成功了。
怎么办呢?思考了很久,发现自己一直困在JS如何通过一个本地路径建立File并上传的思维当中,于是找前端和后台的小伙伴交流,最终确定的方案是:选择图片后先将图片编码成Base64字符串再注入到JS处理,JS端收到数据后进行图片数据绑定,以及上传到服务器,服务器端进行Base64解码处理,然后保存成本地图片。
于是可以稍微修改成这样:

不过这里依然可能存在一些问题,比如内存溢出,因为图片本身可能很大,尤其是使用相机直接拍照取图的情况,一张图片可能会有3~10M,直接编码为图片本身会比较耗时,而编码出来的字符串会存在于内存中,因此很有可能会导致Android端出现内存溢出的情况,因此这里可以考虑先压缩后编码,这样可以降低内存耗尽的几率。

五、总结

本文基于实际项目,介绍了混合开发中JS与原生交互的实现,然后以一个小实验测试了含WebView的Activity的启动速度,优化,然后测试优化后的启动速度,接着介绍了H5分模块更新的逻辑,最后整理了一套工具集,感兴趣的童鞋可以看看 H5MixDevelopTools,欢迎指正。

使用H5混合开发确实能够提升开发速度,但是实际体验确实一般,适合非常追求开发速度的场景。


目录
相关文章
|
6月前
|
移动开发 开发框架 JavaScript
uniapp如何与原生应用进行混合开发?
uniapp如何与原生应用进行混合开发?
501 0
|
24天前
|
存储 前端开发 安全
Tauri 开发实践 — Tauri 原生能力
本文介绍了如何使用 Tauri 框架构建桌面应用,并详细解释了 Tauri 提供的原生能力,包括文件系统访问、系统托盘、本地消息通知等。文章通过一个具体的文件下载示例展示了如何配置 Tauri 来使用文件系统相关的原生能力,并提供了完整的代码实现。最后,文章还提供了 Github 源码链接,方便读者进一步学习和参考。
60 1
Tauri 开发实践 — Tauri 原生能力
|
4天前
|
Dart Android开发 开发者
Flutter跨平台开发实战:构建高性能移动应用
【10月更文挑战第25天】随着移动设备种类的增加,开发者面临跨平台应用开发的挑战。Flutter作为Google推出的开源UI工具包,凭借其强大的跨平台能力和高效的开发效率,成为解决这一问题的新方案。本文将介绍Flutter的核心优势、实战技巧及性能优化方法,通过一个简单的待办事项列表应用示例,帮助读者快速上手Flutter,构建高性能的移动应用。
18 0
|
2月前
|
移动开发 前端开发 JavaScript
构建高效跨平台移动应用:React Native入门指南
【8月更文挑战第47天】在移动开发领域,React Native凭借其跨平台特性和高效的开发模式赢得了开发者的青睐。本文将通过一个简易的待办事项应用实例,带领读者快速入门React Native,并展示如何利用JavaScript和React框架构建具有原生性能的应用。我们将探讨环境配置、界面布局、状态管理和数据流,以及如何打包和发布您的应用。准备好,让我们开始React Native之旅!
70 20
|
6月前
|
XML JSON 缓存
原生AXAJ
本文介绍了原生AJAX的特性,其主要优点是能无刷新获取数据,但也存在跨域问题。接着讲解了Express框架的基本使用,包括创建应用、定义路由和启动服务。然后展示了如何通过AJAX从服务器获取并显示JSON数据,处理了IE的缓存问题和网络延时,以及如何处理重复请求。最后提到了通过设置超时和错误处理来改善用户体验。
55 1
|
6月前
|
开发框架 前端开发 JavaScript
【专栏】对比分析两种流行的跨平台开发框架——Flutter和React Native,探讨它们的优势、劣势以及适用场景
【4月更文挑战第27天】本文对比分析了Flutter和React Native两大跨平台移动开发框架。Flutter,由Google推出,以其接近原生的性能、快速启动和流畅滚动受青睐,适合高性能和高度定制的项目。React Native,Facebook维护,依赖JavaScript,虽性能受限,但热重载优势和丰富第三方库使其适合快速迭代的项目。两者都在拓展多平台应用,Flutter在桌面和Web,React Native在Windows。选择框架需考虑项目需求、团队技能和性能效率平衡。
355 1
|
6月前
|
开发框架 前端开发 Android开发
移动应用开发的未来:跨平台框架与原生系统整合
【5月更文挑战第21天】 随着移动互联网的蓬勃发展,移动应用成为日常生活与业务操作的核心。本文探讨了移动应用开发的最新趋势,特别是跨平台开发框架和原生操作系统之间的融合。分析了Flutter、React Native等流行框架的技术优势及局限,并深入讨论如何将这些框架与iOS和Android系统特性有效整合,以提升性能和用户体验。文章还预测了未来移动应用开发可能面临的挑战和机遇。
|
6月前
|
开发框架 Android开发 开发者
构建未来:使用Flutter框架开发跨平台移动应用
【4月更文挑战第24天】 在移动应用的世界中,Android和iOS一直占据主导地位。然而,开发者经常面临一个难题:如何高效地为这两个平台构建和维护应用。传统的解决方案是分别为每个平台编写和维护独立的代码库,这不仅耗时且低效,还增加了开发成本。本文将深入探讨如何使用Google的Flutter框架来构建高性能、美观且能够在Android和iOS上无缝运行的跨平台移动应用。通过分析Flutter的架构、核心组件以及开发优势,我们将了解为何Flutter成为当今市场上最具潜力的跨平台解决方案。
|
移动开发 小程序 安全
原生小程序Donut多端开发
随着移动应用开发的不断发展,跨平台开发成为了一个备受关注的话题。开发人员希望能够使用一套代码库,同时支持多个平台,从而提高开发效率并覆盖更广泛的用户群体。在小程序领域,微信 Donut 多端(也称为原生小程序 Donut 多端)为开发人员提供了一种全新的跨平台开发方式。本文将介绍 Donut 多端的概念、特点以及如何开始使用它来开发跨平台小程序。
244 0
|
6月前
|
移动开发 开发框架 前端开发
移动端混合开发技术探析:React Native、Weex、Flutter的比较与选择
随着移动应用开发的高速发展,混合开发技术成为了一种备受关注的选择。本文将对移动端混合开发技术中的React Native、Weex和Flutter进行比较与探讨,分析它们在性能、开发体验、生态系统和跨平台支持等方面的差异,以及如何根据项目需求进行选择。
212 1