Weex跨平台项目的构建

赵小温



  Weex是一套简单易用的跨平台开发方案,能以 web 的开发体验构建高性能、可扩展的 native 应用。也就是说,我们可以通过weex构造一个能够适配到Web、Android、IOS三端的高性能可动态化的项目,甚至可以通过weex的可扩展性,做到三端一致。

  通过weex构造的项目,可以做到只写一套weex代码,即可应用到三端(web、Android、IOS),熟练的weex开发工程师,可以大幅提高开发效率,降低开发和沟通成本。文章会从环境配置、工程创建、三端扩展、降级方案四个方面讲解weex开发的简易流程。

一、环境配置

weex环境配置可以参考官网http://weex.apache.org/cn/guide/set-up-env.html
以下是我在mac上的配置过程,和官网基本一致:
1.安装Homebrew
  Homebrew是一个包资源管理器,在mac上可以用了安装软件,卸装软件,查找软件。参考https://brew.sh/index_zh-cn.html
  安装命令: /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  出现安装提示后,“RETURN”开始安装,
  查看版本命令: brew –v
2.安装nvm
  nvm是node.js的版本管理器,可以用nvm来安装node.js。
  安装命令:brew install nvm
3.安装node.js
  Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。   Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
  Node.js 的包管理器 npm,是全球最大的开源库生态系统。
  安装命令:nvm install node nvm alias default node
4.安装weex-toolkit
  weex-toolkit 是官方提供的一个脚手架命令行工具,你可以使用它进行 Weex 项目的创建,调试以及打包等功能。
  安装命令:npm install -g weex-toolkit
  请使用weex-toolkit的最新版本(weex-toolkit 在 1.0.1 之后才支持初始化 Vue 项目)
  版本查看命令:weex –v

二、工程创建

weex工程创建有两个命令:init、create,在我最早接触weex时官网给的案例使用的是init,现在官网用的是create命令,两个命令最大的区别就是init创建的是一个vue工程,create创建的是weexpack工程。

1.创建工程
  初始化项目:
  命令:weex create WeexDemo

安装依赖:
  进入工程目录,命令:npm install

编译打包:
  命令:npm run build & npm run serve
  也可将build和serve合并为一个命令写入工程的package.json文件,例如:在文件的scripts下添加:"app": "npm run build && npm run serve",便可通过npm run app进行编译打包。

2.weexpack的介绍
  为什么weex会提供两种初始化项目的命令呢?
  weexpack工程除了可以像vue工程一样通过run命令进行编译打包,还支持platform和plugin。
  platform:
  命令weex platform add ios/android可以直接在weex项目中生成一个对应的IOS/Android工程,该工程可以直接运行并展示weex页面。

platform命令生成工程,工程里会自动生成一些基础性代码,比如native加载weex页面的代码,在调试上可以节省很多事件。但是platform命令也有自己的问题,IOS工程经常会遇到运行失败的提示,对于不了解IOS的开发者来说,简直就是一头雾水;而Android工程更是奇葩,工程package无法修改,了解Android的开发者知道,package是app的唯一识别标志,相同的package无法在同一个平台上上线。
  plugin:
  命令weex plugin add xxx,可以将插件市场的插件安装到项目中,听起来太牛了……
  插件“weex-action-sheet”(weex market上下载量最大的插件),在android上自动安装后,依赖命令compile 'org.weex.plugin:actionsheet:1.0.0'出现在了build.gradle文件中dependencies的外面,测试多次依旧不行,最近(2017-12-20)更新weexpack到最新版本后,compile的位置才正确加入。
  目前我们工程采用的是init方式初始化,将生成的bundle js在Android/IOS app中加载,对于插件,目前还未使用插件市场上的,主要依靠自定义扩展实现。
3.weex页面的加载
  weex工程编译打包后,会在dist下面生成两个文件weex js和web js,weex js是Android/IOS加载的文件,web js提供给web加载的。

