什么是 weex
weex 是阿里開(kāi)源的一套三端通用方案,只要編寫一次代碼就能運(yùn)行在 iOS、Android 和 Web 上。這里是官網(wǎng)的介紹:
Weex 是一套簡(jiǎn)單易用的跨平臺(tái)開(kāi)發(fā)方案,能以 web 的開(kāi)發(fā)體驗(yàn)構(gòu)建高性能、可擴(kuò)展的 native 應(yīng)用,為了做到這些,Weex 與 Vue 合作,使用 Vue 作為上層框架,并遵循 W3C 標(biāo)準(zhǔn)實(shí)現(xiàn)了統(tǒng)一的 JSEngine 和 DOM API,這樣一來(lái),你甚至可以使用其他框架驅(qū)動(dòng) Weex,打造三端一致的 native 應(yīng)用。
但是實(shí)際上,weex 是從阿里的 WeeApp 發(fā)展而來(lái)的,看開(kāi)源之后 github 上面提交的記錄,大部分工作都是 Vue 框架解析成原生組件的代碼,原生組件早就寫完了。
但是,由于這種版本升級(jí)似的改動(dòng),就造成了一些坑。一開(kāi)始編寫 weex 應(yīng)用的源碼文件擴(kuò)展名是.we,后來(lái),weex 跟 Vue.js 合作直接使用 .vue 文件作為源碼,編寫的方式也改為了 Vue 的格式。
局限在于,并不是所有 Web 特性都支持的,也可以說(shuō)支持的特性很少并且坑很多。
官方的特性支持列表
目前 weex 正處于向 Vue 遷移的過(guò)程,許多文檔和示例代碼都比較舊,bug 也比較多,還不能作為正式的生產(chǎn)工具,最近 weex 正式成為了 Apache 孵化項(xiàng)目,希望能夠成為 ReactNative 這樣的成熟項(xiàng)目。
安裝開(kāi)發(fā)環(huán)境
安裝過(guò)程還是非常友好的,按照官方的教程做就可以了:
Weex 官方提供了 weex-toolkit 的腳手架工具來(lái)輔助開(kāi)發(fā)和調(diào)試。首先,你需要 Node.js 和 weex-toolkit安裝 Node.js 方式多種多樣,最簡(jiǎn)單的方式是在 Node.js 官網(wǎng) 下載可執(zhí)行程序直接安裝即可。對(duì)于 Mac,可以使用 Homebrew 進(jìn)行安裝:
brew install node
安裝完成后,可以使用以下命令檢測(cè)是否安裝成功:
$ node -v
v6.3.1
$ npm -v
3.10.3
通常,安裝了 Node.js 環(huán)境,npm 包管理工具也隨之安裝了。因此,直接使用 npm 來(lái)安裝 weex-toolkit。
但是如果想用 .vue 的模板來(lái)編寫,需要按照說(shuō)明下載 beta 版本的才行!否則下載的環(huán)境生成的項(xiàng)目模板是 .we 的。
weex-toolkit 目前僅有最新的 beta 版本開(kāi)始才支持初始化 Vue 項(xiàng)目,使用前請(qǐng)確認(rèn)版本是否正確。
$ npm install -g weex-toolkit@beta
如果提示權(quán)限錯(cuò)誤(permission error),使用 sudo 關(guān)鍵字進(jìn)行安裝
$ sudo cnpm install -g weex-toolkit@beta
準(zhǔn)備調(diào)試工具
調(diào)試需要 iOS/Android 真機(jī)和 Chrome 瀏覽器。阿里已經(jīng)編寫了一個(gè)原生 app 叫做 "Playground App",可以下載使用,但是最好的辦法還是自己搭建一個(gè) app 畢竟最終還是要把 weex 整合到 app 中才行的。
iOS 的集成很簡(jiǎn)單:
- 使用 cocoapods 安裝 WXDevtool。
- 初始化 weex 頁(yè)面.
- (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 運(yùn)行起來(lái)之后,會(huì)把代碼打包成一個(gè) js bundle, 只要加載上就可以了,無(wú)論是從 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"]]];
}
注意銷毀,不要造成內(nèi)存泄露。
- (void)dealloc{
[self.wxInstance destroyInstance];
}
- 修改 weex 狀態(tài)
參考阿里的 demo,在自己的 app 里面也及時(shí)修改 weex 狀態(tài),狀態(tài)的改變會(huì)直接回調(diào)到 javascript 的相應(yīng)函數(shù)中。
- (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 進(jìn)行編寫的。
初始化項(xiàng)目
直接看官方文檔好了,這部分講解的很清楚。
《Weex 快速上手》
使用 weex-toolkit v1.0.4 按照官方文檔很容易就配置好項(xiàng)目了。
只要使用 weex init 初始化項(xiàng)目,然后進(jìn)入項(xiàng)目目錄,輸入 npm install 配置依子模塊就好了。
各種配置文件已經(jīng)準(zhǔn)備好,幾乎不用修改。
使用 weex 組件和 css 編寫頁(yè)面
weex 三端通用的原理。
看官方文檔上的圖。
源代碼會(huì)編譯成 jsbundle 然后分別發(fā)送給三端解析。
H5 端直接使用 Vue.js 解析預(yù)置的 H5 組件。iOS 端與 android 端直接把組件解析成原生實(shí)現(xiàn)。
因?yàn)閷?duì) css 的解析,需要原生來(lái)做,所以 weex 支持的 css 布局和特性是比較少的。
具體使用可以參考官方文檔。
在 weex 中支持的比較好的布局方式是 flex 布局,block 布局就經(jīng)常存在表現(xiàn)不一致的情況。
- 支持基本的盒模型。
- 支持 position 定位布局。
- 支持使用 flexbox 布局。
使用限制
- 只支持單個(gè)類名選擇器,不支持關(guān)系選擇器,也不支持屬性選擇器。
- 默認(rèn)是組件級(jí)別的作用域,沒(méi)有全局樣式。
- 不支持樣式繼承(因?yàn)橛凶饔糜蚋綦x)。
- 考慮到樣式的數(shù)據(jù)綁定,樣式屬性暫不支持簡(jiǎn)寫。
組件的使用
組件使用只要參考官方文檔就可以了,雖然一開(kāi)始寫起來(lái)有些別扭,例如<a>標(biāo)簽中不能包含文字,但是按照文檔寫也能完成任務(wù)。
有些控件可能不太符合要求,例如<video>標(biāo)簽,在 iOS 上面的實(shí)現(xiàn)是一款開(kāi)源的播放器,自帶的 UI 可能會(huì)不符合要求,這種可以自己修改 native 的實(shí)現(xiàn)。
自定義組件
Weex 自定義組件有兩種方式,一種是使用 Vue 的組件定制方式,另一種是三端分別實(shí)現(xiàn)相應(yīng)組件。
在擴(kuò)展 Weex 組件時(shí),如果只使用了 Weex 提供的內(nèi)置組件,并且使用的都是 Weex 支持的樣式,那么就和普通的自定義組件無(wú)異,不需要 Native 端再有相應(yīng)的實(shí)現(xiàn)。如果你定制組件時(shí)不得不用到目前 Weex 不支持的標(biāo)簽和樣式,在這種情況下才是真正的“擴(kuò)展”了 Weex 的組件,你還需要在 Android 和 iOS 中有相應(yīng)的實(shí)現(xiàn),不然會(huì)導(dǎo)致渲染異常。
Weex HTML 擴(kuò)展
- (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 對(duì)象
- 使用- (instancetype)initWithRef:(NSString )ref type:(NSString)type styles:(nullable NSDictionary *)styles attributes:(nullable NSDictionary *)attributes events:(nullable NSArray *)events weexInstance:(WXSDKInstance *)weexInstance 初始化對(duì)象并接收參數(shù)。
- 在 - (void)viewDidLoad 中初始化自定義 UI。不要在 - (void)viewWillLoad 初始化 UI 會(huì)導(dǎo)致- (void)viewWillLoad 和 - (UIView *)loadView 反復(fù)調(diào)用,最終出現(xiàn) StackOverFlow。原因還需要查看一下 Weex 源碼來(lái)確定。
- 必要的情況下,使用- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params 向 Vue.js 框架發(fā)送事件。
首先,在 Android 里肯定是可以監(jiān)聽(tīng)到“返回”按鈕的點(diǎn)擊事件的,其實(shí)只要實(shí)現(xiàn) Activity 里的 onBackPressed 接口就可以了,它會(huì)在當(dāng)前視圖里點(diǎn)擊返回按鈕時(shí)執(zhí)行。在 weex-hackernews Andorid 項(xiàng)目里的 MainActivity.java 中,就實(shí)現(xiàn)了 onBackPressed 接口:
public void onBackPressed() {
Log.e("USER ACTION", "BACK");
WXSDKManager.getInstance().fireEvent(mWXSDKInstance.getInstanceId(), "_root", "androidback");
}
在這個(gè)方法里,通過(guò) WXSDKManager.getInstance() 取到了當(dāng)前頁(yè)面的實(shí)例,然后調(diào)用 fireEvent 接口給根視圖派發(fā) androidback 事件,事件名是可以自定義的。在 Weex Runtime 中會(huì)接收到這個(gè)事件,會(huì)傳遞給 Vue.js 框架,并且觸發(fā)最外層組件的 androidback 事件,最終會(huì)找到 back 方法并執(zhí)行。(這里說(shuō)的 Weex Runtime 是前端代碼實(shí)現(xiàn)的,比 Vue.js 更底層一些)。
在 Android 中派發(fā)原生事件
- 為 HTML 編寫適合 Web 端特性的組件。參考 weex-html5 擴(kuò)展開(kāi)發(fā)指引 文章比較老,是 we 文件的示例。
如果編寫的組件使用 weex 已有的組件就可以拼裝而成,直接使用 Vue.js 的編寫單文件組件的方式編寫就可以了
開(kāi)始進(jìn)行調(diào)試
由于 Weex 開(kāi)發(fā)工具還在快速變化,所以文檔上寫的不怎么清除。
如果是調(diào)試單獨(dú)的 vue 文件,直接使用 weex debug 命令就好了。
但是此時(shí)程序的入口其實(shí)是 weex.html -> 單個(gè) vue 文件。所以存在著 Vue.use 添加全局插件無(wú)效的情況。
文檔上說(shuō),使用 weex debug [目錄](méi) -e [入口文件] 可以調(diào)試目錄,但是測(cè)試沒(méi)有成功,訪問(wèn) url 找不到文件。
使用 weex debug 相關(guān)命令后,會(huì)自動(dòng)打開(kāi)瀏覽器窗口。
如果用官方的 playground 調(diào)試,可以掃描右側(cè)二維碼,如果自己項(xiàng)目中調(diào)試,可以直接填寫鏈接。
把 jsbundle 直接打包進(jìn) app 中也可以,總之,讀取 jsbundle 就好。
有手機(jī)連接后,會(huì)顯示這個(gè)調(diào)試界面,手機(jī)和 Nodejs 服務(wù)會(huì)使用 websocket 通信,挺好用的。
最后說(shuō)一下目錄調(diào)試。
說(shuō)是調(diào)試,其實(shí)是直接運(yùn)行。因?yàn)闆](méi)有搞清楚如何用 weex debug 命令調(diào)試目錄,于是就直接運(yùn)行了編譯好的 jsbundle。
直接輸入 npm run dev 命令和 npm run serve,如果8080端口沒(méi)有占用,那么訪問(wèn) localhost:8080/weex.html 就打開(kāi)了 app.web.js 這個(gè) bundle。
在 app 里面訪問(wèn) serviceip:8080/dist/app.weex.js 就可以打開(kāi)這個(gè) bundle。
Hanks10100 大神寫的比較系統(tǒng)的介紹 weex 文章
使用 Weex 和 Vue 開(kāi)發(fā)原生應(yīng)用 —— 0 項(xiàng)目介紹和文章目錄