Ionic2入门教程 实现TodoList App-2 实现TodoList App

在上一篇教程 中,我们介绍了Ionic2框架和使用Ionic2的命令行工具创建一个项目,并介绍了创建的项目结构和各个部分。在这一部分教程中,我们还是来实现一个TodoList的实例,来看看怎么用Ionic2开发一个web应用。

本教程的源码,可以从github上获得,感兴趣的同学可以结合着源码阅读以下内容。

最终实现的效果还是跟之前的教程Angular2入门教程-2 实现TodoList App 实现的类似,最终结果如下:
ionic2-todo-app.png
为了方便,大部分的页面和样式,都直接沿用了之前的实例里的内容。只是上面使用了ionic提供的header组件,header里面添加了一个按钮,跳转到about页面。

在开始编码之前,我们还是先看看这个简单的应用的组件结构图:
components.jpg

为了演示模块化开发的方式,我们还是把这个简单的应用分成2个模块,主模块和todo模块。主模块里面包含根组件app.component,和 about组件。todo模块包含了3个todo的组件:list, itemdetail。在实际上的项目开发中,模块化开发是非常重要的。如果不是用模块化的开发,即使我们的组件都是独立的,也需要都在app模块里面注册,这样多人协作开发时,很容易在app模块文件上出现冲突。而且每个模块里面的增加删除页面,都需要更新主模块,使得项目各组件无法解耦。

主模块

首先,我们还是从主模块入手。在 上一部分的教程中,我们简单介绍了Ionic2的主模块app module和app.component,现在再来详细看看加入了新的todo模块以后,主模块该如何编写。

app.module

首先,看app.module.ts的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicModule, IonicApp, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { TodoModule } from '../pages/todo/todo.module';
import { AboutComponent } from '../pages/about/about.component';
@NgModule({
declarations: [ MyApp, AboutComponent ],
imports: [ IonicModule.forRoot(MyApp), TodoModule ],
bootstrap: [IonicApp],
entryComponents: [ MyApp, AboutComponent ],
providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

在一开始创建的项目模板中,是这样的:

1
2
3
4
5
declarations: [ MyApp, HomePage ],
imports: [ IonicModule.forRoot(MyApp) ],
bootstrap: [IonicApp],
entryComponents: [ MyApp, HomePage],
// providers以及后面的省略

可以看到,主要的区别就是imports的部分,因为我们新加了一个模块,就需要在这里把新模块加入进来。这样就不需要在主模块中把所有的子模块的所有的组件都一个个的在这里注册。
然后,我们也在这里的bootstrap中设置了这个应用的根组件是通过IonicModule.forRoot(MyApp)引入的组件IonicApp

app.component

然后再看根组件,在app.component.ts里,定义了一个组件MyApp,也就是应用的根组件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { TodoListComponent } from '../pages/todo/list/list.component';
@Component({
templateUrl: 'app.html'
})
export class MyApp {
rootPage = TodoListComponent;
constructor(platform: Platform) {
platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
StatusBar.styleDefault();
Splashscreen.hide();
});
}
}

它定义了这个组件的模板是app.html,在构造函数里,通过Platform组件来控制app的状态栏的样式,以及关闭app的加载页。这个只是在打包成app的时候才起作用。
StatusBar是一个cordova的插件,用于控制应用在打开时的状态栏的样式、颜色,如是否透明等。
Splashscreen是app的加载页的插件。这里调用它的hide方法确保加载页被关闭。
而所有cordova的插件的方法,都必须在platform.ready()的回调里面调用,也就是在应用打开后,cordova底层的框架初始化完成以后,再去调用相应的插件的方法。

如果你想针对你的web应用做一些环境的初始化之类的操作,也应该在这里实现。例如,你的web应用既想打包成app,又想通过手机浏览器提供访问,这样的话,你就需要设置你的api的路径。就可以在这个ready方法里面这样实现:

1
2
3
4
5
6
7
if (this.platform.is('cordova')) { // 有cordova表示是app
// 在app里,需要指定api服务器的地址
AppConstants.apiHost = 'http://some.server.com/api'
} else { // 没有则说明是web方式部署
// 在web方式里,api服务器的地址,和部署这个web应用的地址一样。
AppConstants.apiHost = ''
}

