Weex中页面导航的实现
Weex为我们提供了navigator模块来控制页面的导航。Navigator模块究竟是怎么运作的,官方没有给我们一个感性的认识。本文旨在探究weex的导航机制,然后实现一个DEMO供参考。
推入一个页面,类似原生的pushViewController:animated:
`。文档告诉我们这么做:
navigator.push({ url: 'http://dotwe.org/raw/dist/519962541fcf6acd911986357ad9c2ed.js', animated: "true" })
顺藤摸瓜,找到原生模块WXNavigatorModule
,push方法是这样定义的:
- (void)push:(NSDictionary *)param callback:(WXModuleCallback)callback { id<WXNavigationProtocol> navigator = [self navigator]; UIViewController *container = self.weexInstance.viewController; [navigator pushViewControllerWithParam:param completion:^(NSString *code, NSDictionary *responseData) { if (callback && code) { callback(code); } } withContainer:container]; }
核心代码在WXNavigationProtocol
协议的默认实现中。于是乎我们切换到官方实现类WXNavigationDefaultImpl
,关键的代码都在这里了。不出所料,是基于UINavigationController
的。
WXBaseViewController *vc = [[WXBaseViewController alloc] initWithSourceURL:[NSURL URLWithString:param[@"url"]]]; vc.hidesBottomBarWhenPushed = YES; [container.navigationController pushViewController:vc animated:animated]; [self callback:block code:MSG_SUCCESS data:nil];
按照weex的设计原则,主视图会附加在一个UIViewController
上。由这个UIViewController
控制页面的加载和展示。承载weex页面的控制器,必须包含在一个UINavigationController
中,否则导航无效。
Weex提供了基础的容器控制器类WXBaseViewController
。在初始化时提供javascript代码的地址,它会从这个地址获取代码并展示页面。WXNavigatorModule
默认使用WXBaseViewController
来展示新的页面。我们可能需要对导航进行定制,或者用一个我们自己实现的控制器代替官方版本。只需要两步就可以做到:
- 实现自己的weex容器控制器。
- 实现自己的
WXNavigationProtocol
协议类,替换官方版本。
我实现了WXViewController
。这里偷个懒,直接继承官方的。我在新控制器中加入了自动刷新逻辑。你会好奇我为什么不调用super
的viewDidLoad
方法?这是因为父类的实现中会隐藏导航栏(而且还有动画),我不想要这样的效果,也不明白这么设计的作用是什么。于是就通过子类覆盖了这个逻辑。
@interface WXViewController (Private) @property (nonatomic, strong) NSURL *sourceURL; - (void)_renderWithURL:(NSURL *)sourceURL; @end @interface WXViewController () <SRWebSocketDelegate> @property (nonatomic, strong) SRWebSocket *hotReloadSocket; @end @implementation WXViewController - (void)dealloc { #if DEBUG [self.hotReloadSocket close]; #endif } - (void)viewDidLoad { void (*viewDidLoad)(id, SEL) = (void (*)(id, SEL))class_getMethodImplementation([UIViewController class], @selector(viewDidLoad)); viewDidLoad(self, @selector(viewDidLoad)); self.view.backgroundColor = [UIColor whiteColor]; self.automaticallyAdjustsScrollViewInsets = NO; [self _renderWithURL:self.sourceURL]; #if DEBUG NSString *hotReloadURL = @"ws://127.0.0.1:8082"; if (hotReloadURL){ _hotReloadSocket = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:hotReloadURL]]; _hotReloadSocket.delegate = self; [_hotReloadSocket open]; } #endif } - (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message { if ([@"refresh" isEqualToString:message]) { [self refreshWeex]; } } @end
接下来就是WXNavigationImpl
了。我只需要修改一个方法,于是同样选择了继承WXNavigationDefaultImpl
。这个类的头文件没有公开怎么办?拷贝WXNavigationDefaultImpl.h到自己的项目就行啦。下面是我的实现,实际上只替换了容器控制器,其他代码不变。
@interface WXNavigationImpl (Private) - (void)callback:(WXNavigationResultBlock)block code:(NSString *)code data:(NSDictionary *)reposonData; @end @implementation WXNavigationImpl - (void)pushViewControllerWithParam:(NSDictionary *)param completion:(WXNavigationResultBlock)block withContainer:(UIViewController *)container { if (0 == [param count] || !param[@"url"] || !container) { [self callback:block code:MSG_PARAM_ERR data:nil]; return; } BOOL animated = YES; NSString *obj = [[param objectForKey:@"animated"] lowercaseString]; if (obj && [obj isEqualToString:@"false"]) { animated = NO; } WXViewController *vc = [[WXViewController alloc]initWithSourceURL:[NSURL URLWithString:param[@"url"]]]; vc.hidesBottomBarWhenPushed = YES; [container.navigationController pushViewController:vc animated:animated]; [self callback:block code:MSG_SUCCESS data:nil]; } @end
然后,在[WXSDKEngine initSDKEnvironment]
调用后替换默认的handler:
[WXSDKEngine registerHandler:[WXNavigationImpl new] withProtocol:@protocol(WXNavigationProtocol)];
设置根视图控制器,这里我们使用了UINavigationController
保证导航能够被支持。
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8081/index.weex.js"]; UIViewController *demo = [[WXViewController alloc] initWithSourceURL:url]; [[UIApplication sharedApplication] delegate].window.rootViewController = [[UINavigationController alloc] initWithRootViewController:demo];
在index.vue代码中,添加一个按钮到屏幕中间,然后绑定一个点击事件。这里为了演示的目的,我们选择跳转到同样的地址。
onclick: function (e) { const navigator = weex.requireModule('navigator') navigator.push({ url: 'http://127.0.0.1:8081/index.weex.js' }) }
我们启动打包服务器weex preview index.vue
,编译运行iOS项目试一下,完美!
可是我的手又痒了,想要在导航栏上添加一个按钮。我在WXNavigatorModule
中找到了方法定义:- (void)setNavBarRightItem:(NSDictionary *)param callback:(WXModuleCallback)callback
。不过需要传一个字(对)典(象),没有文档只好翻源码了。源码比较简单这里就不解释了。因为界面一开始展示的时候就需要显示导航按钮,我们使用beforeCreate生命周期方法。
beforeCreate: function () { navigator.setNavBarRightItem({ title: 'fun', // 编程很有乐趣 titleColor: 'blue' // 不设置就是透明的看不见 }) }
按钮可以显示,点击事件怎么解决呢?在网上搜了一下一无所获,还是啃代码自力更生吧。导航按钮的点击事件绑定到了WXNavigationDefaultImpl
的- (void)onClickBarButton:(id)sender
方法。最核心的一行代码,是触发了一个事件。
[[WXSDKManager bridgeMgr] fireEvent:button.instanceId ref:WX_SDK_ROOT_REF type:eventType params:nil domChanges:nil];
看到fireEvent
方法有些懵,这里我解释一下前三个参数:
instanceId
:页面的ID。避免给一个控制器的事件跑到另一个控制器。我猜测weex组件的ID只能保证在一个页面中唯一。ref
:组件的ID,根据命名判断为根组件(实际情况也是如此)。eventType
:事件类型,这里为clickrightitem
。
既然事件给了根组件,我们只需要把点击事件绑定到根组件就可以啦。至于点击触发什么效果随意啦。
<template> <div class="wrapper" @clickrightitem="onclickrightitem"> ...
最后效果是这样的。PS:导航按钮fun会在页面切换动画完毕才显示出来,暂时忍了吧。
这里我们提一下weex的“兄弟”React Native。在导航方面,它们的差异很大。
- weex:基于
UINavigationController
,更贴近原生。缺陷是传参只能字符串,调试还需多终端。 - React Native:基于主视图,切换动画由js管理。缺陷是用户体验不一致,埋点还需小心思。
各有千秋吧。