2015年5月13日 星期三

Hybrid Apps開發系列之6.3:使用Parse User實作雲端會員管理機制

[Parse.com服務已終止]延續前文Hybrid Apps開發系列之5.5:以REST API存取Parse雲端資料庫 ,本文繼續介紹Parse提供的會員管理機制:Parse User。透過此Parse內建的class,註冊、登入等會員基本管理,都非常簡單。

Parse User是Parse App內建的class,只要建立app,就會自動產生。Parse User class預設有9個欄位如下圖所示,視需求可自行擴充。
圖1. Parse app自動產生User class,內有username, password, email等欄位
以下以EnrollApp的雲端資料庫(如下圖)為例,說明如何以Parse REST API進行會員註冊與登入的動作。
圖2. EnrollApp雲端資料庫

如圖2所示,Role, Session, User都是預設classes,可用來作為會員管理。另外Activity與Instructor則是自建classes,用來記錄活動與活動講師。會員註冊、登入與登出部分將會用到User class(欄位見圖1)。

Parse REST API: 註冊與登入
Parse REST API的基本網址是:
    https://api.parse.com/
跟註冊、登入與登出有關的API如下圖所示:
圖3. Parse REST API: Users部分功能
以註冊為例,官網上curl語法是:
curl -X POST \
  -H "X-Parse-Application-Id: 你的applicaiton id" \
  -H "X-Parse-REST-API-Key: 你的API Key" \
  -H "Content-Type: application/json" \
  -d '{"username":"cooldude6","password":"p_n7!-e8","phone":"415-392-0202"}' \
  https://api.parse.com/1/users
將上述curl改為AngularJS的寫法,則是:
var user={"username":"cooldude6","password":"p_n7!-e8","phone":"415-392-0202"};
... [略] ...
$scope.signin = function ($http) {
    $http.post('https://api.parse.com/1/classes/users', user, {
        headers: {'X-Parse-Application-Id': "你的Application ID",
                  'X-Parse-REST-API-Key': "你的REST-API-Key",
                  'Content-Type': 'application/json'
        }
    });
}
其實註冊動作與一般新增一筆資料的做法(參考Hybrid Apps開發系列之5.5:以REST API存取Parse雲端資料庫)雷同,差別只有API不同,因此實作上,可以直接運用之前範例的DBA服務。

建立Ionic專案

ionic start ParseUser blank
cd ParseUser
bower install ngstorage
bower install angular-messages
bower install angular-validation-match
ionic platform add android
此範例用到ngStorage儲存token,ngMessages, Angular Validation: Match進行表單驗證與密碼欄位內容比對。安裝好上述模組後,在bower_components資料夾內找到這些模組的.js檔,複製到專案內的lib資料夾,接著再修改index.html,例如:
    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>
    <script src="lib/validate/angular-input-match.min.js"></script>
   <script src="lib/angular-messages/angular-messages.js"></script>
   <script src="lib/ngstorage/ngStorage.min.js"></script>
修改app.js,注入相對dependencies:
angular.module('starter', ['ionic', 'starter.services','starter.controllers',
    'ngStorage','ngMessages','validation.match'])
 ...[略]...

程式碼解說

頁面states定義如下 (js/app.js)
angular.module('starter', ['ionic', 'starter.services','starter.controllers',
    'ngStorage','ngMessages','validation.match'])
        .config(function($stateProvider, $urlRouterProvider){
            $stateProvider
            .state('signin',{
                url:'/signin',
                templateUrl: 'templates/signin.html',
                controller: 'signinCtrl'
            })
            .state('login',{
                url:'/login',
                templateUrl : 'templates/login.html',
                controller : 'loginCtrl'
            })
            .state('activities',{
                url: '/activities',
                templateUrl : 'templates/activities.html',
                controller : 'activityCtrl'
            })
            $urlRouterProvider.otherwise('/login');
        })
