「Android」 詳細全面的基於vue2.0Weex接入過程(Android視角)

尚妝產品技術刊讀發表於2017-03-10

本文來自尚妝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。程式碼結構如下:

「Android」 詳細全面的基於vue2.0Weex接入過程(Android視角)
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”,生成如下的介面。

「Android」 詳細全面的基於vue2.0Weex接入過程(Android視角)
Alt text

然後點選“本店訂單”時,呼叫自定義module裡的介面loadPage,引數為h5的連結。三端實現介面loadPage,h5直接跳轉,而iOS和Android通過h5連結從weex跳轉配置裡找到對應的js,重新渲染顯示。下面具體做幾點說明:

1)定義Map wxsdkInstanceMap;來儲存不同js的WXSDKInstance,定義Map viewMap來儲存不同js渲染後的View。之所以要儲存多個WXSDKInstance,是因為WXSDKInstance不能重複渲染,而且當WXSDKInstance destory後,之前渲染的view裡的內容也會被清空。注意在在頁面destory時,記得把所有WXSDKInstance都destory就好了。

2)viewMap裡的key對應頁面的js。點選tab切換頁面時,如對應的js已渲染,則直接取出view來顯示。

3)上文提到的weex跳轉配置,在以下的跳轉規則裡一同介紹。

二、App的跳轉規則的weex支援方案設計

跳轉規則如下圖,如果看不清,可以到新頁面放大檢視。

「Android」 詳細全面的基於vue2.0Weex接入過程(Android視角)
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"  
    }  
}複製程式碼

「Android」 詳細全面的基於vue2.0Weex接入過程(Android視角)
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 注入

參考連結:
github.com/weexteam/
weex-project.io/doc/
github.com/alibaba/wee…

相關文章