Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

「Android」 详细全面的基于vue2.0Weex接入过程(Android视角) #25

Open
or0fun opened this issue Mar 10, 2017 · 1 comment
Labels

Comments

@or0fun
Copy link

@or0fun or0fun commented Mar 10, 2017

本文来自尚妆Android团队路飞
发表于尚妆github博客,欢迎订阅!

一、说在前面的话

目前weex已在尚妆旗下的达人店app上线了一个常用的订单管理页面,截止目前Android上未发现问题,渲染时间在100-300ms之间。

作为Android开发,此文首先会从Android的角度为主来记录接入的过程,希望给未接入的同学更方便省时地接入weex提供一点帮助。其中会涉及到预加载降级热更新埋点以及在app不更新的情况下动态配置新页面等问题,这些Android和iOS都是统一的逻辑,希望和大家一起交流。前端方面可以参考我同事写的《基于vue2.0的weex实践(前端视角)》,iOS可以参考我同事写的weex 实践(iOS 视角)

二、Android接入过程

其实对于module、component的定义,以及IWXImgLoaderAdapter、IWXHttpAdapter等adapter的重写,在playgroud和weexteam里都已经有很好的例子了。

1、gradle依赖

compile 'com.taobao.android:weex_sdk:0.10.0’

compile 'com.android.support:support-v4:24.0.0'
compile 'com.android.support:appcompat-v7:24.0.0'
compile 'com.android.support:recyclerview-v7:24.0.0'

compile 'com.squareup.okhttp:okhttp:2.3.0'
compile 'com.squareup.okhttp:okhttp-ws:2.3.0'

compile 'com.alibaba:fastjson:1.2.8'

//(可选)支持调试的依赖,参考https://github.com/weexteam/weex-devtools-android/blob/master/README-zh.md  
compile 'com.taobao.android:weex_inspector:0.0.8.5'
compile 'com.google.code.findbugs:jsr305:2.0.1'
compile 'com.taobao.android:weex_inspector:0.0.8.5'

2、新建weex module

在原来的project上,新建单独的 weex module。代码结构如下:
Alt text

3、初始化weex

通过类WeexManager来统一管理weex相关的配置,以下是WeexManager里的init函数的主要内容,在application的onCreate里调用:

public void init(Application application, IWeexService weexService) {

        //通过在线参数控制是否使用weex,ConfigManager是尚妆的在线参数模块,以后有机会再简单介绍一下
        if (!ConfigManager.getBoolean(CONFIG_WEEX_ENABLE, true)) {
            return;
        }

        context = application.getApplicationContext();
        weexDir = context.getDir(WEEX_MODULE, Context.MODE_PRIVATE);

        //根据需要注册图片、网络、存储等adapter
        WXSDKEngine.initialize(application,
                new InitConfig.Builder()
                        .setImgAdapter(new FrescoImageAdapter())
                        .setUtAdapter(new UserTrackAdapter())
                        .setStorageAdapter(new StorageAdapter())
                        .setHttpAdapter(new OkHttpAdapter())
                        .setURIAdapter(new CustomURIAdapter())
                        .build());

        this.weexService = weexService;

        //获取本地缓存的weex js配置
        configList = WXJsonUtils.getList(SHStorageManager.get(WEEX_MODULE, WEEX_CONFIG, ""), WeexConfig.class);
        update();

        try {
            //页面通用的一些接口
            WXSDKEngine.registerModule("shopBase", ShopModule.class);
            //主要是a标签的跳转
            WXSDKEngine.registerModule("event", WXEventModule.class);
            //模态对话框
            WXSDKEngine.registerModule("shopModal", ModalModule.class);
            //用fresco重写图片组件
            WXSDKEngine.registerComponent("image", FrescoImageComponent.class);
        } catch (WXException e) {
            LogUtils.e(e);
        }

        SHEventBus.register(ModuleName.WEEX, "weexDebugHost", new ISHEventBusCallback<String>() {
            @Override
            public void handle(String debugHost, String s) {
                if (!TextUtils.isEmpty(s)) {
                    LogUtils.e(s);
                    return;
                }
                if (TextUtils.isEmpty(debugHost)) {
                    WXEnvironment.sRemoteDebugMode = false;
                } else {
                    WXEnvironment.sRemoteDebugMode = true;
                    WXEnvironment.sRemoteDebugProxyUrl = "ws://" + debugHost + "/debugProxy/native";
                }
                WXSDKEngine.reload();
            }
        });

        SHEventBus.register(ModuleName.WEEX, "netChanged", new ISHEventBusCallback<Boolean>() {
            @Override
            public void handle(Boolean result, String s) {
                if (!TextUtils.isEmpty(s)) {
                    LogUtils.e(s);
                } else {
                    if (result.booleanValue()) {
                        update();
                    }
                }
            }
        });

        //获取weex配置,更新js文件
        weexConfigRequest.setCallBack(new IRequestCallBack<SHResponse<List<WeexConfig>>>() {
            @Override
            public void onResponseSuccess(SHResponse<List<WeexConfig>> response) {
                if (response.isSuccess && null != response.data) {
                    SHStorageManager.putToDisk(WEEX_MODULE, WEEX_CONFIG, JsonUtils.toJson(response.data));
                    configList = response.data;
                    update();
                }
            }

            @Override
            public void onResponseError(int i) {

            }
        });
        weexConfigRequest.start();

    }

