Android 视角详细全面的基于 Vue 2.0 Weex 接入过程

StephaineKa 3年前
   <h2>一、说在前面的话</h2>    <p>目前weex已在尚妆旗下的达人店app上线了一个常用的订单管理页面,截止目前Android上未发现问题,渲染时间在100-300ms之间。</p>    <p>作为Android开发,此文首先会从Android的角度为主来记录接入的过程,希望给未接入的同学更方便省时地接入weex提供一点帮助。其中会涉及到 预加载 , 降级 , 热更新 , 埋点 以及在app不更新的情况下 动态配置新页面 等问题,这些Android和iOS都是统一的逻辑,希望和大家一起交流。</p>    <h2>二、Android接入过程</h2>    <p>其实对于module、component的定义,以及IWXImgLoaderAdapter、IWXHttpAdapter等adapter的重写,在playgroud和weexteam里都已经有很好的例子了。</p>    <h3>1、gradle依赖</h3>    <pre>  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'</pre>    <h3>2、新建weex module</h3>    <p>在原来的project上,新建单独的 weex module。代码结构如下:</p>    <h3>3、初始化weex</h3>    <p>通过类WeexManager来统一管理weex相关的配置,以下是WeexManager里的init函数的主要内容,在application的onCreate里调用:</p>    <pre>  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();        }</pre>    <p>1)考虑到第一次接入weex,有点担心兼容问题,万一引起崩溃等不确定因素,所以这里做了一个开关。其实每接入一个新的sdk都最好有个 控制开关 ,以避免因为不确定因素导致不稳定。</p>    <p>2)weexDir是js的下载存储路径,为了加快页面打开时间,会对js进行 预加载到本地</p>    <p>3) sdk对a标签的处理只调用了"event"的openURL接口,但是却没有注册 "event" 。所以需要自己实现WXEventModule,并注册。</p>    <p>4)模态对话框 ModalModule 的实现参考sdk里的WXModalUIModule</p>    <p>5) FrescoImageAdapter 和 FrescoImageComponent 的实现依赖我们开源的 SHImageView 支持webp,支持压缩,支持没有协议的链接(忽略协议可以让浏览器根据页面时http或者https自动选择使用的协议,从而避免了网站改为https的情况下仍然访问http资源而无法访问的问题。)</p>    <p>6)OkHttpAdapter的实现参考github上zjutkz同学的实现 OkHttpAdapter,感谢,经过改写,支持没有协议的链接,支持cookie</p>    <p>7)ShopModule是自定义的Module,定义通用的一些接口,比如设置title bar是否显示,以及title bar的title;关闭当前页面,分享,错误日志收集等。</p>    <p>8)UserTrackAdapter用于埋点,另外可以在ShopModule里自定义接口收集埋点、错误信息等。</p>    <p>9)CustomURIAdapter用于支持相对地址,具体实现参见以下:</p>    <pre>  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);      }  }</pre>    <h3>4、新建统一的weex页面</h3>    <p>这边考虑到以后页面有可能嵌入到其他activity,所以把weex的渲染放入新建的 WeexFragment 。然后新建WeexActivity来引用该WeexFragment 。所有的单独页面的weex渲染都使用这个 WeexActivity ,非单独页面的使用 weexFragment ,这样新加页面时,无需重新注册activity。weex处理逻辑统一,方便管理,方便动态配置。 通过统一跳转协议跳转到WeexActivity,通过intent传入两个参数url和h5 。</p>    <pre>  showjoyshop://page.sh/weex</pre>    <p>intent参数:</p>    <p>url :js链接,可以是本地的存储地址/sdcard/com.showjoy.shop/weex/order.js,也可以是线上链接 <a href="/misc/goto?guid=4959741212382254938" rel="nofollow,noindex">https://xxxxx/0.4.3/order.js</a></p>    <p>h5 :用来降级的h5页面链接,当渲染失败时,会跳转到该h5页面</p>    <h3>5、开始渲染js,失败后降级到h5</h3>    <p>首先实例化WXSDKInstance</p>    <pre>  wxInstance = new WXSDKInstance(activity);  wxInstance.registerRenderListener(this);  wxInstance.onActivityCreate();  registerBroadcastReceiver();</pre>    <p>1)当前类实现接口IWXRenderListener,可以参考weexteam里的AbsWeexActivity实现</p>    <p>2)注册的广播是DefaultBroadcastReceiver,可以可以参考weexteam里的AbsWeexActivity实现</p>    <p>然后讲一下渲染,支持本地js以及线上js</p>    <pre>  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();  }</pre>    <p>其中,getPageName()自定义即可,getDisplayWidth和getDisplayHeight获取屏幕宽高。</p>    <p>传入本地的存储地址时,先读取文件,然后同个Handler在UI线程渲染,如下:</p>    <p>接收LOAD_LOCAL_FILE后handler里的实现:</p>    <pre>  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;</pre>    <p>这里getLifeState()是我们自己BaseActivity的实现,可以自行判断。SHJump和finishActivity都是自己的实现,大家自己实现即可。</p>    <p>渲染回调的实现,按需要处理即可,渲染成功后隐藏loading,view创建后添加view。 渲染异常时降级跳转到h5 。如下:</p>    <pre>  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();  }</pre>    <h3>6、多个js在同个页面渲染</h3>    <p>为了实现如图的tab,一开始在.vue文件里使用tabbar组件,后来发现在Android机型适配上不够好。于是后来就将两个tab做成两个页面,生成两个js文件。首先渲染“我的订单.js”,生成如下的界面。</p>    <p> </p>    <p>然后点击“本店订单”时,调用自定义module里的接口 loadPage ,参数为h5的链接。 三端实现接口loadPage,h5直接跳转,而iOS和Android通过h5链接从weex跳转配置里找到对应的js,重新渲染显示 。下面具体做几点说明:</p>    <p>1)定义Map<String, WXSDKInstance> wxsdkInstanceMap;来存储不同js的WXSDKInstance,定义Map<String, View> viewMap来存储不同js渲染后的View。之所以要存储多个WXSDKInstance,是因为WXSDKInstance不能重复渲染,而且当WXSDKInstance destory后,之前渲染的view里的内容也会被清空。注意在在页面destory时,记得把所有WXSDKInstance都destory就好了。</p>    <p>2)viewMap里的key对应页面的js。点击tab切换页面时,如对应的js已渲染,则直接取出view来显示。</p>    <p>3)上文提到的 weex跳转配置 ,在以下的跳转规则里一同介绍。</p>    <h2>二、App的跳转规则的weex支持方案设计</h2>    <p>跳转规则如下图,如果看不清,可以到新页面放大查看。</p>    <p> </p>    <p>主要介绍一下两个配置参数:</p>    <ul>     <li> <p>在参数weexPages配置所有的weex页面。</p> <p>示例如下:</p> </li>    </ul>    <pre>  [     {         "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"      }  ]</pre>    <p>page : 对应统一跳转的 path</p>    <p>url : 需要渲染的js,</p>    <p>md5 : js文件的md5值用于校验,</p>    <p>h5 : 渲染失败后的降级方案,</p>    <p>v : 最低支持的版本号</p>    <p>在页面访问h5页面时,拿url跟weexPages里的url进行对比,如果一致就采用weex打开。这里的对比,目前还比较简单粗暴,后续会进行优化,最终目标是 只对比 ? 之前的一部分,后面的参数通过intent传入到weex页面,参与weex的渲染。</p>    <p>这样就达到了动态拦截,动态上线weex的目的。</p>    <h2>三、js预加载方案</h2>    <p>前面讲到为了加快weex打开时间,会预加载js,这里就介绍一下js预加载的实现。</p>    <ul>     <li> <p>1)每次更新完配置文件,遍历,查看是否存在md5一致的page_xxx.js文件,如果不存在则更新.</p> </li>     <li> <p>2)下载完成后,保存格式为xxx.js,校验md5</p>      <ul>       <li>相同的话,记录文件的最后修改时间;</li>       <li>不同的话,删除已下载文件,重新下载,重复校验流程。</li>      </ul> </li>     <li> <p>3)支持统一跳转协议,page对应目前app端的统一跳转协议里的page,有必要的时候可以替换原来的native页面,解决native页面错误不能及时修复的问题。加载失败的话,打开h5页面。</p> </li>     <li> <p>4)每次打开指定页面的时候,先检查本地是否有对应page文件,再检验最后修改时间是否跟记录的一致</p>      <ul>       <li>一致就加载</li>       <li>不一致就用线上url。</li>      </ul> </li>    </ul>    <h2>四、遇到的问题以及解决方法</h2>    <p>问题一:上线后,发现在一些机型渲染失败,public void onException(WXSDKInstance instance, String errCode, String msg)回调里,errCode返回wx_create_instance_error,msg返回createInstance fail!</p>    <p>解决办法:将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内添加如下内容:</p>    <pre>  defaultConfig {        ndk {            abiFilters "armeabi", "x86"        }    }</pre>    <p><img src="https://simg.open-open.com/show/73e626bc637aa274d9e80a1d6f19d3c2.png"></p>    <p>问题二:OkHttpAdapter里调用onHttpFinish出现解析异常,日志如下:</p>    <pre>  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)</pre>    <p>解决方法:catch异常</p>    <pre>  try {      if (null != listener) {          listener.onHttpFinish(wxResponse);      }  } catch (Exception e) {      LogUtils.e(e);  }</pre>    <p>问题三:相对地址以及线上线下环境切换问题。</p>    <p>解决方法:在最新版本已支持相对地址,在.vue文件里链接以及请求地址使用相对地址,h5页面自动选择该页面使用的域名,而在iOS和Android都做拦截处理,根据当前环境添加相应的域名。</p>    <ul>     <li>Android 实现URIAdapter 注入</li>     <li>iOS 实现WXURLRewriteProtocol 注入</li>    </ul>    <p>参考链接:</p>    <p><a href="/misc/goto?guid=4959741212465928166" rel="nofollow,noindex">https://github.com/weexteam/</a></p>    <p><a href="/misc/goto?guid=4959741212551189079" rel="nofollow,noindex">http://weex-project.io/doc/</a></p>    <p><a href="/misc/goto?guid=4959738024458557000" rel="nofollow,noindex">https://github.com/alibaba/weex/</a></p>    <p> </p>    <p>来自:https://github.com/ShowJoy-com/showjoy-blog/issues/25</p>    <p> </p>