app.component组件的定义中,我们没有看到样式的定义,但是在文件夹中又可以看到有一个app.scss的文件。实际上,Ionic2在编译打包的时候,默认就是查找跟模板名一样,后缀是.scss的样式文件,来作为模板样式。所以,我们在开发Ionic应用的时候也应该使用scss的样式会比较方便。

虽然很多框架都使用sass(或scss)来定义样式,但是,在国内基于npm使用sass,非常的不方便,主要是因为qiang的问题。编译sass(或scss)都要使用node-sass库,而这个库又依赖lib-sassnode-gyp等库,而这些库的安装,又需要本地编译,需要gcc或微软的.net环境等。所以,这些库不能直接复制过来使用,必须安装,而国内的网络环境访问npm资源库经常会非常慢。虽然国内有taobao的镜像,但是在本地编译安装上面说的库的时候,还是得从国外的地址下载。(至少在我之前安装的时候是这样) 。所以,想搞前端开发的,还是先弄好科学上网的环境,不然会非常麻烦。

回归正题,再看看模板app.html里面的内容:

1
<ion-nav [root]="rootPage"></ion-nav>

这里使用了一个ionic的导航器组件ion-nav。在Ionic2中,它没有使用Angular2的路由模块,而是实现了一套自己的导航模块。它跟Angular2的路由的功能类似,但是在用法上又有很大的区别。我们先直接看怎么用,在本教程的最后再总结它和Angular2路由模块的区别。简单来说,在根组件的模板中,有一个导航器,那么,这个导航器的控制器就帮我们来实现从这个根组件开始的页面的前进(载入)和后退。
这里,使用[root]="rootPage"进行数据绑定,也就是从组件component到页面view的单向绑定。然后在MyApp里,看到rootPage = TodoListComponent;。关联到一起就是,我们把TodoListComponent组件作为这个web应用的第一个页面(root页面),赋给导航器。那么,我们的应用在打开根组件app.component的时候,就会加载第一个页面组件TodoListComponent

对于绑定不了解的,可以看看另一篇有关 Angular2架构浅析 的教程。

about.component

这个组件没什么说的,就是一个简单的页面,显示一段话。模板内容如下:

1
2
3
4
5
6
7
8
9
<ion-header>
<ion-navbar>
<ion-title>关于</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<p>About angular2-basic</p>
<p>Just a simple example project to get started with Angular. Now includes routes!</p>
</ion-content>

我们使用了ion-header来添加一个header,里面有一个ion-navbar,标题是关于。由于我们使用Ionic2提供的导航器,所以,当我们从别的页面进入这个页面的时候,header的左侧就会出现一个后退按钮。点击后退按钮以后,就会退到上一页。这就是Ionic2提供的导航器的优点:自动判断历史记录,自动显示或隐藏后退按钮,并且实现后退功能。

todo模块

todo模块的定义也很简单,就是把里面使用的各个组件和服务注册一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { MyApp } from '../../app/app.component';
import { TodoListComponent } from './list/list.component';
import { TodoDetailComponent } from './detail/detail.component';
import { TodoItemComponent } from './item/item.component';
import { TodoService } from './todo.service';
@NgModule({
imports: [IonicModule.forRoot(MyApp)],
declarations: [TodoListComponent, TodoDetailComponent, TodoItemComponent],
entryComponents:[TodoListComponent, TodoDetailComponent],
providers: [TodoService],
exports: [IonicModule]
})
export class TodoModule {}

需要注意的是,在Ionic2中,所有的需要加到导航器里显示的页面组件,必须加到entryComponents里。如果只是一个modal,那就不需要。在这个例子中,TodoItem是todo列表的一个子组件,它不会被单独显示到页面上,所以不需要加到entryComponents里。
还有,所有的服务组件需要加到providers里,跟Angular2一样。

TodoService和Todo

TodoService和Todo跟之前的实例完全一样,Todo是待办事宜的model定义,TodoService里面定义了一系列的增删改查方法。

TodoListComponent

接下来看这个列表组件,也就是打开app的时候,根组件里面导航器里加载的根页面。还是先直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Todo } from '../todo';
import { TodoService } from '../todo.service';
import { AboutComponent } from "../../about/about.component";
@Component({
selector: 'todo-list',
templateUrl: 'list.component.html'
})
export class TodoListComponent {
newTodo: Todo = new Todo();
constructor(public navCtrl: NavController, private todoService: TodoService) { }
addTodo() {
if (!this.newTodo.title.trim()) {
return;
}
this.todoService.addTodo(this.newTodo);
this.newTodo = new Todo();
}
get todos() {
return this.todoService.getAllTodos();
}
gotoAbout() {
this.navCtrl.push(AboutComponent)
}
}