1)考虑到第一次接入weex,有点担心兼容问题,万一引起崩溃等不确定因素,所以这里做了一个开关。其实每接入一个新的sdk都最好有个控制开关,以避免因为不确定因素导致不稳定。

2)weexDir是js的下载存储路径,为了加快页面打开时间,会对js进行预加载到本地

3) sdk对a标签的处理只调用了"event"的openURL接口,但是却没有注册"event"。所以需要自己实现WXEventModule,并注册。

4)模态对话框ModalModule的实现参考sdk里的WXModalUIModule

5)FrescoImageAdapterFrescoImageComponent的实现依赖我们开源的SHImageView支持webp,支持压缩,支持没有协议的链接(忽略协议可以让浏览器根据页面时http或者https自动选择使用的协议,从而避免了网站改为https的情况下仍然访问http资源而无法访问的问题。)

6)OkHttpAdapter的实现参考github上zjutkz同学的实现 OkHttpAdapter,感谢,经过改写,支持没有协议的链接,支持cookie

7)ShopModule是自定义的Module,定义通用的一些接口,比如设置title bar是否显示,以及title bar的title;关闭当前页面,分享,错误日志收集等。

8)UserTrackAdapter用于埋点,另外可以在ShopModule里自定义接口收集埋点、错误信息等。

9)CustomURIAdapter用于支持相对地址,具体实现参见以下:

public class CustomURIAdapter implements URIAdapter {
    @NonNull
    @Override
    public Uri rewrite(WXSDKInstance instance, String type, Uri uri) {
        if (null == uri) {
            return null;
        }
        String url = uri.toString();
        if (url.startsWith("http")) {
            return uri;
        }else if (url.startsWith("//")) {
            if (SHStorageManager.get("APP", "https", true)) {
                url = "https:" + url;
            }else {
                url = "http:" + url;
            }
        }else {
            url = SHHost.getMobileHost() + url;
        }
        return Uri.parse(url);
    }
}

4、新建统一的weex页面

这边考虑到以后页面有可能嵌入到其他activity,所以把weex的渲染放入新建的WeexFragment。然后新建WeexActivity来引用该WeexFragment 。所有的单独页面的weex渲染都使用这个WeexActivity,非单独页面的使用weexFragment,这样新加页面时,无需重新注册activity。weex处理逻辑统一,方便管理,方便动态配置。通过统一跳转协议跳转到WeexActivity,通过intent传入两个参数url和h5

showjoyshop://page.sh/weex

intent参数:
url:js链接,可以是本地的存储地址/sdcard/com.showjoy.shop/weex/order.js,也可以是线上链接 https://xxxxx/0.4.3/order.js

h5:用来降级的h5页面链接,当渲染失败时,会跳转到该h5页面

5、开始渲染js,失败后降级到h5

首先实例化WXSDKInstance

wxInstance = new WXSDKInstance(activity);
wxInstance.registerRenderListener(this);
wxInstance.onActivityCreate();
registerBroadcastReceiver();

1)当前类实现接口IWXRenderListener,可以参考weexteam里的AbsWeexActivity实现

