什么是 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 的集成很简单:
- 使用 cocoapods 安装 WXDevtool。
- 初始化 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];
}
- 修改 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 三端通用的原理。
看官方文档上的图。
源代码会编译成 jsbundle 然后分别发送给三端解析。
H5 端直接使用 Vue.js 解析预置的 H5 组件。iOS 端与 android 端直接把组件解析成原生实现。
因为对 css 的解析,需要原生来做,所以 weex 支持的 css 布局和特性是比较少的。
具体使用可以参考官方文档。
在 weex 中支持的比较好的布局方式是 flex 布局,block 布局就经常存在表现不一致的情况。
- 支持基本的盒模型。
- 支持 position 定位布局。
- 支持使用 flexbox 布局。
使用限制
- 只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。
- 默认是组件级别的作用域,没有全局样式。
- 不支持样式继承(因为有作用域隔离)。
- 考虑到样式的数据绑定,样式属性暂不支持简写。
组件的使用
组件使用只要参考官方文档就可以了,虽然一开始写起来有些别扭,例如标签中不能包含文字,但是按照文档写也能完成任务。
有些控件可能不太符合要求,例如
自定义组件
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;
}
}
- 自定义组件类,继承 WXComponent 对象
- 使用- (instancetype)initWithRef:(NSString )ref type:(NSString)type styles:(nullable NSDictionary *)styles attributes:(nullable NSDictionary *)attributes events:(nullable NSArray *)events weexInstance:(WXSDKInstance *)weexInstance 初始化对象并接收参数。
- 在 - (void)viewDidLoad 中初始化自定义 UI。不要在 - (void)viewWillLoad 初始化 UI 会导致- (void)viewWillLoad 和 - (UIView *)loadView 反复调用,最终出现 StackOverFlow。原因还需要查看一下 Weex 源码来确定。
- 必要的情况下,使用- (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 中派发原生事件
- 为 HTML 编写适合 Web 端特性的组件。参考 weex-html5 扩展开发指引 文章比较老,是 we 文件的示例。
如果编写的组件使用 weex 已有的组件就可以拼装而成,直接使用 Vue.js 的编写单文件组件的方式编写就可以了
开始进行调试
由于 Weex 开发工具还在快速变化,所以文档上写的不怎么清除。
如果是调试单独的 vue 文件,直接使用 weex debug 命令就好了。
但是此时程序的入口其实是 weex.html -> 单个 vue 文件。所以存在着 Vue.use 添加全局插件无效的情况。
文档上说,使用 weex debug [目录] -e [入口文件] 可以调试目录,但是测试没有成功,访问 url 找不到文件。
使用 weex debug 相关命令后,会自动打开浏览器窗口。
如果用官方的 playground 调试,可以扫描右侧二维码,如果自己项目中调试,可以直接填写链接。
把 jsbundle 直接打包进 app 中也可以,总之,读取 jsbundle 就好。
有手机连接后,会显示这个调试界面,手机和 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视角)