服务的采用Asp.net API实现,数据库用的sqlite,具体实现请看:源代码
唯一需要说明的是跨域问题:
跨域代码:
<system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> <add name="Access-Control-Allow-Headers" value="Content-Type,Accept,Authorization" /> <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE" /> </customHeaders> </httpProtocol>
配置允许Content-Type,Accept,Authorization三种头。
API功能列表:
http://localhost:1856/api/UserLogin | 用户登录并返回用户信息 |
http://localhost:1856/api/UserInfo/id | 根据ID查找用户信息 |
http://localhost:1856/api/UserReg | 注册 |
系统目录规划说明:
详细开发过程
1. 新建登录页面
定位到src\pages目录下,运行命令:
ionic g page login
2. 添加一个Tab页显示登录页
在ionic2.1下面变得容易了,在tabs.ts里面引入并且制定tab,然后在前台增加这个tab即可:
<ion-tab [root]="tab4Root" tabTitle="me" tabIcon="person"></ion-tab>
图标可以采用系统也可以自定义。
然后运行项目:
ionic serve
结果发现点击me不会出现新建的页面?
打开app下面的app.module.ts发现系统所有页面都需要在这里配置一遍才能使用。
这里说明ionic2开发的第一个规则:
规则一:所有页面均需提前在app.module.ts中配置。
然后等待几秒或者重新启动一下就能看到页面。注意:我这里新建的页面名叫login但是我把页面对应的ts类名改为LoginPage了,所以在引用时用的是LoginPage。
3. 写Login的前台布局:
代码
<ion-content padding> <form [formGroup]="loginForm" > <ion-item> <ion-label>Username</ion-label> <ion-input type="text" formControlName="LoginID"></ion-input> </ion-item> <p *ngIf="!loginForm.controls.LoginID.valid && loginForm.controls.LoginID.touched" color="danger">LoginID must is email.</p> <!-- <p *ngIf="loginForm.controls.LoginID.valid && loginForm.controls.LoginID.touched" secondary> LoginID is good</p>--> <ion-item> <ion-label>Password</ion-label> <ion-input type="password" formControlName="LoginPwd"></ion-input> </ion-item> <button ion-button full (click)="login(loginForm.value, $event)" [disabled]="!loginForm.valid">Dark</button> <button ion-button full (click)="signup()">Create Account</button> </form> </ion-content>
解释:
<form [formGroup]="loginForm" >:声明一个表单对象,访问此表单元素需要。
<ion-item>:代表一个条目,一般是一行
<ion-label>:文本标签
<ion-input type="text" formControlName="LoginID">:文本框标签,数据源名称LoginID
<button ion-button full (click)=:按钮标签,full表示占满当前行,后面是单击事件
(click)="login(loginForm.value, $event)":传入参数为表单数据源对象
[disabled]="!loginForm.valid":表单验证通过之后才显示
光写前台是出不了效果的,因为有表单验证数据源
4. Login后台
称之为后台不太准确,但很形象:ts控制html
因为要使用验证和表单对象所以需要引入:
import { FormBuilder, Validators } from '@angular/forms';
同时在构造函数中声明:
private formBuilder: FormBuilder,
初始化表单数据源:
loginForm = this.formBuilder.group({
'LoginID': ['admin@163.com', [Validators.required, Validators.minLength(4),emailValidator]],
'LoginPwd': ['123456', [Validators.required, Validators.minLength(4)]]
});
第一个参数是默认值,一般为空,此处为调试时懒得输入,后面是验证规则,Email的验证我使用的自定义验证规则,把验证规则抽出到单独的文件中去了,此处是支持写正则表达式的。
到了这一步,页面应该能显示出来了。
5. 抽离验证类
在Providers下面新建一个validator.ts,引入表单资源,编写公用验证类
代码如下:
import {FormBuilder,FormControl,Validators,AbstractControl } from '@angular/forms'; export function emailValidator(control: FormControl): { [s: string]: boolean } { if (!control.value.match(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)) { return { invalidEmail: true }; } } export function nicknameValidator(control: FormControl): { [s: string]: boolean } { if (!control.value.match(/^[(\u4e00-\u9fa5)0-9a-zA-Z\_\s@]+$/)) { return { invalidNickname: true }; } }
6. 抽离实体类:
Model文件夹下面新建一个UserInfoData.ts文件,新建模型类,可参考一般model的写法,当然也可以在这里初始化一些数据源什么的。
代码如下:
export class UserInfoData {
ID:number;
LoginID:string;
LoginPwd:string;
RealName:string;
FaceImg:string;
Sex:string;
UserToken:string;
Birthday:Date;
InDate:Date;
}
7. 抽离本地存储类:
由于项目中用到临时数据存储,所以需要抽离出一个公用的本地存储访问服务:
StorageService.ts操作的是html5的localStorage。代码如下
import { Injectable } from '@angular/core'; @Injectable() export class StorageService { constructor() { } write(key: string, value: any) { if (value) { value = JSON.stringify(value); } localStorage.setItem(key, value); } read<T>(key: string): T { let value: string = localStorage.getItem(key); if (value && value != "undefined" && value != "null") { return <T>JSON.parse(value); } return null; } remove(key: string) { localStorage.removeItem(key); } clear() { sessionStorage.clear(); } }
8. 抽离http请求类:
其实一般可以不用抽离的,但是本项目中用到API访问控制,思路是除了公开API,其他的都需要用token去访问,而这个token是和用户挂钩的,在注册的成功就生成了,调用系统功能时每个人必须带上自己的token,否则API返回401.
实现思路是:登录时返回用户信息,其中包含token存储在本地,以后调用时从本地取出,连同请求一起发给服务器。要实现4种请求
httpGetWithAuth、httpGetNoAuth、httpPostNoAuth、httpPostWithAuth
代码如下:
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { StorageService } from "./StorageService"; import { UserInfoData } from "./../model/UserInfoData"; @Injectable() export class HttpService { myInfoLocal: any; local: Storage; constructor( private http: Http, private storageService: StorageService) { //this.local = new Storage(LocalStorage); } public httpGetWithAuth(url: string) { let user = this.storageService.read<UserInfoData>('UserInfo'); var headers = new Headers(); headers.append('Content-Type', 'application/json'); headers.append('Authorization', user.ID + '-' + user.UserToken); let options = new RequestOptions({ headers: headers }); return this.http.get(url, options).toPromise() .then(res => res.json()) .catch(err => { this.handleError(err); }); } public httpGetNoAuth(url: string) { var headers = new Headers(); headers.append('Content-Type', 'application/json'); let options = new RequestOptions({ headers: headers }); return this.http.get(url, options).toPromise() .then(res => res.json()) .catch(err => { this.handleError(err); }); } public httpPostNoAuth(url: string, body: any) { var headers = new Headers(); headers.append('Content-Type', 'application/json'); let options = new RequestOptions({ headers: headers }); return this.http.post(url, body, options).toPromise() .then(res => res.json()) .catch(err => { this.handleError(err); }); } // public httpPostWithAuth(body: any, url: string) { // return this.myInfoLocal = this.local.getJson('UserInfo') // .then((result) => { // var headers = new Headers(); // headers.append('Content-Type', 'application/json'); // headers.append('Authorization', result.ID + '-' + result.UserToken); // let options = new RequestOptions({ headers: headers }); // return this.http.post(url, body, options).toPromise(); // }); // } private handleError(error: Response) { console.log(error); return Observable.throw(error.json().error || 'Server Error'); } }
这里需要说明一下:
本例中公开API是可以随意调用的,但UserInfo API是受保护的,即只有认证过的用户才能调用。
认证思路是注册时,就为每个用户分配一个token,登陆时,拿下来存储在本地,调用受保护API时,把这个token一并发送给服务端验证,这样做的好处是即使这个token泄露了,作为运营商很容易查出来,重新为他生一个即可。
9. 抽离一个数据访问
这里可以实现类似数据访问的功能,调用httpservice实现数据访问:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { HttpService } from "./HttpService"; import { StorageService } from "./StorageService"; @Injectable() export class UserInfoService { API_URL = "http://localhost:1856/api"; constructor( private http: Http, private httpService: HttpService, private storageService:StorageService) { } login(user) { var url = this.API_URL + "/UserLogin"; return this.httpService.httpPostNoAuth(url, user); } GetUserInfo(id:number) { var url = this.API_URL + "/UserInfo/"+id; return this.httpService.httpGetWithAuth(url); } }
10. 然后实现登陆功能
import { Component } from '@angular/core'; import { NavController, ToastController } from 'ionic-angular'; import { FormBuilder, Validators } from '@angular/forms'; import 'rxjs/add/operator/toPromise'; import { UserInfoService } from "./../../providers/UserInfoService"; import { StorageService } from "./../../providers/StorageService"; import { UserInfoData } from "./../../model/UserInfoData"; import { emailValidator } from './../../providers/validator' import { MyinfoPage } from '../myinfo/myinfo'; @Component({ selector: 'page-login', templateUrl: 'login.html', providers: [UserInfoService] }) export class LoginPage { local: Storage; constructor( public navCtrl: NavController, private formBuilder: FormBuilder, public toastCtrl: ToastController, private userInfoService: UserInfoService, private storageService: StorageService) { } loginForm = this.formBuilder.group({ //'LoginID': ['admin@163.com', [Validators.required, Validators.pattern('^([a-zA-Z0-9_.]*)((@[a-zA-Z0-9_.]*)\.([a-zA-Z]{2}|[a-zA-Z]{3}))$')]],// 第一个参数是默认值 'LoginID': ['admin@163.com2', [Validators.required, Validators.minLength(4), emailValidator]],// 第一个参数是默认值 'LoginPwd': ['123456', [Validators.required, Validators.minLength(4)]] }); ionViewDidLoad() { console.log('Hello Login Page'); } login(user, _event) { _event.preventDefault();//该方法将通知 Web 浏览器不要执行与事件关联的默认动作 this.userInfoService.login(user).then(data => { alert(JSON.stringify(data)); if (data.Result.ID > 0)//登录成功 { this.storageService.write('UserInfo', data.Result); //测试写缓存 //let ss = this.storageService.read<UserInfoData>('UserInfo'); //console.log(ss.UserToken); //传参 this.navCtrl.push(MyinfoPage, { item: data.Result.ID }); } else { let toast = this.toastCtrl.create({ message: '用户名或密码错误.', duration: 3000, position: 'middle', showCloseButton: true, closeButtonText: '关闭' }); toast.present(); } }); } }
这里举例说明了验证的几种做法。登陆成功存储用户信息,失败则弹出吐司提示,然后跳转到详情页,并且传一个参数过去。
11. 详情页
export class MyinfoPage { id: number;// 用来接收上一个页面传递过来的参数 user: UserInfoData; constructor(public navCtrl: NavController, navParams: NavParams, private userInfoService: UserInfoService, public actionSheetCtrl: ActionSheetController, private popoverCtrl: PopoverController ) { this.id = navParams.get('item');//这个是通过页面跳转传过来的值 this.getInfo(); } getInfo() { this.userInfoService.GetUserInfo(this.id).then(data => { this.user = data.Result; //alert(JSON.stringify(this.user)); }); }
接收参数用于查询,然后界面显示出来。
暂时结束吧,这些所有框架变化都太快,永远也不会有稳定的那一天,所以暂时不想研究了。
开源地址:
https://git.oschina.net/shiyeping/Ionic-Client-
https://git.oschina.net/shiyeping/Ionic-Server