Android
  Activity实现接口IWXRenderListener,并通过WXSDKInstance的实例加载js文件,renderByUrl加载远程js文件。

mInstance = new WXSDKInstance(this);  
mInstance.registerRenderListener(this);  
Map<String, Object> options = new HashMap<>();  
options.put(WXSDKInstance.BUNDLE_URL, bundleUrl);  
mInstance.renderByUrl(getPageName(), url, options, jsonInitData, PhoneUtils.getScreenWidth(this),PhoneUtils.getScreenHeight(this), WXRenderStrategy.APPEND_ASYNC);  

IOS
  在ViewController中通过renderWithURL加载远程js文件。

_instance = [[WXSDKInstance alloc] init];  
 _instance.viewController = self;  
_instance.frame = self.view.frame;  
__weak typeof(self) weakSelf = self;  
 _instance.onCreate = ^(UIView *view) {
        [weakSelf.weexView removeFromSuperview];
        weakSelf.weexView = view;
        [weakSelf.view addSubview:weakSelf.weexView];
    };  
_instance.onFailed = ^(NSError *error) {  
        NSLog(@"Weex onFailed");
    }; 
_instance.renderFinish = ^(UIView *view) {  
        NSLog(@"Weex renderFinish");
    };
[_instance renderWithURL:self.url 
 soptions:@{@"bundleUrl":[self.url absoluteString]} data:nil];

Web
  在html文件中,先引入vue和weex-vue-render

<script src="../../../node_modules/vue/dist/vue.js"></script>  
<script src="../../../node_modules/weex-vue-render/dist/index.js"></script>  


  在body中加载web js文件:

<div id="root"></div>  
<script src="../../../dist/web/views/order/test.js"></script>  
三、扩展

Weex提供了扩展机制,开发者可以根据自己的业务需要,定制自己的功能。对应的扩展,需要在三端都做实现。

1.Module扩展
  扩展非UI的特定功能,将native方法/函数暴露给bundle js。

Android:
  Module扩展需要继承WXModule;
  在方法上添加注解@JSMethod(uiThread = false或true)将方法暴露给js;官网提到的@WXModuleAnno(runOnUIThread = true)已经过时,最好采用最新的方式JSMethod;
  在Application中通过 WXSDKEngine.registerModule("myModule", MyModule.class);注册Module。
备注:
  a.请勿忘记混淆: -keep public class * extends com.taobao.weex.common.WXModule{*;}
  b.android weex sdk 0.13.0版本之前,android传递数据给js,需要使用回调JSCallback,而不能采用直接方法返回数据的方式。
  测试代码;

/**
     * 已过时,由JSMethod替代
     * @param msg
     */
    @WXModuleAnno(runOnUIThread = true)
    public void printLog(String msg) {

Toast.makeText(mWXSDKInstance.getContext(),msg,Toast.LENGTH_SHORT).show();  
    }

    /**
     * 获取城市ID-同步
     * sdk 0.11.0版本无法返回数据
     * @return
     */
    @JSMethod(uiThread = false)
    public int getCityId(){
        return cityId;
    }

    /**
     * 获取骑手名字-异步
     * 通过方法回调传值
     * @param jsCallback
     */
    @JSMethod(uiThread = true)
    public void getRiderName(JSCallback jsCallback){
        jsCallback.invoke(riderName);
    }

IOS:
  Module扩展需要实现WXModuleProtocol;
  添加@synthesized weexInstance,将Module对象绑定到指定的实例上,必须添加宏WX_EXPORT_METHOD/WX_EXPORT_METHOD_SYNC将函数暴露给js;
  在Appdelegate中通过[WXSDKEngine registerModule:@"userModule" withClass:[UserModule class]];注册Module。
  测试代码:

@synthesize weexInstance;
WX_EXPORT_METHOD(@selector(getCityId))  
WX_EXPORT_METHOD(@selector(getRiderName:))  
-(NSInteger)getCityId{
    return _cityId;
}
-(void)getRiderName:(WXModuleCallback)callback{
    callback(_riderName);
} 