2)注册的广播是DefaultBroadcastReceiver,可以可以参考weexteam里的AbsWeexActivity实现

然后讲一下渲染,支持本地js以及线上js

if (url.startsWith("http")) {
    wxInstance.renderByUrl(
            getPageName(),
            url,
            options,
            jsonInitData,
            CommonUtils.getDisplayWidth(activity),
            CommonUtils.getDisplayHeight(activity),
            WXRenderStrategy.APPEND_ASYNC);

}else {
    new Thread(new Runnable() {
        @Override
        public void run() {
            String file = WeexUtils.readFile(url);
            handler.sendMessage(handler.obtainMessage(LOAD_LOCAL_FILE, file));
        }
    }).start();
}

其中,getPageName()自定义即可,getDisplayWidth和getDisplayHeight获取屏幕宽高。

传入本地的存储地址时,先读取文件,然后同个Handler在UI线程渲染,如下:

接收LOAD_LOCAL_FILE后handler里的实现:

case LOAD_LOCAL_FILE:
                    if (activity.getLifeState() != LifeState.DESTORY ) {
                        if (wxInstance != null) {
                            String content = (String) msg.obj;
                            if (TextUtils.isEmpty(content)) {
                                SHJump.openUrl(activity, h5Url);
                                finishActivity();
                            }else {
                                wxInstance.render((String) msg.obj, null, null);
                            }
                        }
                    }
                    break;

这里getLifeState()是我们自己BaseActivity的实现,可以自行判断。SHJump和finishActivity都是自己的实现,大家自己实现即可。

渲染回调的实现,按需要处理即可,渲染成功后隐藏loading,view创建后添加view。渲染异常时降级跳转到h5。如下:

Override
public void onViewCreated(WXSDKInstance instance, View view) {
    //viewMap.put(weexJsUrl, view);
    addWeexView(view);
}
@Override
public void onRenderSuccess(WXSDKInstance instance, int width, int height) {
    toHideLoading();
}
@Override
public void onRefreshSuccess(WXSDKInstance instance, int width, int height) {
    toHideLoading();
}
@Override
public void onException(WXSDKInstance instance, String errCode, String msg) {
    LogUtils.e("weex exception:", errCode, msg);
    SHJump.openUrlForce(activity, h5Url);
    finishActivity();
}

6、多个js在同个页面渲染

为了实现如图的tab,一开始在.vue文件里使用tabbar组件,后来发现在Android机型适配上不够好。于是后来就将两个tab做成两个页面,生成两个js文件。首先渲染“我的订单.js”,生成如下的界面。

Alt text

然后点击“本店订单”时,调用自定义module里的接口loadPage,参数为h5的链接。三端实现接口loadPage,h5直接跳转,而iOS和Android通过h5链接从weex跳转配置里找到对应的js,重新渲染显示。下面具体做几点说明:

1)定义Map<String, WXSDKInstance> wxsdkInstanceMap;来存储不同js的WXSDKInstance,定义Map<String, View> viewMap来存储不同js渲染后的View。之所以要存储多个WXSDKInstance,是因为WXSDKInstance不能重复渲染,而且当WXSDKInstance destory后,之前渲染的view里的内容也会被清空。注意在在页面destory时,记得把所有WXSDKInstance都destory就好了。

2)viewMap里的key对应页面的js。点击tab切换页面时,如对应的js已渲染,则直接取出view来显示。

3)上文提到的weex跳转配置,在以下的跳转规则里一同介绍。

二、App的跳转规则的weex支持方案设计

跳转规则如下图,如果看不清,可以到新页面放大查看。

App跳转框架

主要介绍一下两个配置参数:

  • 在参数weexPages配置所有的weex页面。
    示例如下:
[
   {
       "page":"order",
       "url":"https://dshdjshjbx.js",
       "md5":"323827382huwhdjshdjs",
       "h5":"http://dsds.html"
       "v":"1.5.0"
    },
    {
       "page":"detail",
       "url":"https://dsdsds.js",
       "md5":"323827382huwhdjshdjs",
       "h5":"http://dsds.html"
       "v":"1.5.0"
    }
]

page: 对应统一跳转的 path

url: 需要渲染的js,

md5: js文件的md5值用于校验,

h5: 渲染失败后的降级方案,

v: 最低支持的版本号

