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)
  1. angular.module('starter', ['ionic', 'starter.services','starter.controllers',
  2. 'ngStorage','ngMessages','validation.match'])
  3. .config(function($stateProvider, $urlRouterProvider){
  4. $stateProvider
  5. .state('signin',{
  6. url:'/signin',
  7. templateUrl: 'templates/signin.html',
  8. controller: 'signinCtrl'
  9. })
  10. .state('login',{
  11. url:'/login',
  12. templateUrl : 'templates/login.html',
  13. controller : 'loginCtrl'
  14. })
  15. .state('activities',{
  16. url: '/activities',
  17. templateUrl : 'templates/activities.html',
  18. controller : 'activityCtrl'
  19. })
  20. $urlRouterProvider.otherwise('/login');
  21. })
  22. .run(function ($ionicPlatform) {
  23. $ionicPlatform.ready(function () {
  24. if (window.cordova && window.cordova.plugins.Keyboard) {
  25. cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
  26. }
  27. if (window.StatusBar) {
  28. StatusBar.styleDefault();
  29. }
  30. });
  31. })

  • 三個頁面,登入(login),註冊(signin)與活動(activities):
圖3.三個頁面執行情況
    • 第1-2行:注入前述安裝的模組,還有自訂controllers, services。
    js/services.js為Parse雲端資料庫介接程式碼:
    1. angular.module('starter.services', [])
    2. .factory('DBA', function ($http, $q) {
    3. var self = this;
    4. self.query = function (method, api, data, header) {
    5. var q = $q.defer();
    6. switch (method) {
    7. case 'GET':
    8. $http.get(api, header).then(function (result) {
    9. q.resolve(result);
    10. }, function (error) {
    11. console.warn(error);
    12. q.reject(error);
    13. })
    14. break;
    15. case 'POST':
    16. $http.post(api, data, header).then(function (result) {
    17. q.resolve(result);
    18. }, function (error) {
    19. console.warn(error);
    20. q.reject(error);
    21. })
    22. break;
    23. case 'PUT':
    24. $http.put(api, data, header).then(function (result) {
    25. q.resolve(result);
    26. }, function (error) {
    27. console.warn(error);
    28. q.reject(error);
    29. })
    30. break;
    31. case 'DELETE':
    32. $http.delete(api, header).then(function (result) {
    33. q.resolve(result);
    34. }, function (error) {
    35. console.warn(error);
    36. q.reject(error);
    37. })
    38. break;
    39. }
    40. return q.promise;
    41. }
    42. return self
    43. })
    44. .factory('Enroll', function (DBA, PARSE_KEYS, PARSE_API, LOGIN_API, SIGNIN_API, LOGOUT_API) {
    45. var self = this;
    46.  
    47. var header = {headers: {
    48. 'X-Parse-Application-Id': PARSE_KEYS.APP_ID,
    49. 'X-Parse-REST-API-Key': PARSE_KEYS.REST_API_KEY
    50. }
    51. }
    52. var headerJson = {
    53. headers: {
    54. 'X-Parse-Application-Id': PARSE_KEYS.APP_ID,
    55. 'X-Parse-REST-API-Key': PARSE_KEYS.REST_API_KEY,
    56. 'Content-Type': 'application/json'
    57. }
    58. }
    59. self.signin = function (object) {
    60. return DBA.query('POST', SIGNIN_API, object, headerJson);
    61. }
    62. self.login = function (object) {
    63. return DBA.query(
    64. 'GET',
    65. LOGIN_API + "?" + encodeURI('username=' + object.username + '&password=' + object.password),
    66. '',
    67. header);
    68. }
    69. self.logout = function(sessionToken) {
    70. header.headers['X-Parse-Session-Token']=sessionToken;
    71. return DBA.query('POST',LOGOUT_API,'',header);
    72. }
    73. self.getAll = function (classname) {
    74. return DBA.query('GET', PARSE_API + classname, '', header);
    75. }
    76. self.get = function (classname, objectId) {
    77. return DBA.query('GET', PARSE_API + classname + '/' + objectId, '', header);
    78. };
    79. self.getWithChild = function (classname, child) {
    80. return DBA.query('GET', PARSE_API + classname +
    81. "?" + encodeURI('include=' + child), '', header);
    82. }
    83. self.query = function (classname, object) {
    84. return DBA.query('GET', PARSE_API + classname, '', header);
    85. };
    86. self.create = function (classname, object) {
    87. return DBA.query('POST', PARSE_API + classname, object, headerJson);
    88. }
    89. self.update = function (classname, objectId, object) {
    90. return DBA.query('PUT', PARSE_API + classname + '/' + objectId, object, headerJson);
    91. }
    92. self.delete = function (classname, objectId) {
    93. return DBA.query('DELETE', PARSE_API + classname + '/' + objectId, '', headerJson);
    94. }
    95. return self;
    96. })
    97. .value('PARSE_KEYS', {
    98. APP_ID: '2Lr9N9wK...略...',
    99. REST_API_KEY: 'n0kVeRn...略...'
    100. })
    101. .value('PARSE_API', "https://api.parse.com/1/classes/")
    102. .value('LOGIN_API', "https://api.parse.com/1/login")
    103. .value('SIGNIN_API', "https://api.parse.com/1/users")
    104. .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檔內容如下:
    1. angular.module('starter.controllers', [])
    2. .controller('loginCtrl', function ($scope, Enroll, $location, $localStorage) {
    3. $scope.username = '';
    4. $scope.password = ''
    5. $scope.logon = function (username, password) {
    6. var object = {'username': username, 'password': password};
    7. Enroll.login(object).then(function (result) {
    8. $localStorage.token = result.data.sessionToken;
    9. $location.path('/activities');
    10. }, function (error) {
    11. console.warn(error);
    12. $location.path('/login');
    13. })
    14. }
    15. $scope.signin = function () {
    16. $location.path('/signin');
    17. }
    18. })
    19. .controller('activityCtrl', function ($scope, Enroll, $location, $localStorage,$window) {
    20. if ($localStorage.hasOwnProperty("token") === true) { // 已登入
    21. $scope.login = true; // 顯示登出按鈕
    22. Enroll.getWithChild('Activity', 'instructor').then(function (result) {
    23. $scope.activities = result.data.results;
    24. })
    25. } else {
    26. $scope.login = false;
    27. $location.path('/login');
    28. }
    29. $scope.logout = function () {
    30. Enroll.logout($localStorage.token).then(function (result) {
    31. delete $localStorage.token;
    32. $window.location.reload('true');
    33. $window.location.href= '#login';
    34. }, function (error){
    35. console.warn(error);
    36. })
    37. }
    38. })
    39. .controller('signinCtrl', function ($scope, Enroll, $location, $window, $localStorage) {
    40. $scope.user = {};
    41. $scope.password02 = ''; // confirmpassword
    42. $scope.signin = function (user) {
    43. Enroll.signin(user).then(function (result) {
    44. $localStorage.token = result.data.sessionToken;
    45. $location.path('/activities');
    46. }, function (error) {
    47. console.warn(error.data.error);
    48. })
    49. }
    50. })

    • 第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,請自行參考。

    沒有留言:

    張貼留言