Html5:
  如果你引入了 weex-vue-render 这个库,那么在全局能获取到 weex 这个变量,其中提供了 registerModule 方法可以注册模块。 API 格式
  registerModule
  name: {String} 必选,模块名称。
  define: {Object} 必选,模块的定义。
  之前Module在Android和IOS成功调用,但是在Web端的Module注册花费了不少时间,主要是因为自己的一个失误。
  错误代码:

var App = require("../../../views/order/test.vue")  
//注册web module
if(weex.config.env.platform === 'Web'){  
    weex.install(modules)
    weex.install(components)
}

编译打包都可以顺利通过,但是一旦调用改js页面,就会提示weex undefined,只需要将module的注册放在 var App = require("../../../views/account/AccountCenterView.vue")之前便可注册成功。
此处有同学可能会问官网中介绍的是registerModule,你为什么用的weex.install(modules)
做一个说明,modules文件夹下是多个Module文件,通过index.js文件将modules文件夹下的所有Module一次性注册完成的。通过install将Module插入。
  index.js代码如下:

const DRiderInfo = {  
    riderInfo:function (callback) {
        userModule.riderInfo(callback);
    },
    setTitle:function (msg) {
        userModule.setTitle(msg)
    }
}

export default {  
  init: function (Weex) {
    Weex.registerModule('DRiderInfo', DRiderInfo)
  }
}

在userModule.setTitle(msg)中调用window.dianwoda.setWebTitle(msg)设置WebView title的方法。

备注:web端注册Module时,请不要忘记判断平台,不然会在Android/IOS上报错。

weex调用:
  直接上代码了,比较简单
const userModule = weex.requireModule('userModule'); userModule.printlog('this is a log.');

Module提供的方法/函数,往往需要数据的传递,为了良好的扩展性最好采用对象的方式;
let params = {key:value,key:value,...};
  不过此处有坑:
  a.在iphone 5c 10.2系统上不支持bool值(IOS同学提供)
  b.android上对double的处理,比如:10.0会转成int类型的10。
  请通过其他类型避开这两个类型的使用。

2.Component扩展
  实现自定义的native组件,提供给weex使用,当然Component中也可以定义Module的方法/函数。

Android:
  Component 扩展类必须集成 WXComponentWXVContainer
  通过添加注解@WXComponentProp设置属性;
通过WXSDKEngine.registerComponent("richtext",RichText.class);注册组件。
  备注:
  a.不要忘记混淆:-keep public class * extends com.taobao.weex.ui.component.WXComponent{*;}
  b.官网提供的initView()方法已过时,请使用initComponentHostView(context)初始化

IOS:
  Component必须实现WXComponent;
  通过registerComponent注册组件,[WXSDKEngine registerComponent:@"richtext" withClass:[WXRichText class]];
  IOS设置属性的方式和Android不太一样,Android是通过注解方式,IOS是通过解析的方式,在组件初始化时,可以对style,attribute,events 中数据进行解析,做特殊逻辑处理。

-(instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance{

    if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
        _tel = [WXConvert NSString:attributes[@"tel"]];
    }
    return self;
}

Html5:
  weex自定义组件中如果使用到了weex不支持的html标签,那么你就应该使用html5的Component的扩展将组件注册到weex,
  代码:
  Vue.component('checkBox', checkBox);
  Sidebar是一个vue文件,用来实现组件的逻辑,同样sidebar的注册要放在var App = require("../../../views/test.vue")之前,并且可以使用install。   复选框的html组件:

<template>  
    <div class="checkbox">
        <input name="Fruit" type="checkbox" value="" /><label>苹果</label>
    </div>
</template>  

复选框截图: alt

3.实现Weex的提供的统一接口/协议
  比如weex没有图片下载能力,但是提供了一个统一的接口/协议,native需要实现相应的功能。 其他的接口/协议:IWXDebugAdapterIWXHttpAdapterIWXStorageAdapter...

