2015年5月4日 星期一

Hybrid Apps開發系列之5.5:以REST API存取Parse雲端資料庫

[Parse.com服務已終止]SQLite是手機內建的資料庫,僅適合用來儲存local data。「資料庫伺服器」或是「網路資料庫服務」對許多apps而言,才是儲存資料的正解。因此,本文以Parse雲端資料庫服務為例,使用Parse REST API存取雲端資料庫。

前置作業(1/4):在parse.com建立App

透過API使用Parse雲端資料庫必須先在Parse.com註冊、建立App、並取得App Keys才能順利使用。Parse的帳號啟用之後,點選"Create a new App"建立App,切換到Dashboard,便可看到已經建立的所有Apps,如下圖:
圖1.Parse.com提供雲端資料庫服務

此處以圖1的"todo" App為例,點選todo App,再切換到Core,從該介面便可為App建立雲端資料庫。

前置作業(2/4):建立 Parse雲端資料庫

Parse雲端資料庫是NoSQL,而非關聯式資料庫,其資料庫是由classes組成,每個class各有自己的屬性(下圖+Col可增加屬性),介面如下圖所示:
圖2. Parse雲端資料庫介面
圖2左上角顯示todo App建立了一個Todo class,內有三筆資料。Todo class除了預設的(objectId, createdAt, updatedAt, ACL) 四個屬性之外,另外新增了title, message兩個String類別的屬性,因此共有6個屬性。之後,Ionic專案建立的app將透過Parse REST API存取此資料集。

前置作業(3/4):瞭解Parse REST API格式

