浅析Ionic页面传参方案

2,334 阅读9分钟

前言

Ionic3 中父页面给子页面传递参数非常容易,使用框架提供的 navParams 来实现就行了,但是反过来子页面给父页面传递就没那么容易了,因为在路由的 navController 里面的 pop 函数并不支持传参数,使得前面的方法没有用了(表面上是,实际上仍然有用,详细看正文),这个时候就需要别的手段来达到我们的目的了,故本文主要将对 Ionic3 中子页面向父页面传参问题提供笔者常用的几种解决方案,若有问题或更多建议,欢迎各位同僚下方评论,共同进步!

简介

在 Ionic 中如何做到页面与页面间的通信?这里给出三种方向:路由传参,事件传参,service 传参。由于三个方向各自又有不同的实现,且每种方案适应的场景都不一样,下面就会从实现,优缺点,适应场景这几个方面来比较一下这几个方案,最后各位看官老爷们各取所需。

一、利用路由传递参数

假如你还不知道Ionic3的路由传值的话,请戳 NavController 了解一下。

好了正文开始,这里要用的就是navParams,前面已经说了,在页面的pop操作里面是不允许传值的,那如何做到用navParams传值。这里给出两种方案

1. 传递“页面“

// 本文出现navCtrl都指代navContronller
// 语法不一定正确,请根据自身代码进行修正

<!--父页面-->
public a = 'a';
navCtrl.push('childrenPage', {
    parent: this
})
<!--子页面-->
parent = navParams.get('parent');
parent.a = 'change from childe';

<!--父页面-->
console.log(a) // change from childe

// 你也可以直接调用父页面的方法

为什么可以这样用?

历史栈

Ionic3 的路由有个历史栈的概念,相信知道栈的同学肯定都很熟悉这个,其实就是和栈一样的操作,app开始在根页面的时候,这个历史栈只有1个页面,就是你的根页面,之后每当使用navController的push方法到一个新的页面的时候,历史栈把新的页面加入进来,pop的时候就把那个页面从历史栈中弹出。

路由

Ionic3 的路由机制比较“神奇”,和别的常见的Vue-Router或Angular单页面路由方式不太一样,有点像伪路由。因为历史栈内的页面全部都真实的存在于当前页面上,嘻嘻,证据如下图:

Ionic3 的伪路由
可以看到,我的页面实际上经过了HomePage —> parentPage —> childPage跳转,现在在childPage了,也就是说历史栈内有这三个页面,而在浏览器内可以看到三个页面都真实的存在。

分析

而查看页面代码可以知道,每个页面不过是被 InoicPage 装饰器修饰过的 Angular 的组件而已,那么就可以通过获取组件的实例来改变组件的状态。结合上面路由的情况我们知道:即使到了子页面,我们的父页面或者说父组件没有被destroy的(可自行验证)。所以在子页面中,我们是可以通过拿到历史页面的实例来对其进行操作的。最后我们再整理一下思路:这里利用navParams来传递父页面本身到子页面中,然后子页面获取到父页面来对其进行操作,通过这个方式来达到传递参数的目的。

优点

使用方式简单高效,甚至可以规避传递参数这一个步骤直接在子页面对父页面进行操作(不推荐)。

缺点

  1. 父子页面之间耦合度较高,子页面的操作对于父页面来说是“隐形”的,使用过度会造成维护的困难,因为你会需要时时刻刻关注子页面对父页面的影响。
  2. 多层级的页面传参需要经过中间多个页面的传递,即使他们或许用不到这个父页面,会导致多个页面传递多个不需要的参数,代码会变得臃肿难以维护。

推荐使用场景

涉及对父页面的操作少且不复杂的场景,且仅推荐父 —> 子间使用,而且希望在使用时声明一个明确的参数名称以表明父子页面之间的联系,提升代码的可维护性。

2. 传递回调

<!--这里是网上常见版本,但笔者不太喜欢此版本-->

<!--父页面-->
public a = 'a';
// 必须使用箭头函数,若不使用,此法将会失效
public parentPromise = (childrenParams) => new Promis((resolve, reject) => {
    // 这里可以做你想利用回传的参数想做的事
    this.a = childrenParams;
    console.log('childe change work: ', this.a);
    resolve();
});
navCtrl.push('childrenPage', {
    parentPromise: this.parentPromise
});

<!--子页面-->
parentPromise = navParams.get('parentPromise');
parentPromise('change from childe').then(() => {
    // 此时会打印 childe change worke: change from childe
    navCtrl.pop();
})

预备知识

这里代码稍微有点绕,需要你了解的知识有 Promise、箭头函数、TypeScript,假如你不太了解,请戳Promise箭头函数,了解过后再看接下来的内容。

分析

  • parentPromise:
    • 接受一个参数(一般由子页面传入),并返回一个 Promise
    • Promise 内部收到参数后就可以利用这个参数执行一些操作,结束 Promise
  • 子页面的 parentPromise 执行顺序:
    • 传入参数,在子页面内执行来自父页面的操作,执行完毕后再执行 pop 回到父页面

注意是在子页面执行 parentPromise 内的代码,代码之所以能够跑成功,原因其实和上面第一种用法的原因是一样的,可以说利用的原理都是一样,但是和第一种略微不同的是:前者是显示的传递 this 给子页面,控制权全部交给了子页面,后者是利用箭头函数将 parentPromise 的 this 绑定在父页面上,控制权在父页面上