Android:
  图片下载的实现,需要继承IWXImgLoaderAdapter,并且实现setImage方法,在setImage方法中实现图片下载。通过InitConfig注册接口。

InitConfig config=new InitConfig.Builder().setImgAdapter(new ImageAdapter()).setHttpAdapter(new DefaultHttpAdapter()).build();  
WXSDKEngine.initialize(this,config);`  
setImage方法:  
`WXSDKManager.getInstance().postOnUiThread(new Runnable() {
            public void run() {
                Log.e("ImageAdapter", "image url=" + url);
                if(TextUtils.isEmpty(url)) {
                    view.setImageBitmap((Bitmap)null);
                } else if(view.getLayoutParams().width > 0 && view.getLayoutParams().height > 0) {
                    ImageAdapter.this.parseUrl(view, url);
                }
            }
        }, 0L);

IOS:
  图片下载需要实现协议WXImgLoaderProtocol;
通过registerHandler注册协议。 [WXSDKEngine registerHandler:[WXImageLoader new] withProtocol:@protocol(WXImgLoaderProtocol)];
实现

-(id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)options completed:(void (^)(UIImage *, NSError *, BOOL)) completedBlock

    if ([url hasPrefix:@"//"]) {
        url = [@"http:" stringByAppendingString:url];
    }
    return (id<WXImageOperationProtocol>)[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:url] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {

    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (completedBlock) {
            completedBlock(image, error, finished);
        }
    }];
}
四、降级方案

weex的降级方案可以分两种:异常降级和主动降级。
  异常降级是在bundle js文件抛出异常时,native捕获到异常后,触发的降级,Android异常会调用onException方法,IOS异常会调用_instance.onFailed函数。
  主动降级是人为设置的一种降级,
  比如:
  a.bundle js文件加载超时 在Android的Activity中,js加载成功后,会回调onRenderSuccess方法; 在IOS的ViewController中,js加载成功后,会回调renderFinish函数; 在ActivityViewController中设置一个计时器,加载超时时,直接调用native的降级方法实现降级
  b.js页面检测到该页面不支持某三星机型,js通过downgrade抛出降级异常到onExceptiononFailed中,实现降级。
  工具类DowngradeUtils.js中提供的主动降级方法

export function force(){  
    Downgrade.force();
}
五、遇到的问题

1.手势
  官网提供的手势Touch、Pan,在Android上事件会有比较忙,如果你想做滑动监听,可能会出现卡顿的现象。
2.JS service
  在Android上暂无实验成功,weex sdk版本0.13.1。
3.list刷新问题
  如果list使用官网提供的loading、refresh标签做刷新和加载更多,有一种临界情况,会有问题,当出现refresh时,你在迅速的上拉出现loading,或者反过来操作,Android上会直接refresh/loading不会消失,并且丧失滑动事件的触发,IOS上虽然可以再次触发事件,但是界面效果会出现短暂混乱。
5.BroadcastChannel
  Android weex sdk版本0.13.1之前不能使用 6.input
对于第一次的改变没有监听事件,并且对于ime语言设置v-model不能生效,要配合input事件才可以;如果你想在change事件中对input组件做样式修改,也不能立马提现出来。
7.double
  bundle js传递数据给Android,对象中的double类型的10.0会自动变为整型10,如果在Android中你又刚好指定了double类型就会造成闪退。
8.JavaScript Array对象
  数组的操作方法sort,reverse,数据顺序改变了,但是列表上只显示一条数据。
9.es6配置
  项目中.babelrc文件不要忘记配置,不然会出现各种编译不通过。

以上只是Weex三端融合过程中最基础的流程,对一个功能如何在Android、IOS、Html5做到一致。还有很多问题是开发过程中需要关注和解决的,比如调试、路由、具体降级方案、Canvas绘制、规范等问题。