下圖是存取物件的API:
圖3. Parse REST API (https://parse.com/docs/rest)
所有功能都以 "https://api.parse.com"開頭,再串接上圖的URL。不過,不同動作使用的HTTP方法不盡相同,從POST, GET, 到DELETE都有(如HTTP Verb一欄),而且在http header部分還要另加一些項目才行,以Creating Objects為例,官網文件以curl格式說明此動作需要在header加上三個項目X-Parse-Application-Id,X-Parse-REST-API-Key,以及Content-Type:
curl -X POST \
  -H "X-Parse-Application-Id: 你的Application Id" \
  -H "X-Parse-REST-API-Key: 你的REST API Key" \
  -H "Content-Type: application/json" \
  -d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
  https://api.parse.com/1/classes/GameScore
因此,將上述curl改為AngularJS的寫法,則必須以$http呼叫REST API,同時設定額外的header項目才行:
  1. var data={"score":1337,"playerName":"Sean Plott","cheatMode":false};
  2. ... [略] ...
  3. $scope.getAll = function ($http) {
  4. $http.post('https://api.parse.com/1/classes/GameScore', data, {
  5. headers: {'X-Parse-Application-Id': "你的Application ID",
  6. 'X-Parse-REST-API-Key': "你的REST-API-Key",
  7. 'Content-Type': 'application/json'
  8. }
  9. });
  10. }
其他API的curl範例請參考https://parse.com/docs/rest

前置作業(4/4):將Application Keys加入Ionic專案中

從Dashboard todo專案處選取Keys,會進入Application Keys畫面。以REST API連線而言,需要用到Application ID與REST API Key。此兩個字串在每一次呼叫$http服務時都必須使用,如前置作業(3/4)的程式碼片段第5-6行所示。如圖4將此兩個字串複製起來,加到Ionic專案中。
圖4. Application Keys
由於此兩個Keys用在許多地方,故可加入js/services,js,並以.value()定義為常數,程式碼如下:
angular.module('starter.services', [])
    .factory(...[略]...
    ...[略]...
    .value('PARSE_KEYS', {
       APP_ID: '7h9gDEasc63I84aBqO6iVn.....',
       REST_API_KEY: 'luGnIWHUb9FS53uvA......'
    })
之後變可以PARSE_KEYS.APP_ID與PARSE_KEYS.REST_API_KEY的方式,使用這個key值。

修改記事App

Hybrid Apps開發系列之5.3:使用Cordova外掛存取SQLite資料庫一文曾以SQLite做為記事App的資料庫,現在可改以前置作業(2/4)建立的雲端資料庫儲存資料。

js/app.js檔因不需開啟資料庫的緣故,變得只需要設定states即可:
angular.module('starter', ['ionic','starter.controllers','starter.services'])
.run(function ($ionicPlatform) {
$ionicPlatform.ready(function () {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if (window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if (window.StatusBar) {
StatusBar.styleDefault();
}
});
})
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'templates/home.html',
controller: 'homeCtrl'
})
.state('edit', {
url: '/home/:id',
templateUrl: 'templates/edit.html',
controller: 'editCtrl'
})
})
view raw app.js hosted with ❤ by GitHub
Parse雲端資料庫的部份實作於js/services.js,同樣將共通的部份寫成DBA服務,Todo類別的存取則另外以.factory()建立todoParse服務:
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('todoParse', function (DBA, PARSE_KEYS, PARSE_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.getAll = function () {
return DBA.query('GET', PARSE_API, '', header);
}
self.get = function (objectId) {
return DBA.query('GET', PARSE_API + '/' + objectId, '', header);
};
self.create = function (object) {
return DBA.query('POST', PARSE_API, object, headerJson);
}
self.update = function (objectId, object) {
return DBA.query('PUT', PARSE_API + '/' + objectId, object, headerJson);
}
self.delete = function (objectId) {
return DBA.query('DELETE', PARSE_API + '/' + objectId, '', headerJson);
}
return self;
})
.value('PARSE_KEYS', {
APP_ID: '7h9gDEasc6...[略]...',
REST_API_KEY: 'luGnIWHUb9...[略]...'
})
.value('PARSE_API', "https://api.parse.com/1/classes/Todo");
view raw services.js hosted with ❤ by GitHub

  • 第2-43行:DBA服務。如圖3不同的物件存取功能,會用到不同的http傳送方法,因此以switch方式決定使用的方法,以及所需參數。
  • 第7-14行:HTTP GET,取出物件或進行查詢時使用。
  • 第15-22行:HTTP POST,建立物件時使用,因此參數多了data—亦即要寫入雲端的資料內容。
  • 第23-30行:HTTP PUT,更新物件內容時使用。
  • 第31-38行:HTTP DELETE,刪除物件時使用。
  • 關於第5行$q.defer()與回傳值q.promise等與非同步執行有關的說明,請參見Hybrid Apps開發系列之5.3:使用Cordova外掛存取SQLite資料庫一文。
  • 第44-75行:Todo class資料存取服務。資料存取包含:建立、讀取、更新、刪除等CRUD資料存取基本功能,分別對應到getAll, get(objectId), create(object), update(object), delete(objectId)等函式。
  • 第75-79行:Parse API Key常數。請記得改為自己的App id。
  • 第80行:Parse API基本網址常數,隨著不同存取功能,可能需要在此常數之後加上objectId。
頁面部份原本與5.3範例大致相同,差別有:

  1. home.html各筆資料的超連結設定改用雲端資料庫的objectId欄位值。
  2. edit.html, addNote.html要做表單驗證,確認各個欄位確實有值,因此加入驗證程式碼。
templates/home.html
<ion-view title="記事本">
<ion-nav-buttons side="right">
<button class="button" ng-click="addNote()">新增</button>
</ion-nav-buttons>
<ion-content>
<ion-list class="list list-inset" ng-repeat="item in notes" >
<ion-item class="item item-button-right" ng-href="#/home/{{item.objectId}}">
<p>{{item.title}}</p>
<span>{{item.message}}</span>
<button class="button button-clear">
<i class="icon ion-compose"></i>
</button>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
view raw home.html hosted with ❤ by GitHub