在页面访问h5页面时,拿url跟weexPages里的url进行对比,如果一致就采用weex打开。这里的对比,目前还比较简单粗暴,后续会进行优化,最终目标是只对比?之前的一部分,后面的参数通过intent传入到weex页面,参与weex的渲染。

这样就达到了动态拦截,动态上线weex的目的。

三、js预加载方案

前面讲到为了加快weex打开时间,会预加载js,这里就介绍一下js预加载的实现。

  • 1)每次更新完配置文件,遍历,查看是否存在md5一致的page_xxx.js文件,如果不存在则更新.

  • 2)下载完成后,保存格式为xxx.js,校验md5

    • 相同的话,记录文件的最后修改时间;
    • 不同的话,删除已下载文件,重新下载,重复校验流程。
  • 3)支持统一跳转协议,page对应目前app端的统一跳转协议里的page,有必要的时候可以替换原来的native页面,解决native页面错误不能及时修复的问题。加载失败的话,打开h5页面。

  • 4)每次打开指定页面的时候,先检查本地是否有对应page文件,再检验最后修改时间是否跟记录的一致

    • 一致就加载
    • 不一致就用线上url。

四、遇到的问题以及解决方法

问题一:上线后,发现在一些机型渲染失败,public void onException(WXSDKInstance instance, String errCode, String msg)回调里,errCode返回wx_create_instance_error,msg返回createInstance fail!

解决办法:将apk解压出来后,发现编译出了支持5种abi的包。然而libweexv8.so只在armeabi和x86里有,缺少对其它三种abi的支持,那么如果应用运行于arm64-v8a,x86_64,armeabi-v7a为首选abi的设备上时,就会加载失败了。其实arm64-v8a,armeabi-v7a,x86_64这三个abi,应用并不是必须要做支持,手机一般都会提供自动兼容。所以我们只要把对x86, arm64-v8a,x86_64的支持去掉就可以。如下在主模块的build.gradle的android里的defaultConfig内添加如下内容:

defaultConfig {  
    ndk {  
        abiFilters "armeabi", "x86"  
    }  
}

enter image description here

问题二:OkHttpAdapter里调用onHttpFinish出现解析异常,日志如下:

com.alibaba.fastjson.JSONException: syntax error, pos 2
	at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1300)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1210)
	at com.alibaba.fastjson.JSON.parse(JSON.java:109)
	at com.alibaba.fastjson.JSON.parse(JSON.java:100)
	at com.taobao.weex.http.WXStreamModule.parseJson(WXStreamModule.java:378)
	at com.taobao.weex.http.WXStreamModule$2.onResponse(WXStreamModule.java:365)
	at com.taobao.weex.http.WXStreamModule$StreamHttpListener.onHttpFinish(WXStreamModule.java:523)
	at com.showjoy.weex.commons.adapter.OkHttpAdapter$6.onResponse(OkHttpAdapter.java:161)
	at okhttp3.RealCall$AsyncCall.execute(RealCall.java:133)
	at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
	at java.lang.Thread.run(Thread.java:818) 

解决方法:catch异常

try {
    if (null != listener) {
        listener.onHttpFinish(wxResponse);
    }
} catch (Exception e) {
    LogUtils.e(e);
}

问题三:相对地址以及线上线下环境切换问题。

解决方法:在最新版本已支持相对地址,在.vue文件里链接以及请求地址使用相对地址,h5页面自动选择该页面使用的域名,而在iOS和Android都做拦截处理,根据当前环境添加相应的域名。

  • Android 实现URIAdapter 注入
  • iOS 实现WXURLRewriteProtocol 注入

参考链接:
https://github.com/weexteam/
http://weex-project.io/doc/
https://github.com/alibaba/weex/

@ShowJoy-com ShowJoy-com changed the title {Android} 详细全面的Weex接入过程 「Android」 详细全面的Weex接入过程(Android视角) Mar 10, 2017
@ShowJoy-com ShowJoy-com changed the title 「Android」 详细全面的Weex接入过程(Android视角) 「Android」 详细全面的基于vue2.0Weex接入过程(Android视角) Mar 10, 2017
@ShowJoy-com ShowJoy-com added the weex label Oct 18, 2017
@sadpup

This comment has been minimized.

Copy link

@sadpup sadpup commented May 19, 2018

nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.