TodoListComponent里,有一个成员变量newTodo,类型是Todo,用来新建一个待办事宜的时候保存新建的数据。
在构造函数里,我们使用Angular2的依赖注入,注入了2个服务:NavControllerTodoService。后一个不用说,就是用来操作业务数据的。NavController就是Ionic2导航器的控制器。

TodoListComponent里,header里面的右上角有一个按钮可以跳转到about页面,所以,这个组件里面有一个方法gotoAbout(),它用this.navCtrl.push(AboutComponent)打开一个about页面。
下面就是列表组件的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<ion-header>
<ion-navbar>
<ion-title>Todo List</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="gotoAbout()">
<ion-icon name="construct"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-card>
<section class="todoapp">
<header class="header">
<input class="new-todo" placeholder="Get things done!" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
</header>
<section class="main" *ngIf="todos.length > 0">
<ul class="todo-list">
<todo-item *ngFor="let todo of todos" [todo]="todo">
</todo-item>
</ul>
</section>
<footer class="footer" *ngIf="todos.length > 0">
<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>
</section>
</ion-card>
</ion-content>

这个模板里,上面有一个header,下面的内容跟之前的实例一样,它的解释请参考之前的教程Angular2入门教程-2 实现TodoList App
实例中所有的css都是在相应的.scss文件中,具体内容请参考实例源代码

Ionic2中,所有的页面跳转都由导航控制器来实现,它有一些方法,其中常用的有:

  • push(page, params, opts)
    用来往导航器的栈里加一个组件,也就是前进到一个页面。
  • pop(opts)
    就是从导航器的栈里弹出一页,也就是后退一页。
  • setRoot(page, params, opts)
    这个用来设置跟页面,设置跟页面以后,历史记录会被清空,也就无法后退。

Ionic2的导航器还有一些hooks,你可以用来定义在导航器加载某个组件的时候调用的方法,例如,ionViewCanEnter,你可以通过在一个组件里实现这个方法,来控制用户是否能够进入这个页面。例如对于todo list页面,我们控制用户需要登录才能打开,大概可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class TodoListComponent {
// 省略其他...
ionViewCanEnter(): Promise<boolean> {
console.log('in home component can enter.')
return this.authService.isLoggedIn().map(isLogin => {
if (isLogin) {
console.log('ionView Home Can Enter.')
return true
} else {
console.log('ionView Home Cannot Enter.')
this.navCtrl.push(SigninPage)
return false
}
}).toPromise()
}
}

它需要返回一个boolean类型的值,或者一个boolean类型的promise。我这里是在一个authService里判断用户是否登录,而我的所有service返回的都是Observable,所以我把它在转换成promise返回。

其他的方法和详细说明可以参考官方文档

TodoItemComponent

这个也不需要多说,就是列表组件的一个子组件,显示每一条待办事宜。具体的还是请参考之前的教程。只有一个不一样的地方就是,点击一个item项跳转到详情页。这里还是用的NavController,具体如下:

1
2
3
gotoDetail(todo) {
this.navCtrl.push(TodoDetailComponent, {todoId: todo.id})
}

在这里跳转的时候传了一个参数todoId

TodoDetailComponent

详情页的组件也很简单,在组件初始化的时候,获取传过来的参数todoId,并用这个参数从todoService里获得todo。然后模板就会将todo数据显示到页面上。

1
2
3
4
5
6
7
8
export class TodoDetailComponent implements OnInit {
selectedTodo: Todo;
constructor(public navParams: NavParams, private todoService: TodoService) {}
ngOnInit() {
let todoId = this.navParams.get('todoId')
this.selectedTodo = this.todoService.getTodoById(todoId);
}
}

编译和打包