templates/edit.html
<ion-view title="編輯記事">
<ion-nav-buttons side="left">
<button class="button" ng-click="go('#home')">取消返回</button>
</ion-nav-buttons>
<ion-nav-buttons side="right">
<button class="button" ng-click="update(note)"
ng-disabled="editForm.title.$invalid || editForm.message.$invalid">
修改
</button>
<button class="button" ng-click="del(note)">刪除</button>
</ion-nav-buttons>
<ion-content>
<form name="editForm" novalidate>
<label class="item item-input">
<span class="input-label">標題</span>
<input type="text" ng-model="note.title" name="title" required>
</label>
<label class="item">
<span class="item-note" ng-show="editForm.title.$error.required">標題不得為空</span>
</label>
<label class="item item-input">
<textarea placeholder="輸入記事內容" ng-model="note.message" name="message" required></textarea>
</label>
<label class="item">
<span class="item-note" ng-show="editForm.message.$error.required">內容不得為空</span>
</label>
</form>
</ion-content>
</ion-view>
view raw edit.html hosted with ❤ by GitHub

templates/addNote.html
<ion-modal-view>
<ion-header-bar class="bar-energized">
<h1 class="title">新增記事</h1>
<button class="button" ng-click="closeModal()">返回</button>
</ion-header-bar>
<ion-content>
<form name="addForm" novaliate>
<label class="item item-input">
<span class="input-label">標題</span>
<input type="text" placeholder="輸入標題" ng-model="note.title" name="title" required>
</label>
<label class="item">
<span class="item-note" ng-show="addForm.title.$error.required">標題不得為空</span>
</label>
<label class="item item-input">
<textarea placeholder="輸入記事內容" ng-model="note.message"
name="message" required></textarea>
</label>
<label class="item">
<span class="item-note" ng-show="addForm.message.$error.required">內容不得為空</span>
</label>
<button class="button button-block" ng-click="create(note)"
ng-disabled="addForm.title.$invalid
|| addForm.message.$invalid">新增</button>
</form>
</ion-content>
</ion-modal-view>
view raw addNote.html hosted with ❤ by GitHub

最後js/controllers.js如下:
angular.module('starter.controllers', [])
.controller('homeCtrl', function ($scope, todoParse, $ionicModal) {
$scope.updateNotes = function () {
todoParse.getAll().then(function (result) {
$scope.notes = result.data.results;
});
}
$scope.updateNotes(); // 讀取資料
$scope.addNote = function () { // 開啟modal視窗,填寫資料存入資料庫
$scope.note = {}; // 新增記事使用
$scope.openModal(); //開啟視窗
};
$scope.create = function (note) { // 新增記事
todoParse.create(note).then(function (success) {
$scope.closeModal();
$scope.updateNotes(); // 更新資料
});
}
$ionicModal.fromTemplateUrl('templates/addNote.html', {// modal視窗定義
scope: $scope,
animation: 'silde-in-up'
}).then(function (modal) {
$scope.modal = modal;
})
$scope.openModal = function () {
$scope.modal.show();
}
$scope.closeModal = function () {
$scope.modal.hide();
}
$scope.$on('$destroy', function () {
$scope.modal.remove();
})
})
.controller('editCtrl', function ($scope, todoParse, $stateParams, $location, $window) {
todoParse.get($stateParams.id).then(function (result) {
$scope.note = result.data;
}, function (error) {
console.warn(error);
})
$scope.update = function (note) {
todoParse.update($stateParams.id, note).then(function (success) {
$window.location.reload(true);
$window.location.href = '#home';
});
}
$scope.del = function () {
var promise = todoParse.delete($stateParams.id).then(function (success) {
$window.location.reload(true);
$window.location.href = '#home';
});
}
$scope.go = function (path) {
$location.path(path);
}
})
view raw controllers.js hosted with ❤ by GitHub


沒有留言:

張貼留言