初识 weex

什么是 weex

weex 是阿里开源的一套三端通用方案,只要编写一次代码就能运行在 iOS、Android 和 Web 上。这里是官网的介绍:
Weex 是一套简单易用的跨平台开发方案,能以 web 的开发体验构建高性能、可扩展的 native 应用,为了做到这些,Weex 与 Vue 合作,使用 Vue 作为上层框架,并遵循 W3C 标准实现了统一的 JSEngine 和 DOM API,这样一来,你甚至可以使用其他框架驱动 Weex,打造三端一致的 native 应用。
但是实际上,weex 是从阿里的 WeeApp 发展而来的,看开源之后 github 上面提交的记录,大部分工作都是 Vue 框架解析成原生组件的代码,原生组件早就写完了。

但是,由于这种版本升级似的改动,就造成了一些坑。一开始编写 weex 应用的源码文件扩展名是.we,后来,weex 跟 Vue.js 合作直接使用 .vue 文件作为源码,编写的方式也改为了 Vue 的格式。
局限在于,并不是所有 Web 特性都支持的,也可以说支持的特性很少并且坑很多。
官方的特性支持列表
目前 weex 正处于向 Vue 迁移的过程,许多文档和示例代码都比较旧,bug 也比较多,还不能作为正式的生产工具,最近 weex 正式成为了 Apache 孵化项目,希望能够成为 ReactNative 这样的成熟项目。

安装开发环境

安装过程还是非常友好的,按照官方的教程做就可以了:
Weex 官方提供了 weex-toolkit 的脚手架工具来辅助开发和调试。首先,你需要 Node.js 和 weex-toolkit安装 Node.js 方式多种多样,最简单的方式是在 Node.js 官网 下载可执行程序直接安装即可。对于 Mac,可以使用 Homebrew 进行安装:

brew install node

安装完成后,可以使用以下命令检测是否安装成功:

$ node -v
v6.3.1
$ npm -v
3.10.3

通常,安装了 Node.js 环境,npm 包管理工具也随之安装了。因此,直接使用 npm 来安装 weex-toolkit。
但是如果想用 .vue 的模板来编写,需要按照说明下载 beta 版本的才行!否则下载的环境生成的项目模板是 .we 的。
weex-toolkit 目前仅有最新的 beta 版本开始才支持初始化 Vue 项目,使用前请确认版本是否正确。

$ npm install -g weex-toolkit@beta

如果提示权限错误(permission error),使用 sudo 关键字进行安装

$ sudo cnpm install -g weex-toolkit@beta

准备调试工具

调试需要 iOS/Android 真机和 Chrome 浏览器。阿里已经编写了一个原生 app 叫做 "Playground App",可以下载使用,但是最好的办法还是自己搭建一个 app 毕竟最终还是要把 weex 整合到 app 中才行的。
iOS 的集成很简单:

  1. 使用 cocoapods 安装 WXDevtool。
  2. 初始化 weex 页面.
- (void)viewWillAppear:(BOOL)animated{
    self.wxInstance = [[WXSDKInstance alloc]init];
    self.wxInstance.viewController = self;
    self.wxInstance.frame = self.view.bounds;
    __weak typeof(self) weakSelf = self;
    self.wxInstance.onCreate = ^(UIView* view){
        [weakSelf.weexView removeFromSuperview];
        weakSelf.weexView = view;
        [view removeFromSuperview];
        [weakSelf.view addSubview:view];
    };
    

    self.wxInstance.onFailed = ^(NSError* error){
        weakSelf.wxInstance.viewController = nil;
    };
    self.wxInstance.renderFinish = ^(UIView* view){
        [weakSelf updateInstanceState:WeexInstanceAppear];
        NSLog(@"%@",view);
    };
//weex 运行起来之后,会把代码打包成一个 js bundle, 只要加载上就可以了,无论是从 weburl 还是从 fileurl 加载都是很方便的.    
//    [self.wxInstance renderWithURL:[NSURL URLWithString:@"http://192.168.25.85:8088/weex/foo.js"] options:@{@"bundleUrl":@"http://192.168.25.85:8088/weex/foo.js"} data:nil];
    [self.wxInstance renderWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"foo" ofType:@"js"]]];
}

注意销毁,不要造成内存泄露。

- (void)dealloc{
    [self.wxInstance destroyInstance];
}
  1. 修改 weex 状态
    参考阿里的 demo,在自己的 app 里面也及时修改 weex 状态,状态的改变会直接回调到 javascript 的相应函数中。
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self updateInstanceState:WeexInstanceAppear];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [self updateInstanceState:WeexInstanceDisappear];
}