优点

  1. 相比之前的做法,此法降低了父页面与子页面的耦合,提升了代码的可维护性。

缺点

  1. 多层级无法高效的使用。
  2. 相比前一种方法,此法更难理解一点。

我想说

此段代码是有问题的。并不是结果有问题,而是代码的实际执行行为与代码表现的执行预期出现的差异的问题。笔者尝试对 parentPromise 解读时的预期是:子页面传入参数 —> 子页面返回 —> 父页面获得参数然后做相关操作,但是实际执行行为如之前的分析根本不是这样的:子页面传入参数 —> 获得参数然后做相关操作 —> 子页面返回,可以发现实际执行过程中 parentPromise 内部的 Promise 其实是没有太多意义的,在子页面完全可以直接回调然后执行 pop 的操作。这就是笔者对这段代码不太喜欢的原因。 这段下面给出笔者的“改进版”供读者评论。

<!--父页面-->
public a = 'a';
// 不使用箭头函数依旧可以使用
public popWithParams(childNavCtrl: navController, childeParams: any) {
    // 回到父页面再操作
    childNavCtrl.pop().then(function() {
        this.a = childrenParams;
        console.log('childe change work: ', this.a);
    }
};
navCtrl.push('childrenPage', {
    popWithParams: this.popWithParams
});

<!--子页面-->
popWithParams = navParams.get('parentPromise');
popWithParams(navCtrl, 'change from childe');

3. 利用this和回调

<!--此法是前两者的结合,和第二种方法更像,但是用法更简单,也更容易理解-->

<!--父页面-->
public a = 'a';

public parentCallback = (childeParams: any) => {
    // this被绑定在父页面
    // 仅当此处代码为异步时(例:数据需要ajax获取),考虑法2传递Promis的用法
    this.a = childrenParams;
    console.log('childe change work: ', this.a);
};
navCtrl.push('childrenPage', {
    parentCallback: this.parentCallback
});

<!--子页面-->
popWithParams = navParams.get('parentPromise');
parentCallback('change from childe');
navCtrl.pop()

优点

使用简单,容易理解,且父子页面耦合也比较低控制权在父页面。

缺点

同前两者。

二、事件传参

1. Ionic 提供的 Events



<!--父页面-->
constructor(public events: Events) {}

ngOnInit() {
    // 订阅来自子页面的事件
    events.subscribe('childParams:doWhenInit', (childParams) => {
        console.log(childParams); // params from childPage
    });  
}

<!--子页面-->
constructor(public events: Events) {}
getParams() {
  this.events.publish('childParams:doWhenInit', 'params from childPage');
}

预备知识

此为 Ionic 提供的事件支持,详细请戳 Ionic Events

优点

  1. 思路易理解,子页面发布一个事件,父页面订阅事件并处理。
  2. 父子页面解耦。
  3. 多层级页面传递参数方便。

缺点

  1. 事件名称不易管理,应用复杂的情况下,很多事件名称需要管理,造成维护上的困难。
  2. 订阅方即父页面需要手动取消订阅,不然可能会订阅多次。
  3. 跨页面传递参数时,订阅方即父页面需要在事件发布时已经生成并订阅事件,才能收到参数。

2. RxJS 的 Subject ,BehaviorSubject

3. Angular 的 EventEmitter

这两种方法都需要搭配 Service 结合,思路和 Ionic 提供的 Events 类似,此处不多做研究了,感兴趣的同学可以看看相关内容。

三、Service

<!--pageParamsManagerService-->
// 可在全应用范围内被访问及改变
public a = 'no change';

<!--父页面-->
import 'pageParamsManagerService' from './pageParamsManagerService'

constructor(public ppmService: pageParamsManagerService) {}

// 注意不可放在 onInit 或 ionViewDidLoad钩子内,因为 push 到子页面时父页面并未销毁, pop 回来时不会执行这两个钩子
ionViewWillEnter() {
    // 第一次进入:no change;
    // 子页面pop进入:change from childePage
    console.log(this.ppmService.a); 
}

<!--子页面-->
import 'pageParamsManagerService' from './pageParamsManagerService'

constructor(public ppmService: pageParamsManagerService) {
    // 改变公共变量
    this.ppmService.a = 'change from childePage';
}


预备知识

Angular 应用里有个 Service 的概念,一般来说 Service 都是单例模式且在全应用级别作用域都可访问(此处不做过多探讨),故我们可以利用这个特性来达到我们的传参目的。

优点

  1. 使用方便简单。
  2. 页面间完全解耦。
  3. 支持多页面间访问,且无页面生成时间要求,多处页面也完全解耦。

缺点

  1. 因为是单例模式且需要在 app.module 引入,需要做好模块管理,这里又是一大坑,感兴趣的同学可以自己了解一下。
  2. 若管理不慎会造成意外的情况,需要时刻注意是 Service 单例的。
  3. 若参数较少,操作不多的情况下,仅仅为了传参引入一个 service 会显得程序较为臃肿。

结语

我们从路由传参、事件传参、service 传参三个方向给出了对应的传参方案并分析了各自优劣和适用场景,笔者能力有限,有更多方案的同学欢迎在评论提出,若发现问题的,也希望不吝赐教,谢谢!!