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。这里偷个懒,直接继承官方的。我在新控制器中加入了自动刷新逻辑。你会好奇我为什么不调用superviewDidLoad方法?这是因为父类的实现中会隐藏导航栏(而且还有动画),我不想要这样的效果,也不明白这么设计的作用是什么。于是就通过子类覆盖了这个逻辑。

@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中页面导航的实现

这里我们提一下weex的“兄弟”React Native。在导航方面,它们的差异很大。

  • weex:基于UINavigationController,更贴近原生。缺陷是传参只能字符串,调试还需多终端。
  • React Native:基于主视图,切换动画由js管理。缺陷是用户体验不一致,埋点还需小心思。

各有千秋吧。

相关推荐