- (void)updateInstanceState:(WXState)state
{
    if (self.wxInstance && self.wxInstance.state != state) {
        self.wxInstance.state = state;
        
        if (state == WeexInstanceAppear) {
            [[WXSDKManager bridgeMgr] fireEvent:self.wxInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewappear" params:nil domChanges:nil];
        }
        else if (state == WeexInstanceDisappear) {
            [[WXSDKManager bridgeMgr] fireEvent:self.wxInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewdisappear" params:nil domChanges:nil];
        }
    }
}

这部分是参考 Demo 进行编写的。

初始化项目

直接看官方文档好了,这部分讲解的很清楚。
《Weex 快速上手》
使用 weex-toolkit v1.0.4 按照官方文档很容易就配置好项目了。
只要使用 weex init 初始化项目,然后进入项目目录,输入 npm install 配置依子模块就好了。
各种配置文件已经准备好,几乎不用修改。

使用 weex 组件和 css 编写页面

weex 三端通用的原理。


flow-2017331

看官方文档上的图。
源代码会编译成 jsbundle 然后分别发送给三端解析。
H5 端直接使用 Vue.js 解析预置的 H5 组件。iOS 端与 android 端直接把组件解析成原生实现。
因为对 css 的解析,需要原生来做,所以 weex 支持的 css 布局和特性是比较少的。
具体使用可以参考官方文档。

在 weex 中支持的比较好的布局方式是 flex 布局,block 布局就经常存在表现不一致的情况。

  • 支持基本的盒模型。
  • 支持 position 定位布局。
  • 支持使用 flexbox 布局。

使用限制

  • 只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。
  • 默认是组件级别的作用域,没有全局样式。
  • 不支持样式继承(因为有作用域隔离)。
  • 考虑到样式的数据绑定,样式属性暂不支持简写。

组件的使用

组件使用只要参考官方文档就可以了,虽然一开始写起来有些别扭,例如<a>标签中不能包含文字,但是按照文档写也能完成任务。

有些控件可能不太符合要求,例如<video>标签,在 iOS 上面的实现是一款开源的播放器,自带的 UI 可能会不符合要求,这种可以自己修改 native 的实现。

自定义组件

Weex 自定义组件有两种方式,一种是使用 Vue 的组件定制方式,另一种是三端分别实现相应组件。
在扩展 Weex 组件时,如果只使用了 Weex 提供的内置组件,并且使用的都是 Weex 支持的样式,那么就和普通的自定义组件无异,不需要 Native 端再有相应的实现。如果你定制组件时不得不用到目前 Weex 不支持的标签和样式,在这种情况下才是真正的“扩展”了 Weex 的组件,你还需要在 Android 和 iOS 中有相应的实现,不然会导致渲染异常。
Weex HTML 扩展

- (instancetype)initWithRef:(NSString *)ref
                       type:(NSString*)type
                     styles:(nullable NSDictionary *)styles
                 attributes:(nullable NSDictionary *)attributes
                     events:(nullable NSArray *)events
               weexInstance:(WXSDKInstance *)weexInstance{
    if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
        if ([attributes objectForKey:@"content"]) {
            id content = [attributes objectForKey:@"content"];
            if ([content isKindOfClass:[NSString class]]) {
                self.title = (NSString*)content;
            }
        }
        return self;
    }
    return nil;
}


- (void)viewDidLoad{
    [super viewDidLoad];
    if (self.title == nil) {
        self.title = [NSString string];
    }
    [self showHUDProgressingBarWithTitle:self.title];
}

- (void)viewWillUnload{
    [self dismissHUDProgressingBar];
    [super viewWillUnload];
}

- (void)showHUDProgressingBarWithTitle:(NSString *)title {
    CGSize screenSize = self.view.bounds.size;
    if(!self.hudView){
        self.hudView = [[UIView alloc] initWithFrame:CGRectMake((screenSize.width - DEFAULT_HUD_VIEW_WIDTH) / 2,
                                                                (screenSize.height - DEFAULT_HUD_VIEW_HEIGHT) / 2,
                                                                DEFAULT_HUD_VIEW_WIDTH, DEFAULT_HUD_VIEW_HEIGHT)];
        
        self.hudView.backgroundColor = [UIColor clearColor];
        UIFont* titleFont = [UIFont systemFontOfSize:DEFAULT_HUD_TITLE_FONTSIZE];
        CGFloat titleHeight = [title sizeWithAttributes:@{
                                                          NSFontAttributeName : titleFont
                                                          }].height;
        self.indicatorView = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        self.indicatorView.center = CGPointMake(self.hudView.bounds.size.width/2, self.hudView.bounds.size.height/2);
        self.hudViewTitle = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, DEFAULT_HUD_VIEW_WIDTH, titleHeight)];
        self.hudViewTitle.center = CGPointMake(self.hudView.bounds.size.width/2, self.indicatorView.center.y + (self.indicatorView.bounds.size.height/2 + self.hudViewTitle.bounds.size.height/2 + 12));
        self.hudViewTitle.font = titleFont;
        self.hudViewTitle.textColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.8];
        self.hudViewTitle.textAlignment = NSTextAlignmentCenter;
        [self.hudView addSubview:self.indicatorView];
        [self.hudView addSubview:self.hudViewTitle];
        [self.view addSubview:self.hudView];
    }
    self.hudView.frame = CGRectMake((screenSize.width - DEFAULT_HUD_VIEW_WIDTH) / 2,
                                    (screenSize.height - DEFAULT_HUD_VIEW_HEIGHT) / 2,
                                    DEFAULT_HUD_VIEW_WIDTH, DEFAULT_HUD_VIEW_HEIGHT);
    self.hudView.hidden = NO;
    self.hudViewTitle.text = title;
    [self.indicatorView startAnimating];
    [self.view bringSubviewToFront:self.hudView];
}


