2018年10月11日 星期四

【Ionic 4】會員Email註冊-以Firebase為雲端平台(含FormBuilder表單與輸入驗證)

Firebase支援「Email/密碼登入與註冊」以及多種OAuth登入方式,如Google,Twitter,Facebook等,本文說明如何使用Firebase「Email/密碼」認證API,設計會員Email註冊功能。除此之外,註冊表單則以Reactive Forms(FormBuilder)設計,並帶有輸入驗證功能,如下圖所示。
有驗證功能的註冊表單。註冊與登出都是使用Firebase認證API
此App有註冊、HomePage兩個頁面。首頁設定為註冊頁面,註冊後,會在Cloud Firestore新增會員資料,並轉往HomePage;在HomePage按下登出又會回註冊頁面。完整程式碼在Github。製作重要步驟如下:

Step 1:建立專案
除了前述註冊與HomePage兩個頁面,須另建立登入服務AuthService,處理Firebase Email/密碼註冊功能,以及登出:
ionic start RegisterExample blank --type=angular
cd RegisterExample
ionic g page register
ionic g service services/auth
而為了使用Firebase認證API,必須安裝firebase與@angular/fire兩個套件:
npm install firebase @angular/fire
Step 1.1:加入Firebase連線設定
Firebase控制台建立專案後,進入專案資料庫,建立Cloud Firestore資料庫:
Cloud Firestore是Firebase新版的NoSQL文件資料庫
接著先「以測試模式啟動」,方便後續測試。不過需注意畫面提醒訊息:測試模式,所有使用者都有存取權限。後續務必要自行設定安全性規則。
以測試模式啟動Cloud Firestore,方便測試
緊接著,在[Project Overview] 下,新增應用程式,點取「網路應用程式」符號(如下圖),以便取得Ionic App連線時所需參數設定值:
選取網路應用程式
Ionic App所需連線參數
如上圖,複製反白區域的程式碼。
修改src/environments/environment.ts檔,將上述程式碼貼入其中,如下:
export const environment = {
production: false,
firebaseConfig: {
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: '專案id.firebaseapp.com',
databaseURL: 'https://專案id.firebaseio.com',
projectId: '專案id',
storageBucket: '專案id.appspot.com',
messagingSenderId: 'XXXXXXXXXXXX'
}
};
view raw environment.ts hosted with ❤ by GitHub
最後,修改app.module.ts,引入AngularFireModule模組與environment元件,同時設定AngularFireModule.initializeApp(),帶入Firebase連線所需參數:
//...
import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
//...
@NgModule({
//...
imports: [
AngularFireModule.initializeApp(environment.firebaseConfig),
// ...
],
// ...
})
export class AppModule {}
view raw app.module.ts hosted with ❤ by GitHub
Step 1.2:修改app-routing.module.ts
為了展示方便,將首頁改至"register",直接進入RegisterPage,如下之第3行:
// ...
const routes: Routes = [
{ path: '', redirectTo: 'register', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{ path: 'register', loadChildren: './register/register.module#RegisterPageModule' },
];
// ...

Step 2: 編修註冊頁面
Step 2.1: 修改register.module.ts,引入ReactiveFormsModule
但由於Ionic 4 CLI建立頁面時,會產生lazy loading頁面,每個頁面有自己的模組檔;因此,import ReactiveFormsModule是加在RegisterPage的模組檔-register.module.ts檔:
import { ReactiveFormsModule } from '@angular/forms';
而不是加在最上層的app.module.ts。
Step 2.2:製作註冊表單
以FormGroup製作表單,使用Validators內建功能驗證欄位輸入,重點摘要如下:
//...
import { Validators, FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../services/auth.service';
//...
export class RegisterPage implements OnInit {
registerForm: any;
constructor(
private builder: FormBuilder,
private auth: AuthService
) { }
ngOnInit() {
this.buildForm();
}
buildForm() {
this.registerForm = this.builder.group({
email: ['',
[Validators.required, Validators.email]
],
displayName: ['',
[Validators.required, Validators.minLength(3), Validators.maxLength(32)]
],
passwordGroup: new FormGroup({
password: new FormControl('',
[Validators.required,
Validators.pattern('^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$'),
Validators.minLength(6),
Validators.maxLength(15)]
),
confirmPassword: new FormControl('',
[Validators.required]
)
}, { validators: PasswordValidator.MatchPassword }),
}
);
this.registerForm.valueChanges.subscribe(data => this.onValueChanged(data));
// reset messages
this.onValueChanged();
}
// Update validation messages of the form
private onValueChanged(data?: any) {
// ...
}
}

  • 第2行:引入FormBuilder等元件。
  • 第18行:FormBuilder提供group()功能,可設定表單欄位。
  • 第19-21行:每一欄位有其名稱,如19行之email;另外可訂定驗證規則。20行使用了必要欄位與email格式驗證(Validators.email)。22~24行則是訂定displayName欄位與其驗證規則。
  • 第25-35行:密碼欄位比較特殊,除了第28行.pattern(),用regular expression定義密碼格式外,還有password, confirmPassword兩個欄位的比對需求。因此,第23行改用new FormGroup製作子群組,將password, confirmPassword歸入同群組,並在第35行設定此群組的驗證規則MatchPassword(位於src/app/_validators/password.validator.ts)。
  • 第39行:訂閱valueChanges服務,監聽表單內容變動。一旦有所變動,則交由自訂函數onValueChange()進行判讀,並顯示錯誤訊息。
如上所述,需另外撰寫密碼比對是否相符的驗證函式MatchPassword,以及自訂內容變動判讀函式onValueChange()。
Step 2.3 加入密碼比對函式
密碼比對需使用@angular/forms的AbstractControl。透過AbstractControl定義的屬性,如value,可讓validator取得輸入欄位的值。新增src/app/_validators/password.validator.ts,並加入下列程式碼:
import { AbstractControl } from '@angular/forms';
export class PasswordValidator {
static MatchPassword(ac: AbstractControl) {
let password = ac.get('password').value;
let confirmPassword = ac.get('confirmPassword').value ;
if( password != confirmPassword){
return {matchPassword : true}; // 比對失敗
}
return null;
}
}
唯一要注意是:自訂validator當「比對失敗」時,要回傳true(如第8行),否則回傳null(第10行)。
Step 2.4 加入內容變動判讀onValueChange()
此處將所有輸入驗證的錯誤訊息定義於validatorMessages物件,另外formErrors則是列出各欄位與額外加上的validator,onValueChange()每次執行,都會依序檢視formErrors每一項目,看是否有錯誤訊息。部份程式碼如下:
formErrors = {
'email': '',
'displayName': '',
// ...
};
validatorMessages = {
//...
'password': {
'required': '必填欄位',
'pattern': '至少須包含一字母一數字',
'minlength': '長度至少為6',
'maxlength': '長度最多為15'
},
'confirmPassword': {
'required': '必填欄位',
},
'matchPassword': '密碼不相符'
}
//...
private onValueChanged(data?: any) {
if (!this.registerForm) { return; }
const form = this.registerForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
switch (field) {
case 'email':
case 'displayName':
//...
break;
case 'password':
case 'confirmPassword':
var group = form.get('passwordGroup');
var control = group.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validatorMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
break;
case 'matchPassword':
var group = form.get('passwordGroup');
if (group.get('password').dirty && group.get('confirmPassword').dirty
&& group.errors && group.errors.matchPassword) {
this.formErrors[field] = this.validatorMessages[field];
}
break;
}
}
}

  • 第35-40行:一般預設的驗證都是如35行以dirty, not valid皆成立,確認輸入驗證失敗,此時便要顯示錯誤訊息。
  • 第42-48行:密碼比對較為複雜,需透過上層的passwordGroup,才能取得其內的兩組密碼欄位,錯誤判斷規則如44-45所示。
Step 3 註冊連帶建立會員資料
services/auth.service.ts提供Firebase註冊服務,並在註冊時,一併建立會員資料。程式碼如下:

  • 第21行:訂閱authState,如此一旦有登入或登出時,便會取得第22行user值(登入時為user資料,登出時為null)。因此如有登入,this.user便會填入current user(如24行)。
  • 第32-38行:createUserWithEmailAndPassword()是Firebase註冊功能,會在Firebase後台建立一組email帳號(需6位以上)。但為了在Firestore資料庫產生相對應會員資料,35行呼叫自訂函式updateUserData()。
  • 第40-44行:登出。透過Router轉向首頁。
  • 第46-53行:userRef指向Cloud Firestore資料庫users集合內之文件;該文件的文件id:${user.uid}是在33行註冊時,系統產生的「使用者UID」,且已記錄於Firebase Authentication表格裡。如此使用者登入時,藉由UID便可取得users集合內對應到此user的文件。48-51設定表單傳送過來的user資料,52行將資料寫入Cloud Firestore。
import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore';
import { Observable, of } from 'rxjs';
import { User } from '../../_models/user';
import { switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthService {
user: Observable<User>;
constructor(
private afAuth: AngularFireAuth,
private db: AngularFirestore,
private router: Router
) {
this.user = this.afAuth.authState.pipe(
switchMap(user => {
if(user) {
return this.db.doc<User>(`users/${user.uid}`).valueChanges();
} else {
return of(null);
}
})
);
}
signUp(user){
return this.afAuth.auth.createUserWithEmailAndPassword(user.email,user.password)
.then(credential=>{
this.updateUserData(credential.user, user.displayName);
})
.catch(error=> console.log("註冊失敗:",error));
}
signOut(){
return this.afAuth.auth.signOut().then(()=>{
this.router.navigate(['/']);
});
}
private updateUserData(user, displayName){
const userRef: AngularFirestoreDocument<User> = this.db.doc(`users/${user.uid}`)
const data:User = {
email: user.email,
displayName: displayName
}
return userRef.set(data);
}
}
view raw auth.service.ts hosted with ❤ by GitHub

最後,為了方便寫入user資料,另外定義了_model/user.ts,裡面定義了users集合內,各文件應該有的欄位。專案完整程式碼在Github,惟在git clone後,需改寫environment.ts裡關於Firebase的連線設定。

使用版本
firebase 5.5.3
@angular/fire 5.0.2


沒有留言:

張貼留言