最后,再提一下编译打包的问题,这里说的打包,是将TypeScript的文件编译并合并打包成main.js,所有的css页打包成main.css。对于使用cordova将应用打包成app,也没什么可说的,就是用它的命令打包就可以,网上的相关教程也很多。
Ioni2给我们提供了一套编译打包的脚本,并把它们放在单独的库里:https://github.com/driftyco/ionic-app-scripts。 然后在ionic的项目的package.json里,加入这个依赖就可以使用。它给我们提供了很多现成的脚本,用于执行多种任务,例如编译、监听文件修改、并启动开发服务器,打包成main.js和main.css,minify等。
通过命令行工具生成的项目里,提供了4个执行命令:

1
2
3
4
5
6
"scripts": {
"clean": "ionic-app-scripts clean",
"build": "ionic-app-scripts build",
"ionic:build": "ionic-app-scripts build",
"ionic:serve": "ionic-app-scripts serve"
},

ionic buildnpm run build执行的是ionic-app-scripts build,就是执行编译,生成main.js和main.css。
除了这几个默认提供的以外,常用的还有一个:

1
"min": "ionic-app-scripts minify",

可以通过npm run min执行。执行这个之前,需要先执行`npm run build,也就是main.js和main.css文件必须存在,然后,min这个命令就会执行一系列的代码混淆、压缩等脚本,将js和css都进行压缩。在这篇教程的实例项目中,压缩前main.js是5.5M左右,压缩后是1.3M。

总结

上面,就是一个比较完整的Ionic2 app的开发的过程,如果你需要从服务器获取数据,可以使用Angular2的http模块,使用方法也一样。剩下的,就是了解Ionic2提供的样式组件和服务组件,特别是服务组件的使用,需要仔细阅读官方文档。然后,基本上你就可以用Ionic2开发应用了,不管是web应用、混合app。你用习惯Ionic以后,就会发现,开发app会变得非常简单。

Ionic2是完全基于Angular2,它的很多概念和用法,也都是来自于Angular2。但是,也有一些不一样,最主要的可能就是路由了。在上面说到,Ionic2使用自己的导航器组件来实现路由和导航。它跟Angular2的路由模块相比,主要的区别有:

  1. Ionic2的导航器不是基于url的,也就是说,默认情况下,导航不会引起地址栏的变化,你也无法通过在地址栏输入一个url,来直接进入一个组件页面。但是,Ionic2又提供了一个DeepLinker的组件,来实现url和导航器的结合。你需要在模块定义里面,用IonicModule引入根组件的时候,定义组件和路径的对应关系。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    imports: [
    IonicModule.forRoot(MyApp, {backButtonText: ''}, {
    links: [
    { component: Home, name: 'home', segment: 'home' },
    { component: SigninPage, name: 'sign', segment: 'sign' },
    { component: TodoListPage, name: 'list', segment: 'list', defaultHistory: [Home] },
    { component: TodoDetailPage, name: 'detail', segment: 'detail', defaultHistory: [Home] }
    ]
    }),
    HttpModule
    ],

    具体用法请参考官方文档。

  2. Ionic2的导航器无法嵌套,你不能像Angular2的路由一样,通过嵌套的路由来实现一个模块中的页面、子页面等功能。所以也无法通过嵌套的路由来实现权限控制。例如,有一套路由/account, /account/wallet/account/address。如果使用Angular2的路由,我们可以在/account这个路由的定义上加一个Guard,来控制只有用户登录了才能访问/account和它所有的子路径。但是Ionic2的导航器就无法实现这种控制。

  3. Angular2的路由是一个服务,整个应用里面应该只有一个路由配置。虽然你可以通过子模块、延时加载模块来实现多个路由配置。但是从效果上来说,他们应该也是一起构成一套路由配置。但是Ionic2里面的导航器就是一个控制器类,以及配套的navbar导航栏。举个例子来说,在一个典型的多个tab页面的应用中,通常,有关页面直接导航跳转是设计是这样:在每个tab页里面,你可以进入一个个页面,后退的时候,又是一页一页后退到这个tab的第一个页面。你不应该能后退到进入这个tab之前的那个tab页。也就是说,在每个tab里面,有一个导航历史的栈,在一个tab里面的前进后退不会影响另一个tab页里的前进后退。这虽然不是什么强制的标准,但是也是从ionic1开始就遵守的模式。在Ionic2中,对于这种情况,它在每个tab页里创建了一个导航器,每个导航器控制自己tab页内的前进后退。如果你使用了它提供了导航栏,是否显示后退按钮、以及点击后退的行为都是已经实现好的。
坚持原创技术分享,您的支持将鼓励我继续创作!