- (void)dismissHUDProgressingBar {
    if (self.hudView) {
        [self.indicatorView stopAnimating];
        self.hudView.hidden = YES;
    }
}

  1. 自定义组件类,继承 WXComponent 对象
  2. 使用- (instancetype)initWithRef:(NSString )ref type:(NSString)type styles:(nullable NSDictionary *)styles attributes:(nullable NSDictionary *)attributes events:(nullable NSArray *)events weexInstance:(WXSDKInstance *)weexInstance 初始化对象并接收参数。
  3. 在 - (void)viewDidLoad 中初始化自定义 UI。不要在 - (void)viewWillLoad 初始化 UI 会导致- (void)viewWillLoad 和 - (UIView *)loadView 反复调用,最终出现 StackOverFlow。原因还需要查看一下 Weex 源码来确定。
  4. 必要的情况下,使用- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params 向 Vue.js 框架发送事件。
    首先,在 Android 里肯定是可以监听到“返回”按钮的点击事件的,其实只要实现 Activity 里的 onBackPressed 接口就可以了,它会在当前视图里点击返回按钮时执行。在 weex-hackernews Andorid 项目里的 MainActivity.java 中,就实现了 onBackPressed 接口:
public void onBackPressed() {
    Log.e("USER ACTION", "BACK");
    WXSDKManager.getInstance().fireEvent(mWXSDKInstance.getInstanceId(), "_root", "androidback");
}

在这个方法里,通过 WXSDKManager.getInstance() 取到了当前页面的实例,然后调用 fireEvent 接口给根视图派发 androidback 事件,事件名是可以自定义的。在 Weex Runtime 中会接收到这个事件,会传递给 Vue.js 框架,并且触发最外层组件的 androidback 事件,最终会找到 back 方法并执行。(这里说的 Weex Runtime 是前端代码实现的,比 Vue.js 更底层一些)。
在 Android 中派发原生事件

  1. 为 HTML 编写适合 Web 端特性的组件。参考 weex-html5 扩展开发指引 文章比较老,是 we 文件的示例。

如果编写的组件使用 weex 已有的组件就可以拼装而成,直接使用 Vue.js 的编写单文件组件的方式编写就可以了

开始进行调试

由于 Weex 开发工具还在快速变化,所以文档上写的不怎么清除。
如果是调试单独的 vue 文件,直接使用 weex debug 命令就好了。
但是此时程序的入口其实是 weex.html -> 单个 vue 文件。所以存在着 Vue.use 添加全局插件无效的情况。
文档上说,使用 weex debug [目录] -e [入口文件] 可以调试目录,但是测试没有成功,访问 url 找不到文件。
使用 weex debug 相关命令后,会自动打开浏览器窗口。


DingTalk20170331103639-2017331

如果用官方的 playground 调试,可以扫描右侧二维码,如果自己项目中调试,可以直接填写链接。
把 jsbundle 直接打包进 app 中也可以,总之,读取 jsbundle 就好。

DingTalk20170331104943-2017331

有手机连接后,会显示这个调试界面,手机和 Nodejs 服务会使用 websocket 通信,挺好用的。

最后说一下目录调试。

说是调试,其实是直接运行。因为没有搞清楚如何用 weex debug 命令调试目录,于是就直接运行了编译好的 jsbundle。
直接输入 npm run dev 命令和 npm run serve,如果8080端口没有占用,那么访问 localhost:8080/weex.html 就打开了 app.web.js 这个 bundle。
在 app 里面访问 serviceip:8080/dist/app.weex.js 就可以打开这个 bundle。

Hanks10100 大神写的比较系统的介绍 weex 文章
使用 Weex 和 Vue 开发原生应用 —— 0 项目介绍和文章目录

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

推荐阅读更多精彩内容