.run(function ($ionicPlatform) {
    $ionicPlatform.ready(function () {
        if (window.cordova && window.cordova.plugins.Keyboard) {
            cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
        }
        if (window.StatusBar) {
            StatusBar.styleDefault();
        }
    });
})

  • 三個頁面,登入(login),註冊(signin)與活動(activities):
圖3.三個頁面執行情況
    • 第1-2行:注入前述安裝的模組,還有自訂controllers, services。
    js/services.js為Parse雲端資料庫介接程式碼:
    angular.module('starter.services', [])
            .factory('DBA', function ($http, $q) {
                var self = this;
                self.query = function (method, api, data, header) {
                    var q = $q.defer();
                    switch (method) {
                        case 'GET':
                            $http.get(api, header).then(function (result) {
                                q.resolve(result);
                            }, function (error) {
                                console.warn(error);
                                q.reject(error);
                            })
                            break;
                        case 'POST':
                            $http.post(api, data, header).then(function (result) {
                                q.resolve(result);
                            }, function (error) {
                                console.warn(error);
                                q.reject(error);
                            })
                            break;
                        case 'PUT':
                            $http.put(api, data, header).then(function (result) {
                                q.resolve(result);
                            }, function (error) {
                                console.warn(error);
                                q.reject(error);
                            })
                            break;
                        case 'DELETE':
                            $http.delete(api, header).then(function (result) {
                                q.resolve(result);
                            }, function (error) {
                                console.warn(error);
                                q.reject(error);
                            })
                            break;
                    }
                    return q.promise;
                }
                return self
            })
            .factory('Enroll', function (DBA, PARSE_KEYS, PARSE_API, LOGIN_API, SIGNIN_API, LOGOUT_API) {
                var self = this;
    
                var header = {headers: {
                        'X-Parse-Application-Id': PARSE_KEYS.APP_ID,
                        'X-Parse-REST-API-Key': PARSE_KEYS.REST_API_KEY
                    }
                }
                var headerJson = {
                    headers: {
                        'X-Parse-Application-Id': PARSE_KEYS.APP_ID,
                        'X-Parse-REST-API-Key': PARSE_KEYS.REST_API_KEY,
                        'Content-Type': 'application/json'
                    }
                }
                self.signin = function (object) {
                    return DBA.query('POST', SIGNIN_API, object, headerJson);
                }
                self.login = function (object) {
                    return DBA.query(
                            'GET',
                            LOGIN_API + "?" + encodeURI('username=' + object.username + '&password=' + object.password),
                            '',
                            header);
                }
                self.logout = function(sessionToken) {
                    header.headers['X-Parse-Session-Token']=sessionToken;
                    return DBA.query('POST',LOGOUT_API,'',header);
                }
                self.getAll = function (classname) {
                    return DBA.query('GET', PARSE_API + classname, '', header);
                }
                self.get = function (classname, objectId) {
                    return DBA.query('GET', PARSE_API + classname + '/' + objectId, '', header);
                };
                self.getWithChild = function (classname, child) {
                    return DBA.query('GET', PARSE_API + classname +
                            "?" + encodeURI('include=' + child), '', header);
                }
                self.query = function (classname, object) {
                    return DBA.query('GET', PARSE_API + classname, '', header);
                };
                self.create = function (classname, object) {
                    return DBA.query('POST', PARSE_API + classname, object, headerJson);
                }
                self.update = function (classname, objectId, object) {
                    return DBA.query('PUT', PARSE_API + classname + '/' + objectId, object, headerJson);
                }
                self.delete = function (classname, objectId) {
                    return DBA.query('DELETE', PARSE_API + classname + '/' + objectId, '', headerJson);
                }
                return self;
            })
            .value('PARSE_KEYS', {
                APP_ID: '2Lr9N9wK...略...',
                REST_API_KEY: 'n0kVeRn...略...'
            })
            .value('PARSE_API', "https://api.parse.com/1/classes/")
            .value('LOGIN_API', "https://api.parse.com/1/login")
            .value('SIGNIN_API', "https://api.parse.com/1/users")
            .value('LOGOUT_API',"https://api.parse.com/1/logout");
    

    • 第2-43行:DBA服務與Hybrid Apps開發系列之5.5:以REST API存取Parse雲端資料庫完全相同。
    • 第59-61:使用Parse Rest API進行註冊,此處object包含帳號、密碼、與email。在輸入帳號、密碼、email的註冊表單必須做輸入驗證,驗證條件可自訂,但email必須符合格式,否則會註冊失敗。另外注意email部分可在Parse App處設定要做email驗證,如此系統便會自動寄信。詳請請參考Parse官網。
    • 第62-68行:登入。同樣需提供帳號、密碼。
    • 第69-72行:登出。需提供登入或註冊時拿到的sessionToken。登出成功會將Parse Session內的資料移除。
    • 第79-82行:此函式專為取得Parse Pointer的物件內容。由於活動Activities類別裡有一欄位instructor指向另一個Instructor物件,因此需用此函式才能取得Instructor物件內容。(註:Pointer並不是關聯式資料庫的東西,這是Parse雲端資料庫用來實作多對多關連的方法,詳情請參考Parse官網Relational Queries的說明)
    js/controllers.js檔內容如下:
    angular.module('starter.controllers', [])
            .controller('loginCtrl', function ($scope, Enroll, $location, $localStorage) {
                $scope.username = '';
                $scope.password = ''
                $scope.logon = function (username, password) {
                    var object = {'username': username, 'password': password};
                    Enroll.login(object).then(function (result) {
                        $localStorage.token = result.data.sessionToken;
                        $location.path('/activities');
                    }, function (error) {
                        console.warn(error);
                        $location.path('/login');
                    })
                }
                $scope.signin = function () {
                    $location.path('/signin');
                }
            })
            .controller('activityCtrl', function ($scope, Enroll, $location, $localStorage,$window) {
                if ($localStorage.hasOwnProperty("token") === true) { // 已登入
                    $scope.login = true; // 顯示登出按鈕
                    Enroll.getWithChild('Activity', 'instructor').then(function (result) {
                        $scope.activities = result.data.results;
                    })
                } else {
                    $scope.login = false;
                    $location.path('/login');
                }
                $scope.logout = function () {
                    Enroll.logout($localStorage.token).then(function (result) {
                        delete $localStorage.token;
                        $window.location.reload('true');
                        $window.location.href= '#login';                   
                    }, function (error){
                        console.warn(error);
                    })
                }
            })
            .controller('signinCtrl', function ($scope, Enroll, $location, $window, $localStorage) {
                $scope.user = {};
                $scope.password02 = ''; // confirmpassword
                $scope.signin = function (user) {
                    Enroll.signin(user).then(function (result) {
                        $localStorage.token = result.data.sessionToken;
                        $location.path('/activities');
                    }, function (error) {
                        console.warn(error.data.error);
                    })
                }
            })

    • 第5-14行:登入函式logon()。第7行使用了services.js定義的login()進行雲端資料庫登入。登入成功的話,雲端資料庫Session class會多一筆token紀錄,並回傳到controller。此處將sessionToken存到$localStorage.token內,隨之轉往activities頁面。

    圖4. Activity類別內有Pointer類型的欄位instructor

    • 第19-38行:activities頁面之controller。如已成功登入,便會顯示登出按鈕,同時取得Activity類別內所有活動記錄。第22行以getWithChild('Activity', 'instructor')指定除了取得Activity所有物件,同時對Pointer欄位instructor,也要進一步取得資料。如圖4所示,Activity有一instructor欄位為Pointer,儲存objectId,但實際內容則在Instuctor類別內。因此要靠getWithChild('Activity', 'instructor')才能取得實際資料。
    • 第39-50行:註冊頁面controller。呼叫Enroll.signin()進行註冊。成功同樣回傳seesionToken。

    其他templates/login.html, templates/signin.html, 內使用了ngMessages指令進行表單驗證,可參考ngMessages。此兩個html檔,index.html,templates/activities.html與其他.js檔完整內容放置於Gist,請自行參考。

    沒有留言:

    張貼留言