概要
少し前に、Androidアプリ開発のmBaaSとしてParseをつかい、バックエンドの処理をCloud Codeを使って書きました。その際に引っかかったことや、解決したこと、やっておけばよかったことなどを共有します。Parse自体の使い方や、Cloud Codeの基本的な開発方法についてはあまり書かずに、公式のドキュメントに載っていないことを中心に書いていきます。
Parseとサーバサイド
Parse上のデータを操作したり、プッシュ通知を送ったりする方法として、もともとREST APIや、iOSやAndroid用のSDKが用意されています。これらを利用することで、単純なデータのCRUD操作についてはデータベースにクエリを投げてデータを返す・・・といったような典型的なサーバサイドの実装が不要となり、クライアントサイド/アプリ開発に集中することができるというのが売りです(mBaaSってそういうものですよね)。
プロトタイプの開発ではAPIを複数組み合わせてやればそれだけでよいのですが、実際のアプリケーション開発では困ることがあります。具体的には入力値のバリデーションが必要であったり、特定の条件でのみデータの更新を行いたかったり、クライアントに直接Push通知を行わせたくないといったケースが考えられます。つまり、セキュリティ上クライアントサイドから呼ばれてほしくないときや、複数のデータ操作をひとまとめに行いたいときは、上記のクライアントサイドの処理だけでは実現できません。
そこで登場するのがCloud Codeです。
Cloud Code
Cloud Codeは、Parse上で動作するサーバサイドJavascript実行環境です。
Cloud CodeのAPIはexpressのミドルウェアを書く感じです。
// Cloud functionの定義
// https://appname.parse.com/functions/endpoint1でPOSTを受け付ける
Parse.Cloud.define('endpoint1', function(req, res) {
new Parse.Query('ClassA')
.get(req.params.id)
.then(function(obj) {
res.success(obj);
})
.fail(function(err) {
res.error(err);
});
});
// `className`へデータの挿入または更新が行われる前に実行されるフック
Parse.Cloud.beforeSave('className', function(req, res) {
if (/\d+/.test(req.object.get('fieldShouldBeNum'))) {
res.success();
} else {
res.error("validation error");
}
});
- データ操作やPush送信にはJavascript SDKのAPIがそのまま使えます。
雰囲気としてはNode.jsと似ていますが、異なる部分もあります。Node.jsとの相互運用を困難にしている部分としては
-
setTimeout()
が使えない- これが原因で使えないモジュールが結構あります。
-
process.nextTick()
は使用可能
-
require()
がcloud/以下のパスしか受け付けない- 例えば
require('./app')
と書きたいところを、require('cloud/app.js')
と書く必要があります。 - モジュールも同様に、
require('lodash')
などとする部分を、require('cloud/node_modules/lodash')
と書く必要があります。。 - 書いてる途中に見つけましたが、parse-requireを使えばこの問題は解決できそうです。
- 例えば
などが挙げられます。
また、Cloud CodeはES5でしか動かないため、ES2015やそれ以降の文法を用いることはできません。これはのちほどbabelを使うことで解決します。
Javascript SDK APIの非同期処理
問題点
Javascript SDKのAPIは非同期処理のインターフェースとしてPromiseを備えていますが、ES6 PromiseのようにPromise#then(onFullfiled[, onRejected])
/Promise#catch(onRejected)
/Promise.all(promises)
ではなく、Promise#then(onFulfilled[, onRejected])
/Promise#fail(onRejected)
/Promise.when(promises)
という実装になっており、少し違和感があります。 (ちなみにfail()
はAPIドキュメントには記載されているが、公式ドキュメントの例ではほとんどthen(onFulfilled, onRejected)
を使っているために気づくのに時間がかかりました。。。)
また、Cloud Codeの処理では複数のデータ操作の結果を利用して次の処理につなげる、というパターンが多いので、Promiseのチェーンが切れてしまいコード上にデータ依存の関係を見出しにくく、コードが読みにくかったりします。
少し抽象的な例となりますが、クラスAからデータを取得し、その結果を元にクラスBを更新し、AとBをPush通知するという処理を実現するコードを書いてみます。
Parse.Cloud.define('endpointX', function(req, res) {
// Aを取得
var a_promise = new Parse.Query('ClassA')
.get(req.params.A);
var b_promise = a_promise.then(function(a) {
if (!a) {
return Parse.Promise.reject('a not found');
}
return new Parse.Query('ClassB')
.get(req.params.B);
})
.then(function(b) {
if (!b) {
return Parse.Promise.reject('b not found');
}
return b.save({refA: a});
});
Parse.Promise.when([a_promise, b_promise])
.then(function(arr) {
var a = arr[0];
var b = arr[1];
return Parse.Push.send({
data: {a: a, b: b}
});
})
.then(function() {
res.success();
})
.fail(function(err) {
res.error(err);
});
});
どうでしょうか。値の受け渡しとフローの制御が冗長な印象です。さらに扱う非同期APIの数が増えると、もっと大変になります。
解決法
ES.nextのasync
/await
を使って、Promiseの冗長な構文を削減し、コードの可読性を向上させてみようと思います。http://qiita.com/plasticstraw/items/f1f06a06ad11da95166b とやっていることはほぼ同じですが、最近の感じっぽくbabelを使って実現します。
npm install --save-dev babel-cli babel-preset-es2015 babel-preset-stage-3
-
async
/await
は現在stage-3ですが、これからstageが進んだり廃止されることがあるかもしれませんので、その点にご注意下さい (2015/12)。
babelの設定としては、pluginにes2015
とstage-3
を追加します。私はsrc/にES2015のソースコードを置き、変換後のコードをcloud/に置かれるように、以下の様にpackage.jsonに追記して変換を行っていました。
{
"scripts": {
"build": "babel src --out-dir cloud --presets es2015,stage-3",
"deploy": "npm run build && parse deploy"
}
}
async
/await
を動作させるためには、babelでの変換に加えてregeneratorRuntimeとPromiseが必要になります。ParseのPromiseには前述のようにfail
がないなどの問題がありますがregeneratorRuntimeで使用する分には問題ないので、気にならない場合は
const Promise = Parse.Promise;
const regeneratorRuntime = require('cloud/modules/regeneratorRuntime');
を変換前のソースの先頭に追記します。
ES2015のPromiseを使用したい場合は、ES2015(ES6)準拠のPromiseライブラリ/Polyfillを使用します。RSVPおよびes6-promiseはsetTimeout()
の問題がありcloudCode上では利用できないため、私はypromiseを使っていました。この場合は以下のようになります。
const Promise = require('cloud/modules/ypromise');
const regeneratorRuntime = require('cloud/modules/regeneratorRuntime');
以上の手順により、ES2015 + async
/await
で上記の例を書き換えてみます。
const Promise = require('cloud/modules/ypromise');
const regeneratorRuntime = require('cloud/modules/regeneratorRuntime');
Parse.Cloud.define('endpointX', async (req, res) => {
try {
// Aを取得
const a = await new Parse.Query('ClassA')
.get(req.params.A);
if (!a) {
throw new Error('a not found');
}
const b = await new Parse.Query('ClassB')
.get(req.params.B);
if (!b) {
throw new Error('b not found');
}
await b.save({refA: a});
await Parse.Push.send({
data: {a, b}
});
res.success();
} catch (e) {
res.error(e);
}
});
無名関数が減り、データの受け渡しがだいぶすっきりしました。
おわりに
- Cloud Code上ではブレークポイントを貼ったりすることはできず、ログしか頼りにならないので、デバッグが大変でした。
- ローカルのNode.jsで実行して、APIリクエストのみCloud Codeに投げるparse-cloud-debuggerが使えそうでした
- Cloud Codeの最近の状況
- ParseはCloud Code Webhookや、Parse + Herokuが最近発表されました
- Cloud CodeのJavascriptで直接処理を書くのではなく、Cloud Codeをプロキシとして他のサーバやHeroku上で処理を書く
- 本物のNode.jsや言語に縛られない開発ができて、デバッグもしやすい
- 外部のサーバにアクセスすることになってしまうので、レスポンスタイムが増加しますが、使用したほうが楽に開発できると思います。
- ParseはCloud Code Webhookや、Parse + Herokuが最近発表されました
- あまりにもCloud Codeでやることが増えすぎると、普通のサーバサイド開発と変わらずmBaaSであることのメリットがPush通知が楽であることくらいしかなくなってしまいます。そうなりそうなときは、本当にParse(mBaaS)が必要か?というのは考えたほうが良いと思います。