Facebook Parseの世界

260
Parseの世界 @maruyama097 丸山 不二夫

Transcript of Facebook Parseの世界

Parseの世界

@maruyama097

丸山 不二夫

“I think the biggest mistake that we made, as a company, is betting too much on HTML5

as opposed to native…”

Mark Zuckerburg 2012年9月

“Facebook does not hate the Web.”

Tom Occhino f8 2015年4月

Agenda

Part I

モバイル・アプリをめぐる動向

Part II

Parseの世界

Part III

Web技術の新しい展開

モバイル・アプリをめぐる動向

このところ、モバイル・アプリの開発スタイルをめぐって、注目すべき動きが連続している。アプリ開発のスタイルの大きな変革期に差し掛かっていると考えていいと思う。

Part I

Part Iモバイルアプリをめぐる動向

モバイル・アプリをめぐる新しい動き

新しいモバイル・アプリの諸特徴とその背景

FacebookとWebテクノロジー

Facebook : “Parse”https://parse.com/

Amazon : “AWS Mobile SDK” http://goo.gl/Nc5Bta

Microsoft : “Azure Mobile App Services” http://goo.gl/dcWlRF

Twitter : "Fabric" https://goo.gl/dV3Zsb

モバイルアプリをめぐる新しい動き

Facebook : Parsehttps://parse.com/

Amazon : AWS Mobile SDKhttp://goo.gl/Nc5Bta

Microsoft : Azure Mobile App Serviceshttp://goo.gl/dcWlRF

Twitter : Fabrichttps://goo.gl/dV3Zsb

新しいモバイル・アプリの諸特徴とその背景

これらの動きは、幾つかの特徴を共有している。ここでは、これらの動きを典型的に表していると思われるFacebook Parseを中心に、旧来のWebアプリとの比較でその特徴を見ることにしよう。あわせて、その背景を見ておこう。

新しいモバイル・アプリの諸特徴とその背景

Server/Clientから、Cloud/Mobileへ

Server-sideから、Mobile-sideへ

同期型から、非同期型へ

PULLモデルから、PUSHのサポートへ

Server/Clientから

Cloud/Mobileへ

ネットワーク・プログラミングでのサーバーとクライアントの役割は、現在も基本的なものである。ただ、その役割の主要な担い手は、クラウドとモバイルに、大きく変わりつつある。現在、もっとも一般的なアプリ開発の舞台は、クラウドとモバイルであることに注意しよう。

Parse Old Web Apps

Cloud & Mobile Server & Client

モバイルは、人類史上最大のプラットフォーム

Mooreの法則でチップは、こんなにも小さくなった

ウエアラブル用Cortex A7 UP

500MHz, 0.36mm2

IOT用Cortex M0

40MHz, 0.05mm2

ハイエンドウエアラブル用

Cortex A7 MP2500MHz, 1.1mm2

モバイル用Cortex A7 MP2

1.3GHz, 2.2mm2

Qualcomm のフラグシップ・モバイルCPUのパフォーマンスの変化

指数関数的に、スピードアップしている

PCとモバイルのプロセッサー

歴史的には、モバイルのプロセッサーは、PCに数年遅れていた。ただし、現在(2014年)では、処理能力でもメモリーのアクセス・スピードでも、PCに並び、コア数では、それを追い越している。

処理速度

コア数 メモリー・アクセス

Server-sideから

Mobile-sideへ

旧来のエンタープライズ・アプリの中心は、サーバー・サイドのWebアプリであった。ただ、現在進行中の変化は、モバイル・サイドのアプリ開発が、今後、主流になるだろうことを示している。この点では、Thin Server Architectureという考えが非常に参考になる。

Parse Old Web Apps

Mobile-side Programming Server-side

+ Cloud Code Programming

Code

Code

Cloud Code

サーバーサイドのWebアプリのスタイルの見直しの進行

サーバーサイドのWebアプリ

+HTML5の新機能

AndroidNative

クライアントのデバイスの処理能力が飛躍的に向上して、サーバーの生成するHTMLをブラウザーでレンダリングする以上の能力を持ち始めた。

サーバーの負担・ネットワーク帯域の増大等、いろいろな困難がうまれていて、それらの見直しが進行。

サーバー側

クライアント側

アプリケーション開発の多様化

J2EE,Spring.ASP,.NET等のサーバーサイドアプリのクライアント Application

Cacheを利用したアプリ Widget

PackagedWeb AppchromeTizenFirefox OS

JavaScript MVCFrameworks

AndroidNative

現実に進行したのは、「サーバーサイドWebアプリ+HTML5」の世界の一方的拡大ではなかった。

サーバーサードのWebアプリと、クライアント中心のNativeアプリの中間に、様々のアプリケーションの形態が生まれた。

Webアプリのサーバーとクライアントへの分岐とPackaged Web Appへの関心

J2EE,Spring.ASP,.NET等のサーバーサイドアプリのクライアント Application

Cacheを利用したアプリ Widget

JavaScript MVCFrameworks

AndroidNative

従来型のWebアプリ(サーバーサイド)

新しいタイプのWebアプリ(クライアントサイド)

外部にWebサーバーを必要としない

外部にWebサーバーを必要とする

PackagedWeb AppChromeTizenFirefox OS

サーバーとクライアントの役割の見直しの一般的な背景

1. ハードの性能が、特にクライアント側で飛躍的に向上したこと。クラウド・デバイスは、PCより、はるかにリッチなクライアントである。

2. サーバーの負荷の増大

3. ネットワーク・トラフィックの増大

4. プログラムとViewの分離の難しさ

全てがサーバー側でコントロールされ、特に、両者が密に絡み合っているようなWebアプリでは、どちらの変更も、開発の工程に大きなインパクトを与える可能性がある。

プログラマはデザインの変更を嫌い、デザイナーは、プログラムの変更を嫌う。

Thin Server Architectureについて

Thin Server Architecture というのは、2008年の初め頃に、Mario Valenteを中心とするグループが考えだしたコンセプト。

残念ながら、発表当時は、大きな反響を起こすこともなく、忘れられていた。今また、こうした数年前のコンセプトに照明があたると言うのも、興味深いことである。

https://sites.google.com/a/thinserverarchitecture.com/home/Home

Thin Server Architectureの定義

Thin Server Architecture(TSA)は、今日のWebアプリケーション・フレームワークの、慢心と複雑さに対する、反応である。

TSAは、全てのプレゼンテーション層のロジックを、サーバーから、クライアント(Webブラウザー)上の、JavaScript(あるいは他)のロジックに移し変えることを提案する。

これによって、次の三つのポジティブなことが得られる。

Thin Server Architectureの定義

1. サーバー側の開発者は、ビジネス・ロジックに集中出来る。

2. クライアントが分離して開発されるので、アプリケーションは、より複雑でなくなる。

3. サーバーとクライアントの通信は、データを他方の、あるいは将来のシステムとの間のデータのインポート、エクスポート、あるいは表現に利用可能な、プロトコルを利用する。

Thin Server Architectureの図式

クライアント サーバー

資料:「Package Web Appについて--- AndroidとChromeの統合」

2013年 6月24日 マルレク第一回

概要: http://bit.ly/1tNxHWH

資料: http://bit.ly/1wdbBfX

「アプリ開発の新しい動向」http://bit.ly/1E2hOxD

同期型から

非同期型へ

関数の呼び出し、プロセス間・ノード間の通信のスタイルも大きく変わろうとしている。相手の応答があるまでブロックして待機する同期型の処理の見直しと、非同期型の処理への移行が進んでいる。Parseでは、メソッドの呼び出しは、デフォールトで非同期型である。

Parse Old Web Apps

Async Sync

1ナノ秒を1秒だとした場合の実行時間

val socket = Socket()

val packet = socket.readFromMemory()

// 3日間ブロックする

// 例外が発生しない時には、次の処理へ

val confirmation = socket.sendToEurope(packet)

// 5年間ブロックする

// 例外が発生しない時には、次の処理へ

“Principles of Reactive Programming”Coursera lecture 2014 by Erik Meijerhttps://class.coursera.org/reactive-001/lecture/51

JSConf 2009 http://goo.gl/I4HLUc

John Resig

Reactive Extension (Rx) by Microsoft

http://msdn.microsoft.com/en-us/data/gg577609.aspx

interface IObservable<out T>

{

IDisposable Subscribe(IObserver<T> observer);}

interface IObserver<in T>

{

void OnNext(T value);

void OnError(Exception ex);

void OnCompleted();

}

Rxの最も重要なインターフェースObservable

Observerが実装する3つのタイプのメソッド

Rx 資料

2010年 11月 DevCamp Keynote “Rx: Curing your asynchronous programming blues”http://channel9.msdn.com/Blogs/codefest/DC2010T0100-Keynote-Rx-curing-your-asynchronous-programming-blues

2012年 6月 TechEd Europe “Rx: Curing your asynchronous programming blues”http://channel9.msdn.com/Events/TechEd/Europe/2012/DEV413

JavaOne 2012http://goo.gl/rLr91w

JavaOne 2012http://goo.gl/rLr91w

Reactive プログラミング 資料

2014年 1月21日 マルレク第八回

概要: http://bit.ly/1tDnESC

資料: http://bit.ly/1kkJelH

PULLモデルから

PUSHのサポートへ

現在主流のWebアプリは、Webページの閲覧と一緒で、ユーザーがアクセスしないと起動しないPULL型のモデル。クラウドからモバイルへの、一斉通知のPUSHのサポートに対するニーズが高まっている。

Parse Old Web Apps

Push Pull

https://goo.gl/4V7vRL

Google GCM

https://goo.gl/lhJ8bl

Apple APNS

Facebook Push Notification

https://goo.gl/CuLNqf

FacebookとWebテクノロジー

FacebookのWebテクノロジーに対するスタンスは、興味深いものがある。Nativeによるユーザー・エクスペリエンスの向上を追求しながら、ポストHTML5のWebコンポーネント技術を、最大限、取り入れているように見える。Polymerと Reactを比較されたい。

FacebookのHTML5からの離脱TechCrunch誌とのインタビュー 2012年9月11日

I think the biggest mistake that we made, as a company, is betting too

much on HTML5 as opposed to

native… because it just wasn’t there.

会社として、我々が犯した最大の誤りは、ネーティブに対して、HTML5に、あまりに賭けすぎたことだと思う。

Facebook does not hate the Web.

But we're realists. We just can't use the Web right now to build the types of user experiences that we want

Tom Occhino f8 2015 React Native

Facebookの新しいWebテクノロジー

Fluxhttps://github.com/facebook/flux

Reacthttps://github.com/facebook/react

Parse + Reacthttp://blog.parse.com/learn/parse-and-react-shared-chemistry/

React Nativehttps://goo.gl/O9MjKI

GraphQL + Relayhttps://goo.gl/TXKRXQ

React Native

F8 2015

React Native & Relay: Bringing Modern Web Techniques to Mobilehttps://goo.gl/FDVQdK

By Tom Occhino

Parseの世界

ここでは、新しいモバイル・アプリ開発の代表的なプラットフォームとしてFacebook Parseを取り上げる。Parse + React, React Nativeについては、Part III で紹介する。

Part II

Part IIParseの世界

Parse Object

Relational Data の表現

Queries / Query Samples

Promises

PUSH

Cloud Code / Cloud Code Trigger

Parse Object

Parseの最大の特徴は、Parse Objectにある。

それは、モバイルとクラウドの間を自由に行き来するRemote Objectである。と同時に、それは、データの担い手として Data Objectである。

Parse Old Web Apps

Data Object

MVC

HTML/HTTPget/Fetch Save

Parse.Objectクラスとインスタンスの生成

Parse JavaScript

// Parse.objectのサブクラスを生成する簡単なシンタックス

var GameScore = Parse.Object.extend("GameScore");

// このクラスの新しいインスタンスを生成する。

var gameScore = new GameScore();

// 先の方法の代わりに、典型的なBackboneのシンタックスを利用することもできるvar Achievement = Parse.Object.extend({

className: "Achievement"});

A complex subclass of Parse.Object

// Parse.objectのサブクラスを生成する少し複雑ななシンタックスvar Monster = Parse.Object.extend(“Monster”, // 第一引数 クラス名{ // 第二引数 インスタンスのメソッド達

hasSuperHumanStrength: function () {return this.get("strength") > 18;

},// インスタンスのプロパティの初期値は、initialize で定義する

initialize: function (attrs, options) {this.sound = "Rawr"

}}, { // 第三引数 クラスのメソッド達

spawn: function(strength) {var monster = new Monster();monster.set("strength", strength);return monster;

}});

var monster = Monster.spawn(200);alert(monster.get(‘strength’)); // spawnでsetされた 200を表示alert(monster.sound); // インスタンスの soundの初期値 Rawr を表示.

Data Objectとしての Parse.Object

var number = 42;var string = "the number is " + number;var date = new Date();var array = [string, number];var object = { number: number, string: string };

var BigObject = Parse.Object.extend("BigObject");var bigObject = new BigObject();bigObject.set("myNumber", number);bigObject.set("myString", string);bigObject.set("myDate", date);bigObject.set("myArray", array);bigObject.set("myObject", object);bigObject.set("myNull", null);bigObject.save();

Parse.Objectクラスとインスタンスの生成

Parse Android Java

ParseObject gameScore =new ParseObject("GameScore");

Parse iOS

PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];

Parse .NET

ParseObject gameScore =new ParseObject("GameScore");

Parse iOS Swift

var gameScore = PFObject(className:"GameScore")

Parse.Objectオブジェクトの保存 save

var GameScore = Parse.Object.extend("GameScore");var gameScore = new GameScore();

gameScore.set("score", 1337);gameScore.set("playerName", "Sean Plott");gameScore.set("cheatMode", false);

gameScore.save( null, {

success: function(gameScore) {// オブジェクトの保存に成功した後で、実行されるべき処理alert('New object created with objectId: ' + gameScore.id);

},error: function(gameScore, error) {

// オブジェクトの保存に失敗した後で、実行されるべき処理// error は、 Parse.Errorで、 error code と messageを持っているalert('Failed to create new object, with error code: ' + error.message);

}});

フィールドを、saveの第一引数で、直接に設定

var GameScore = Parse.Object.extend("GameScore");var gameScore = new GameScore();

gameScore. save({

score: 1337,playerName: "Sean Plott",cheatMode: false

}, {success: function(gameScore) {

// The object was saved successfully.},error: function(gameScore, error) {

// The save failed.// error is a Parse.Error with an error code and message.

}});

Saveされるデータ

objectId: “xWMyZ4YEGZ”, score: 1337, playerName: “Sean Plott”, cheatMode: false,createdAt: “2011-06-10T18:33:42Z”, updatedAt: "2011-06-10T18:33:42Z"

SaveされたオブジェクトのユニークID

Saveされた時点で追加・変更されるフィールド

オブジェクトの保存(非同期で行われる)

Parse Android JavagameScore.put("score", 1337);gameScore.put("playerName", "Sean Plott");gameScore.put("cheatMode", false);

gameScore.saveInBackground();

Parse .NETgameScore["score"] = 1337;gameScore["playerName"] = "Sean Plott";gameScore[“cheatMode”] = false;

await gameScore.SaveAsync();

Parse iOS SwiftgameScore["score"] = 1337gameScore["playerName"] = "Sean Plott”gameScore["cheatMode"] = false

gameScore.saveInBackgroundWithBlock { … }

Parse.Objectオブジェクトの取得 get

var GameScore = Parse.Object.extend("GameScore");

var query = new Parse.Query(GameScore);

query.get( "xWMyZ4YEGZ",

{success: function(gameScore) {

// オブジェクトの取得に成功},error: function(object, error) {

// オブジェクトの取得に失敗// error は Parse.Errorで、error codeとmessageを持つ.

}});

オブジェクトの取得 (Android)

ParseQuery<ParseObject> query = ParseQuery.getQuery(

"GameScore");

query.getInBackground( "xWMyZ4YEGZ",

new GetCallback<ParseObject>() {public void done(ParseObject object, ParseException e) {

if (e == null) { // objectは、ゲームのスコア

} else { // なんか、うまくいかない

} }

});

オブジェクトの取得

Parse .NETParseQuery<ParseObject> query =

ParseObject.GetQuery("GameScore");

ParseObject gameScore = await query.GetAsync("xWMyZ4YEGZ");

Parse iOS Swiftvar query = PFQuery(className:"GameScore")

query.getObjectInBackgroundWithId("xWMyZEGZ") { (gameScore: PFObject?, error: NSError?) -> Void in

if error == nil && gameScore != nil {println(gameScore)

} else { println(error)

}}

データベースとの同期 FetchmyObject.fetch({

success: function(myObject) {// オブジェクトの更新に成功

},error: function(myObject, error) {

// オブジェクトの更新に失敗// error は Parse.Errorで、error codeとmessageを持つ.

}});

myObject.fetchInBackground(new GetCallback<ParseObject>() {

public void done(ParseObject object, ParseException e) {if (e == null) {

// Success! } else {

// Failure! }

}});

await myObject.FetchAsync();

オブジェクトの更新// オブジェクトの生成var GameScore = Parse.Object.extend("GameScore");var gameScore = new GameScore();

gameScore.set("score", 1337);gameScore.set("playerName", "Sean Plott");gameScore.set("cheatMode", false);gameScore.set("skills", ["pwnage", "flying"]);

gameScore.save(null, {success: function(gameScore) {

// 保存に成功したら、新しいデータで更新しよう// この場合、cheatModeとscoreがクラウドに送られて// playerNameは、変わらない。gameScore.set("cheatMode", true);gameScore.set("score", 1338);gameScore.save();

}});

オブジェクトの削除 destroy

myObject. destroy({success: function(myObject) {

// objectは、Parse Cloudから削除された},error: function(myObject, error) {

// 削除失敗。// error は Parse.Errorで、error codeとmessageを持つ.

}});

// unsetメソッドで、オブジェクトの一つのフィールドの削除ができる// このあと、 playerName フィールドは、空になるmyObject.unset("playerName");

// 削除されたフィールドをParse Cloudに保存するmyObject.save();

Relational Data の表現

Parse Objectでは、Relationalなデータの表現が可能である。あとで見る Parse Queryを使って、データの検索が可能となる。

一対一、一対多の関係の表現// 型を宣言するvar Post = Parse.Object.extend("Post");var Comment = Parse.Object.extend("Comment");

// postを生成するvar myPost = new Post();myPost.set("title", "I'm Hungry");myPost.set("content", "Where should we go for lunch?");

// commentを生成するvar myComment = new Comment();myComment.set("content", "Let's do Sushirrito.");

// postをcommentの parentの値として設定する

myComment.set("parent", myPost);

// myPost とmyCommentの両方が保存されるmyComment.save();

MyPost

MyComment

parent

MyPost

MyComment

parent

内部的には、Parseのフレームワークは、整合性を維持するために、参照されたオブジェクトを一箇所のみに格納する。オブジェクトIDを使った場合のみ、次のように、オブジェクトのリンクは可能である。

var post = new Post();post.id = "1zEcyElZ80";

myComment.set("parent", post);

myComment.put(“parent”, ParseObject.createWithoutData("Post", "1zEcyElZ80"));

myComment[“parent”] = ParseObject.CreateWithoutData("Post", "1zEcyElZ80");

myComment[“parent”] = PFObject(withoutDataWithClassName:“Post”,objectId:"1zEcyElZ80")

var post = fetchedComment. get("parent");post.fetch({success: function(post) {var title = post.get("title");

}});

関連付けられたオブジェクトの取得

オブジェクトを取り出しても、デフォールトでは、Parse.Objectに関連付けられたオブジェクトは取り出せない。これらのオブジェクトの値は、次のようにしないと取り出せない。

post

fetchedComment

post

fetchedComment

get(“parent”)

var post = myComment[“parent”] as PFObjectpost.fetchIfNeededInBackgroundWithBlock {

(post: PFObject?, error: NSError?) -> Void inlet title = post?[“title”] as? NSString // do something with your title variable

}

ParseObject post = fetchedComment.Get<ParseObject>(“parent”);

await post.FetchIfNeededAsync();

fetchedComment.getParseObject(“post”).fetchIfNeededInBackground(

new GetCallback<ParseObject>(){public void done(ParseObject post, ParseException e) {

String title = post.getString(“title”);// Do something with your new title variable

}});

多対多の関係の表現 relation

Many-to-manyの関係は、Parse.Relation を使ってモデル化される。このモデルは、一つのキーに、Parse.Objectの配列を格納するのに似ている。ただし、全ての関係のオブジェクトを一回で取り出す必要がないことを除いては。

加えて、このことで Parse.Relation は、 Parse.Object

の配列を利用するアプローチと比較して、より多くのオブジェクトにスケールすることが可能になる。

var user = Parse.User.current();var relation = user.relation("likes");relation.add(post);user.save();

1

2

User

Post

likes

1 3

2

ParseUser user = ParseUser.getCurrentUser();ParseRelation<ParseObject> relation = user.getRelation("likes");relation.add(post);user.saveInBackground();

var user = ParseUser.CurrentUser;var relation = user.GetRelation<ParseObject>(“likes”);relation.Add(post);await user.SaveAsync();

var user = PFUser.currentUser()var relation = user.relationForKey(“likes”)relation.addObject(post)user.saveInBackground()

var user = Parse.User.current();var relation = user.relation("likes");relation.add(post);user.save();

関係リストの取得

デフォールトでは、次のような関係のオブジェクトのリストはダウンロードできない。

この場合には、queryで返される Parse.Query を利用すれば、ユーザーが「いいね」をつけたポストのリストを、取得できる。次のようなコードになる。

relation.add( [post1, post2, post3] );user.save();

relation.query().find({ success: function(list) {

// リストは、ユーザーがいいねをしたpostを含んでいる});

relation.getQuery().findInBackground(new FindCallback<ParseObject>() {

void done(List<ParseObject> results, ParseException e) {if (e != null) {

// There was an error } else { // results have all the Posts the current user liked.

} }});

IEnumerable<ParseObject> relatedObjects = await relation.Query.FindAsync();

relation.query().findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in

if let error = error {// There was an error

} else { // objects has all the Posts the current user liked.

}}

関係リストの項目の削除 remove

// postをParse.Relationから削除できる

relation.remove(post);user.save();

// saveを呼ぶ前に、複数回removeを呼べるrelation.remove(post1);relation.remove(post2);user.save();

Queryの制限条件

ポストのサブセットが欲しいならば、次のように制限条件をParse.Queryに課す。

var query = relation.query();query.equalTo("title", "I'm Hungry");query.find({

success:function(list) {// list は、現在のユーザーによって「いいね」されたポストで、タイトルが// "I'm Hungry” であるものを含んでいる。

}});

Queries

Parseの強力な機能の一つは、クラウド上のデータベースに対する検索機能が備わっていることである。

Parse.Queryが、それを可能にする。

基本的な Query

var GameScore = Parse.Object.extend("GameScore");

var query = new Parse.Query(GameScore);query.equalTo("playerName", "Dan Stemkoski");query.find({

success: function(results) {alert("Successfully retrieved " + results.length + " scores.");// 返された Parse.Objectの値を処理for (var i = 0; i < results.length; i++) {

var object = results[i];alert(object.id + ' - ' + object.get('playerName'));

}},error: function(error) {

alert("Error: " + error.code + " " + error.message);}

});

基本的な Query

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");

query.whereEqualTo("playerName", "Dan Stemkoski");query.findInBackground(

new FindCallback<ParseObject>() {public void done(

List<ParseObject> scoreList, ParseException e) { if (e == null) {

Log.d("score", "Retrieved " + scoreList.size() + " scores"); } else {

Log.d("score", "Error: " + e.getMessage()); }

}});

基本的な Query

var query = from gameScore in ParseObject.GetQuery("GameScore")where gameScore.Get<string>("playerName") == "Dan Stemkoski" select gameScore;

IEnumerable<ParseObject> results = await query.FindAsync();

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];NSArray* scoreArray = [query findObjects];

.NET版 Parseでは、LINQが使える。await ... async 構文と合わせて、なかなか、格好がいい。

フィールドの選択 select

var GameScore = Parse.Object.extend("GameScore");var query = new Parse.Query(GameScore);query.select("score", "playerName");query.find().then(function(results) {// resultsは、 “score”, “playerName” のみのフィールドを含む

});

Query の制約条件

複数の制約条件をqueryに与えることができる。全ての条件にマッチしたオブジェクトのみが結果に含まれる。別の言い方をすれば、それは、制約条件のANDに似ている。

query.notEqualTo("playerName", "Michael Yabuti");query.greaterThan("playerAge", 18);

limitを設定することで、結果の数を制限できる。デフォールトでは、結果は100個に制限されている。1から1000までの任意の設定ができる。

query.limit(10); // 最大 10個の値に制限する

Query の制約条件

一つの結果だけが必要なら first を利用できる。

var GameScore = Parse.Object.extend("GameScore");var query = new Parse.Query(GameScore);query.equalTo("playerEmail", "[email protected]");query.first({

success: function(object) {// オブジェクトの取得に成功

},error: function(error) {

alert("Error: " + error.code + " " + error.message);}

});

skipを設定して、最初の結果をスキップできる。

query.skip(10); // 最初の10この結果をスキップ

ソートと比較演算

// score フィールドを昇順に整列するquery.ascending("score");

// scoreフィールドを降順に整列するquery.descending("score");

// ソート可能な型に対して、比較演算が可能である// wins < 50query.lessThan("wins", 50);

// wins <= 50query.lessThanOrEqualTo("wins", 50);

// wins > 50query.greaterThan("wins", 50);

// wins >= 50query.greaterThanOrEqualTo("wins", 50);

メンバー、存在、非存在のチェック

// Jonathan, Dario, Shawnのスコアを検索するquery.containedIn("playerName",

["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);

// Jonathan, Dario, Shawn以外のスコアを検索するquery.notContainedIn("playerName",

["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);

// スコアが設定されているオブジェクトを検索query.exists("score");

// スコアが設定されていないオブジェクトを検索query.doesNotExist("score");

// wins < 50という条件で絞るquery = from gameScore in query

where gameScore.Get<int>("wins") < 50 select gameScore;

// wins <= 50という条件で絞るquery = from gameScore in query

where gameScore.Get<int>("wins") <= 50select gameScore;

// wins > 50という条件で絞るquery = from gameScore in query

where gameScore.Get<int>("wins") > 50select gameScore;

// wins >= 50という条件で絞るquery = from gameScore in query

where gameScore.Get<int>("wins") >= 50select gameScore;

.NET ParseでのLINQの利用

var Team = Parse.Object.extend("Team");var teamQuery = new Parse.Query(Team);teamQuery.greaterThan("winPct", 0.5);var userQuery = new Parse.Query(Parse.User);userQuery.matchesKeyInQuery("hometown", "city", teamQuery);userQuery.find({

success: function(results) {// ホームタウンで、勝ち越しているチームの検索結果

}});

ParseQuery<ParseObject> teamQuery = ParseQuery.getQuery(“Team”);teamQuery.whereGreaterThan(“winPct”, 0.5);ParseQuery<ParseUser> userQuery = ParseUser.getQuery();userQuery.whereMatchesKeyInQuery(“hometown”, “city”, teamQuery);userQuery.findInBackground(

new FindCallback<ParseUser>() {void done(List<ParseUser> results, ParseException e) {

// results has the list of users with a hometown team with // a winning record

}});

ホームタウンで、勝ち越しているユーザーの検索

var teamQuery = from team in ParseObject.GetQuery("Team") where team.Get<double>("winPct") > 0.5select team;

var userQuery =from user in ParseUser.Queryjoin team in teamQuery on user["hometown"] equals team["city"] select user;

IEnumerable<ParseUser> results = await userQuery.FindAsync();// results will contain users with a hometown team with a winning record

var teamQuery = PFQuery(className:"Team")teamQuery.whereKey("winPct", greaterThan:0.5)var userQuery = PFUser.query()userQuery!.whereKey("hometown", matchesKey:"city", inQuery:teamQuery)userQuery!.findObjectsInBackgroundWithBlock {

(results: [AnyObject]?, error: NSError?) -> Void inif error == nil {

// results will contain users with a hometown team with a winning record}

}

var losingUserQuery = new Parse.Query(Parse.User);losingUserQuery.doesNotMatchKeyInQuery(

"hometown", "city", teamQuery);losingUserQuery.find({

success: function(results) {// results has the list of users with a hometown team with// a losing record

}});

ParseQuery<ParseUser> losingUserQuery = ParseUser.getQuery();losingUserQuery.whereDoesNotMatchKeyInQuery(

"hometown", "city", teamQuery);losingUserQuery.findInBackground(

new FindCallback<ParseUser>() {void done(List<ParseUser> results, ParseException e) {

// results has the list of users with a hometown team with// a losing record

}});

ホームタウンで、負け越しているユーザーの検索

Parse Query Sample

以下、いくつかのサンプルを紹介する。Parse Queryが、SQLやLINQと、ほぼ同等の機能を持つことを確認されたい。

Relationalな検索

// Parse.Object myPost は、すでに作られているとしようvar query = new Parse.Query(Comment);query.equalTo("post", myPost);query.find({

success: function(comments) {// comments は、myPostに対するコメントである

}});

ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");query.whereEqualTo("post", myPost); query.findInBackground(

new FindCallback<ParseObject>() { public void done(List<ParseObject> commentList, ParseException e) {

// commentList now has the comments for myPost}

});

MyPost

MyComment

“post”

// Assume ParseObject myPost was previously created.

var query = from comment in ParseObject.GetQuery("Comment") where comment["post"] == myPostselect comment;

var comments = await query.FindAsync();// comments now contains the comments for myPost

PFQuery *query = [PFQuery queryWithClassName:@"Comment"];[query whereKey:@"post" equalTo:myPost]; [query findObjectsInBackgroundWithBlock:

^(NSArray *comments, NSError *error) { // comments now contains the comments for myPost

}];

画像付きのコメントの検索var Post = Parse.Object.extend("Post");var Comment = Parse.Object.extend("Comment");var innerQuery = new Parse.Query(Post);innerQuery.exists("image");var query = new Parse.Query(Comment);query.matchesQuery("post", innerQuery);query.find({

success: function(comments) {

// postに対するコメントで、画像付きのコメントの検索結果}

});

ParseQuery<ParseObject> innerQuery = ParseQuery.getQuery("Post");innerQuery.whereExists("image");ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");query.whereMatchesQuery("post", innerQuery);query.findInBackground(new FindCallback<ParseObject>() {

public void done(List<ParseObject> commentList, ParseException e) { // comments now contains the comments for posts with images.

}});

var imagePosts = from post in ParseObject.GetQuery("Post") where post.ContainsKey("image") select post;

var query = from comment in ParseObject.GetQuery("Comment") join post in imagePosts on comment["post"] equals postselect comment;

var comments = await query.FindAsync();// comments now contains the comments for posts with images

PFQuery *innerQuery = [PFQuery queryWithClassName:@"Post"];[innerQuery whereKeyExists:@"image"];

PFQuery *query = [PFQuery queryWithClassName:@"Comment"];[query whereKey:@"post" matchesQuery:innerQuery];

[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {

// comments now contains the comments for posts with images}];

画像なしのコメントの検索var Post = Parse.Object.extend("Post");var Comment = Parse.Object.extend("Comment");var innerQuery = new Parse.Query(Post);innerQuery.exists("image");var query = new Parse.Query(Comment);query.doesNotMatchQuery("post", innerQuery);query.find({

success: function(comments) {// postに対するコメントで、画像なしのコメントの検索結果

}});

var post = new Post();post.id = "1zEcyElZ80";query.equalTo("post", post);

最新のコメント10個の検索var query = new Parse.Query(Comment);

// Retrieve the most recent onesquery.descending("createdAt");// Only retrieve the last tenquery.limit(10);// Include the post data with each commentquery.include("post");

query.find({success: function(comments) {

// Comments now contains the last ten comments, and the "post" field// has been populated. For example:for (var i = 0; i < comments.length; i++) {

// This does not require a network access.var post = comments[i].get("post");

}}

});

オブジェクトを数える count

var GameScore = Parse.Object.extend("GameScore");var query = new Parse.Query(GameScore);query.equalTo("playerName", "Sean Plott");query.count({

success: function(count) {// The count request succeeded. Show the countalert("Sean has played " + count + " games");

},error: function(error) {

// The request failed}

});

Queryの組み合わせ or

var lotsOfWins = new Parse.Query("Player");lotsOfWins.greaterThan("wins", 150);

var fewWins = new Parse.Query("Player");fewWins.lessThan("wins", 5);

var mainQuery = Parse.Query.or(lotsOfWins, fewWins);mainQuery.find({

success: function(results) {// たくさん勝っているひと、「あるいは」、ほとんど勝ってない人

},error: function(error) {

// There was an error.}

});

Queryの組み合わせ

ParseQuery<ParseObject> lotsOfWins = ParseQuery.getQuery("Player");lotsOfWins.whereGreaterThan(150);

ParseQuery<ParseObject> fewWins = ParseQuery.getQuery("Player");fewWins.whereLessThan(5);

List<ParseQuery<ParseObject>> queries = new ArrayList<ParseQuery<ParseObject>>();

queries.add(lotsOfWins);queries.add(fewWins);

ParseQuery<ParseObject> mainQuery = ParseQuery.or(queries);mainQuery.findInBackground(

new FindCallback<ParseObject>() {public void done(List<ParseObject> results, ParseException e) {

// results has the list of players that win a lot or haven't won much. }

});

Compound Queries

var lotsOfWins = from player in ParseObject.GetQuery("Player") where player.Get<int>("wins") > 150 select player;

var fewWins = from player in ParseObject.GetQuery("Player")where player.Get<int>("wins") < 5 select player;

ParseQuery<ParseObject> query = lotsOfWins.Or(fewWins); var results = await query.FindAsync();// results contains players with lots of wins or only a few wins.

Compound Queries

var lotsOfWins = PFQuery(className:"Player")lotsOfWins.whereKey("wins", greaterThan:150)

var fewWins = PFQuery(className:"Player")fewWins.whereKey("wins", lessThan:5) var query = PFQuery.orQueryWithSubqueries([lotsOfWins, fewWins])

query.findObjectsInBackgroundWithBlock {(results: [AnyObject]?, error: NSError?) -> Void in

if error == nil {// results contains players with lots of wins or only a few wins.

}}

Promises

Promiseの利用は、Parse JavaScript SDKの大きな特徴の一つである。Parseの全ての非同期メソッドは、Promiseを返す。Promiseを利用することで、callbackのネストのない、クリーンなコードを書くことができる。

Promiseを生成する

var successful = new Parse.Promise();successful.resolve(“The good result.”); // 結果は成功である

var failed = new Parse.Promise();failed.reject(“An error message.”); // 結果は失敗である

// Promiseの生成時に、その結果が分かっているのなら// as (resolved) あるいは error (rejected)メソッドを利用できる。

var successful = Parse.Promise.as("The good result.");

var failed = Parse.Promise.error("An error message.");

非同期のメソッドを作る

var delay = function(millis) {var promise = new Parse.Promise();setTimeout(function() {

promise.resolve();}, millis);return promise;

};

delay(100).then(function() {// 100m秒後に実行される

});

then メソッド

全てのPromiseは、then という名前のメソッドを持つ。

then は、2つのcallback を持つ。最初の callback は、Promise が成功した時に呼ばれ、二つ目の callback は、Promise が失敗した時に呼ばれる。

obj.save().then(function(obj) {// the object was saved successfully.

}, function(error) {// the save failed.

});

複数のPromiseを一つのチェインにつなげる

var query = new Parse.Query("Student");query.descending("gpa");query.find().then(function(students) {

students[0].set("valedictorian", true);return students[0].save();

}).then(function(valedictorian) {return query.find();

}).then(function(students) {students[1].set("salutatorian", true);return students[1].save();

}).then(function(salutatorian) {// Everything is done!

});

エラー処理

チェインの中のPromiseのどれか一つでもエラーを返せば、それ以降の全ての success コールバックは、errorコールバックに会うまでスキップされる。

errorコールバックは、そのエラーを変形して、リジェクトされない新しいPromiseを返して、それをハンドルできる。

リジェクトされたPromiseは、例外を投げるのと同じように考えることができる。errorコールバックは、エラーをハンドルし、あるいは、再度エラーを返す catch ブロックのようなものである。

Error Handling

var query = new Parse.Query("Student");query.descending("gpa");query.find().then(function(students) {

students[0].set("valedictorian", true);// 強制的にこのコールバックを失敗させるreturn Parse.Promise.error("There was an error.");

}).then(function(valedictorian) {// この処理はスキップされるreturn query.find();

}).then(function(students) {// この処理もスキップされるstudents[1].set("salutatorian", true);return students[1].save();

}, function(error) {// このエラーハンドラーが呼ばれることになる。// この中で、新しいPromiseを返すことができるreturn Parse.Promise.as("Hello!");

}).then(function(hello) {// うまくいく

}, function(error) {// エラーはすでに処理されているので、ここは呼ばれない。

});

シリアルなpromiseの実行var query = new Parse.Query("Comments");query.equalTo(“post”, 123); // post 123のcommentを検索する

query.find().then(function(results) {// promiseを一つ用意するvar promise = Parse.Promise.as();_.each(results, function(result) {

// それぞれのcommentに、それを削除する関数を適用するpromise = promise.then(function() {

// 削除が終了した時にpromiseを返す。このpromiseは、次の繰り返しで// thenに渡される。削除は、一個づつ進む。return result.destroy();

});});return promise;

}).then(function() {// 全てのcommentが削除された.

});

パラレルなpromiseの実行

whenメソッドを使って、promiseを複数のタスクをパラレルに実行するのにも利用できる。

複数の操作を一度に開始することができる。Parse.Promise.whenを使えば、その入力の全てのpromiseが解決された時に、promiseを返すような新しいpromiseを生成できる。

この新しいpromiseは、渡されたpromiseのいずれもが失敗しなかった時に成功する。そうでなければ、最後のエラーで失敗する。

パラレルに操作を実行するのは、シリアルな実行より高速であろう。しかし、それは、多くのシステムのリソースと帯域を消費するかもしれない。

パラレルなpromiseの実行

var query = new Parse.Query("Comments");query.equalTo("post", 123);

query.find().then(function(results) {// promiseの配列を用意して、一つの削除に一つのpromiseを割り当てる。var promises = [];_.each(results, function(result) {

// 削除を直ちに開始して、promiseを配列に追加するpromises.push(result.destroy());

});// whenで並列実行。全ての削除が終わったら、新しいpromiseを返すreturn Parse.Promise.when(promises);

}).then(function() {// 全てのcommentが削除された.

});

PUSH

Android, iOSといった異なるプラットフォームに対して、細かなターゲットの設定も行いながら、通知を一斉送信できる ParseのPUSH機能は、とても強力である。

PUSHを送る二つの方法

push通知を送るには、二つの方法がある。一つは、Parseの channnelを使うやり方で、もう一つは、advanced targeting を使うやり方である。

channelは、pushを送るために単純で容易なモデルを提供し、advanced targetingは、より強力で柔軟なモデルを提供する。双方ともに、お互いに完全な互換性がある。

PUSHでのクラウド利用 Cloud Code

通知の送出には、Parse.comのpushコンソールやREST API、あるいは、Cloud Codeからの送出がよく利用される。

Cloud Codeでは、JavaScript SDKが使われているので、Cloud Functionからpushを送りたいと思うのなら、まず、ここからスタートすることになる。

しかし、 Cloud Codeの外側のJavaScript SDKや他のクライアントSDKから通知を送ることに決めるなら、Parseアプリのpush通知の中で、クライアントのpushを可能にする必要がある。

クライアントからの送出は、セキュリティ上の脆弱性を持つ

しかし、クライアントからのpushは、アプリにセキュリティ上の脆弱性をもたらしうることを、きちんと理解しておくこと。推奨できるのは、クライアントからのpushをテスト目的にのみ利用することである。そして、アプリが製品段階に入る準備ができた時には、push通知のロジックをCloud Codeに移すことである。

PUSHの解析情報

ユーザーは、pushを作成してから30日間まで、過去のpushを見ることができる。将来に予定されているpushは、送出が行われていない限りは、pushコンソールから削除できる。pushの送出の後には、pushコンソールには、pushの解析情報が表示される。

PUSHで利用される属性 Installations

badge

channels

timeZone

deviceType

pushType

installationId

deviceToken

channelUris

appName

appVersion

parseVersion

appIdentifier

Channelを使う

通知を送る最も簡単な方法は、channel を使うことである。channelを使えば、pushを送るのに、publisher-subscriber モデルを利用することが可能になる。

デバイスは、一つ以上のchannelにサブスクライブすることから始める。その後、通知は、サブスクライブしたデバイスに送られる。

与えられたInstallationによってサブスクライブされたchannelは、Installationオブジェクトのchannel フィールドに格納される。

ChannelにPUSHを送る

Parse.Push.send({// “Giants”と "Mets" という二つのchannelにPUSHを送るchannels: [ “Giants”, “Mets” ], // Pushで送られるデータdata: { alert: "The Giants won against the Mets 2-3."

}}, {

success: function() {// Push は成功した

}, error: function(error) {// エラー

}});

GiantsチャンネルとMetsチャンネルに、試合結果を配信する

Sending Pushes to Channels

LinkedList<String> channels = new LinkedList<String>();channels.add("Giants");channels.add("Mets"); ParsePush push = new ParsePush();push.setChannels(channels); // Notice we use setChannels not setChannelpush.setMessage("The Giants won against the Mets 2-3.");push.sendInBackground();

// Send a notification to all devices subscribed to the "Giants" channel.var push = new ParsePush();push.Channels = new List<string> {"Giants"};push.Alert = "The Giants just scored!";await push.SendAsync();

let channels = [ "Giants", "Mets" ]let push = PFPush() // Be sure to use the plural 'setChannels'.push.setChannels(channels)push.setMessage("The Giants won against the Mets 2-3.")push.sendPushInBackground()

Advanced Targetingを使う

channelは、多くのアプリでとても役に立つのだが、push

の受け取り手をターゲットする際に、もっと細かく指定することが必要となることがある。

Parseでは、Query APIを使えば、Installationオブジェクトの任意のサブセットに対するqueryを書くことができるので、それらにpushを送出できる。

Installationオブジェクトは、他のオブジェクトと同様に、Parseに格納されているので、どんなデータでも保存できるし、Installationオブジェクトと他のオブジェクトとの関係も作り出すこともできる。 こうして、ユーザーベースで、非常にカスタマイズ化されたダイナミックなセグメントに、pushを送ることができる。

Queryの結果にPushを送る

var query = new Parse.Query(Parse.Installation);query.equalTo('injuryReports', true); // injuryReportsが真のものParse.Push.send({

where: query, // 送出先data: { alert: "Willie Hayes injured by own pop fly."

}}, {success: function() {// Push は成功した}, error: function(error) {// Handle error

}}); 負傷者情報を配信する

QueryをChannelに使う

var query = new Parse.Query(Parse.Installation);query.equalTo(‘channels’, ‘Giants’); // Set our channelquery.equalTo('scores', true);

Parse.Push.send({ where: query, // channelがGiantsで、scoreが真のものへdata: {

alert: "Giants scored against the A's! It's now 2-2." }

}, { success: function() { // Push was successful }, error: function(error) { // Handle error }

});

Giantsチャンネルに得点を配信する

// Create our Installation queryParseQuery pushQuery = ParseInstallation.getQuery();// Set the channelpushQuery.whereEqualTo("channels", "Giants"); pushQuery.whereEqualTo("scores", true);

// Send push notification to queryParsePush push = new ParsePush();push.setQuery(pushQuery);push.setMessage("Giants scored against the A's! It's now 2-2.");push.sendInBackground();

var push = new Parse.Push();push.Query = from installation in ParseInstallation.Query

where installation.Get<bool>("scores") == true select installation;

push.Channels = new List<string> { "Giants" };push.Alert = "Giants scored against the A's! It's now 2-2.";await push.SendAsync();

// Create our Installation querylet pushQuery = PFInstallation.query()// Set channelpushQuery.whereKey("channels", equalTo: "Giants") pushQuery.whereKey("scores", equalTo: true)

// Send push notification to querylet push = PFPush()// Set our Installation querypush.setQuery(pushQuery) push.setMessage("Giants scored against the A's! It's now 2-2.")push.sendPushInBackground()

// ユーザーがその場所に近いか調べるvar userQuery = new Parse.Query(Parse.User);

userQuery.withinMiles("location", stadiumLocation, 1.0);

// そのユーザーのPushの送り先を調べるvar pushQuery = new Parse.Query(Parse.Installation);

pushQuery.matchesQuery('user', userQuery);

// Send push notification to query

Parse.Push.send({

where: pushQuery,

data: {

alert: "Free hotdogs at the Parse concession stand!"

}

}, {

success: function() {

// Push was successful

},

error: function(error) {

// Handle error

}

});

スタジアムに近いところにいるユーザーに通知を送る

// Find users near a given locationParseQuery userQuery = ParseUser.getQuery();userQuery.whereWithinMiles("location", stadiumLocation, 1.0) // Find devices associated with these usersParseQuery pushQuery = ParseInstallation.getQuery();pushQuery.whereMatchesQuery("user", userQuery); // Send push notification to queryParsePush push = new ParsePush();push.setQuery(pushQuery); // Set our Installation querypush.setMessage("Free hotdogs at the Parse concession stand!");push.sendInBackground();

// Find users in the Seattle metro areavar userQuery = ParseUser.Query.WhereWithinDistance(

"location",marinersStadium, ParseGeoDistance.FromMiles(1));

var push= new ParsePush();push.Query = from installation in ParseInstallation.Query

join user in userQuery on installation["user"] equals userselect installation;

push.Alert = "Mariners lost? Free conciliatory hotdogs at the Parse concession stand!";await push.SendAsync();

// Find users near a given locationlet userQuery = PFUser.query()userQuery.whereKey("location", nearGeoPoint: stadiumLocation, withinMiles: 1) // Find devices associated with these userslet pushQuery = PFInstallation.query()pushQuery.whereKey("user", matchesQuery: userQuery)

// Send push notification to querylet push = PFPush()push.setQuery(pushQuery)// Set our Installation querypush.setMessage("Free hotdogs at the Parse concession stand!")push.sendPushInBackground()

送出オプション

Push通知は、単にメッセージを送る以上のことが可能である。

iOSでは、pushに音や画像を含めることができるし、任意のカスタムデータを送ることができる。

Androidでは、通知を受け取った時に起動されるIntent

の指定までできる。通知が、時間に敏感な場合には、エクスパイアーする日付の設定も可能である。

通知をカスタマイズする

単なるメッセージ以上のものを送ろうと思ったら、データ辞書の他のフィールドを設定することができる。特別な意味を持つ次のようなフィールドが用意されている。

alert: 通知のメッセージ

badge: (iOS のみ) アプリのアイコンの上右隅に示される値。値を指定することも、一つづつその値を増やすこともできる。

sound: (iOS のみ) アプリケーション内のサウンド・ファイルの名前

content-available: (iOS のみ) Newsstandアプリや、iOS7から導入された Remote Notification Background Mode (“Background Push”とよばれている)を使っている場合には、バックグラウンドでのダウンロードを起動するために、この値に1を設定する。

category: (iOS のみ) このpush通知のUIUserNotificationCategoryの識別子

uri: (Android のみ) URIを含む、オプションのフィールド。通知が開かれた時、このURIに関連したActivityが起動される。

title: (Android のみ) Androidのシステム・トレーの通知に表示される値。

Parse.Push.send({channels: [ "Mets" ], data: {

alert: "The Mets scored! The game is now tied 1-1.", badge: "Increment", sound: "cheering.caf", title: "Mets Score!"

}}, {

success: function() {// Push was successful

}, error: function(error) {

// Handle error }

});

JSONObject data = new JSONObject("{¥“alert¥”: ¥“The Mets scored!¥”, ¥“badge¥”: ¥“Increment¥”, ¥"sound¥": ¥"cheering.caf¥"}");

ParsePush push = new ParsePush();push.setChannel("Mets");push.setData(data);push.sendPushInBackground();

Metsが得点した時に、Metsチャンネルに通知する。音付きで。

let data = ["alert" : "The Mets scored! The game is now tied 1-1!", "badge" : "Increment","sounds" : "cheering.caf"]

let push = PFPush()push.setChannels(["Mets"])push.setData(data)push.sendPushInBackground()

var push = new ParsePush();push.Channels = new List<string> {"Mets"};push.Data = new Dictionary<string, object> {

{"title", "Score Alert"} {"alert", "The Mets scored! The game is now tied 1-1!"},

};await push.SendAsync();

var query = new Parse.Query(Parse.Installation);

query.equalTo('channels', 'Indians');

query.equalTo('injuryReports', true);

Parse.Push.send({

where: query,

data: {

action: "com.example.UPDATE_STATUS"

alert: "Ricky Vaughn was injured in last night's game!",

name: "Vaughn",

newsItem: "Man bites dog"

}

}, {

success: function() {

// Push was successful

},

error: function(error) {

// Handle error

}

});

Indians チャンネルに負傷者情報を流す。アクション付きで。

JSONObject data = new JSONObject("{¥“name¥”: ¥“Vaughn¥”, ¥"newsItem¥": ¥"Man bites dog¥"}”

)); ParsePush push = new ParsePush();push.setQuery(injuryReportsQuery);push.setChannel("Indians");push.setData(data);push.sendPushInBackground();

let data = ["alert" : "Ricky Vaughn was injured in last night's game!", "name" : "Vaughn", "newsItem" : "Man bites dog”

]

let push = PFPush()push.setQuery(injuryReportsdata)push.setChannel("Indians")push.setData(data)push.sendPushInBackground()

エクスパイアの設定

デバイスの電源が入っていなかったり、インターネットに接続されていない場合、push通知は配達されない。もし、遅れて配達されれば意味のない時間に敏感な通知の場合には、エクスパイアーする日付を設定できる。これで、もはや重要ではない情報で、不必要にユーザーに警告するのを避ける。

通知のエクスパイアーの時間を指定するために、Parseでは二つのパラメーターが用意されている。一つは、Parseが通知を送るのを止めるべき日付を指定する expiration dateである。

例えば、これから正確に一週間後に、通知をエクスパイアーさせるには、次のようにする。

Parse.Push.send({where: everyoneQuery, expiration_time: new Date(2015, 5, 10)data: {

alert: "Season tickets on sale until May 10, 2015" }

}, { success: function() {// Push was successful

}, error: function(error) {// Handle error

}});

ParsePush push = new ParsePush();push.setExpirationTime(1430762356);push.setQuery(everyoneQuery);push.setMessage("Season tickets on sale until May 4th");push.sendPushInBackground();

指定の日付5/15以降は通知を受け取れないように設定する。

var push = new ParsePush();push.Expiration = new DateTime(2015, 5, 4);push.Alert = "Season tickets on sale until May 4th";await push.SendAsync();

// Create date object for tomorrowNSDateComponents *comps = [[NSDateComponents alloc] init];[comps setYear:2015];[comps setMonth:5];[comps setDay:4];NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];NSDate *date = [gregorian dateFromComponents:comps];

// Send push notification with expiration datePFPush *push = [[PFPush alloc] init];[push expireAtDate:date];[push setQuery:everyoneQuery];[push setMessage:@"Season tickets on sale until May 4th"];[push sendPushInBackground];

プラットフォームでターゲットする// Notification for Android usersvar queryAndroid = new Parse.Query(Parse.Installation);queryAndroid.equalTo('deviceType', 'android'); Parse.Push.send({

where: queryAndroid, data: {

alert: "Your suitcase has been filled with tiny robots!" }

});

// Notification for iOS usersvar queryIOS = new Parse.Query(Parse.Installation);queryIOS.equalTo(‘deviceType’, ‘ios'); Parse.Push.send({

where: queryIOS, data: {

alert: "Your suitcase has been filled with tiny robots!" }

});

Android端末のみに通知を送る

iOS端末のみに通知を送る

// Notification for Windows 8 usersvar queryWindows = new Parse.Query(Parse.Installation);queryWindows.equalTo('deviceType', 'winrt'); Parse.Push.send({

where: queryWindows, data: {

alert: "Your suitcase has been filled with tiny glass!" }

});

// Notification for Windows Phone 8 usersvar queryWindowsPhone = new Parse.Query(Parse.Installation);queryWindowsPhone.equalTo('deviceType', 'winphone'); Parse.Push.send({

where: queryWindowsPhone,data: {

alert: "Your suitcase is very hip; very metro." }

});

WinRT端末のみに通知を送る

WindowsPhone端末のみに通知を送る

ParseQuery query = ParseInstallation.getQuery();query.whereEqualTo("channels", "suitcaseOwners");

// Notification for Android usersquery.whereEqualTo("deviceType", "android");ParsePush androidPush = new ParsePush();androidPush.setMessage("Your suitcase has been filled with tiny robots!");androidPush.setQuery(query);androidPush.sendPushInBackground();

// Notification for iOS usersquery.whereEqualTo("deviceType", "ios");ParsePush iOSPush = new ParsePush();iOSPush.setMessage("Your suitcase has been filled with tiny apples!");iOSPush.setQuery(query);iOSPush.sendPushInBackground();

Targeting by Platform

// Notification for Windows 8 usersquery.whereEqualTo("deviceType", "winrt");ParsePush winPush = new ParsePush();winPush.setMessage("Your suitcase has been filled with tiny glass!");winPush.setQuery(query);winPush.sendPushInBackground();

// Notification for Windows Phone 8 usersquery.whereEqualTo("deviceType", "winphone");ParsePush wpPush = new ParsePush();wpPush.setMessage("Your suitcase is very hip; very metro.");wpPush.setQuery(query);wpPush.sendPushInBackground();

// Notification for Android usersvar androidPush = new ParsePush();androidPush.Alert = "Your suitcase has been filled with tiny robots!";androidPush.Query =

from installation in ParseInstallation.Query where installation.Channels.Contains("suitcaseOwners") where installation.DeviceType == "android" select installation;

await androidPush.SendAsync();

// Notification for iOS usersvar iOSPush = new ParsePush();iosPush.Alert = "Your suitcase has been filled with tiny apples!";iosPush.Query =

from installation in ParseInstallation.Query where installation.Channels.Contains("suitcaseOwners") where installation.DeviceType == "ios" select installation;

await iosPush.SendAsync();

// …….

PFQuery *query = [PFInstallation query];[query whereKey:@"channels" equalTo:@"suitcaseOwners"];

// Notification for Android users[query whereKey:@"deviceType" equalTo:@"android"];PFPush *androidPush = [[PFPush alloc] init];[androidPush setMessage:@"Your suitcase has been filled with tiny robots!"];[androidPush setQuery:query];[androidPush sendPushInBackground];

// Notification for iOS users[query whereKey:@"deviceType" equalTo:@"ios"];PFPush *iOSPush = [[PFPush alloc] init];[iOSPush setMessage:@"Your suitcase has been filled with tiny apples!"];[iOSPush setChannel:@"suitcaseOwners"];[iOSPush setQuery:query];[iOSPush sendPushInBackground];

// ……

Pushのスケジューリング

var query = new Parse.Query(Parse.Installation);query.equalTo('user_id', 'user_123'); Parse.Push.send({

where: query, data: {

alert: "You previously created a reminder for the game today" }, push_time: new Date(2015, 5, 10)

}, {success: function() {

// Push was successful }, error: function(error) {// Handle error

}});

2015年5月10日に、通知を送るようにスケジュールする

Cloud Code

PUSHは、クラウドからの送出が推奨されている。Parseでは、デフォールトで利用可能なParse Objectを通じた、Parse Cloudのデータベース利用以外にも、明示的にクラウド側のコードを設定できる。

Cloud Codeのセットアップ

$ parse new MyCloudCodeEmail: [email protected]

Password:

1:MyApp

Select an App: 1

$ cd MyCloudCode

-config/global.json

-cloud/main.js

-public/index.html

Cloud Codeのdeploy

Parse.Cloud.define("hello", function( request, response ) {

response.success("Hello world!");}

);

$ parse deploy

curl -X POST \-H "X-Parse-Application-Id: …. " \-H "X-Parse-REST-API-Key: …." \

-H "Content-Type: application/json" \-d '{}' \

https://api.parse.com/1/functions/hello

Cloud上の関数に渡される二つの引数

request

params

user

response

success

error

Cloud Codeの実行 run

Parse.Cloud.run('hello', {}, {

success: function(result) {// result is 'Hello world!'

}, error: function(error) {}

});

ParseCloud.callFunctionInBackground("hello", new HashMap<String, Object>(), new FunctionCallback<String>() {

void done(String result, ParseException e) {if (e == null) {

// result is "Hello world!”}

}});

var result = await

ParseCloud.CallFunctionAsync<IDictionary<string, object>>("hello", new Dictionary<string, object>()

);// result is "Hello world!"

Cloud Codeの実行

[PFCloud callFunctionInBackground:@"hello" withParameters:@{}

block:^(NSString *result, NSError *error) {if (!error) {

// result is @"Hello world!" }

}];

Cloud Codeサンプル

Parse.Cloud.define("averageStars", function( request, response ) {

var query = new Parse.Query("Review"); query.equalTo("movie", request.params.movie);query.find({

success: function(results) { var sum = 0;for (var i = 0; i < results.length; ++i) {

sum += results[i].get("stars"); } response.success(sum / results.length);

},error: function() {

response.error("movie lookup failed");}

});});

映画のレビューの平均値を計算する

sendPushToUser

Parse.Cloud.define(“sendPushToUser”, function(request, response) {

var senderUser = request.user;var recipientUserId = request.params.recipientId;var message = request.params.message;// メッセージを送っていいのかのチェックをする (友達にのみ送る)// ユーザーは、友達の情報を持っているif (senderUser.get("friendIds").indexOf(recipientUserId) === -1) {

response.error("The recipient is not the sender's friend, cannot send push.”

);} // メッセージが140文字以内かのチェックをするif (message.length > 140) {// 長すぎたら切り詰めて後ろに“...”を置く

message = message.substring(0, 137) + "..."; }

友人にショートメッセージを送る

// 受け取り手の送り先を調べるvar recipientUser = new Parse.User();recipientUser.id = recipientUserId;var pushQuery = new Parse.Query(Parse.Installation); pushQuery.equalTo("user", recipientUser);

// Push通知を行うParse.Push.send({

where: pushQuery,data: {

alert: message}

}).then(function() {response.success("Push was sent successfully.")

}, function(error) {response.error("Push failed to send with error: " + error.message);

});});

友人にショートメッセージを送る

ショート・メッセージの送信

[PFCloud callFunctionInBackground:@"sendPushToUser" withParameters:@{@"recipientId": userObject.id, @"message": message}

block:^(NSString *success, NSError *error) {if (!error) { // Push sent successfully }

}];

HashMap<String, Object> params = new HashMap<String, Object>();params.put("recipientId", userObject.getObjectId());params.put("message", message);ParseCloud.callFunctionInBackground("sendPushToUser", params,

new FunctionCallback<String>() {void done(String success, ParseException e) {

if (e == null) {// Push sent successfully

}}

});

Cloud Code Trigger

Cloud Codeは、Cloud Code Trigger として、データの妥当性チェックや、データの再フォーマット、例外の処理等に利用できる。AOP的な使い方。

beforeSave Triggers

Parse.Cloud.beforeSave("Review", function(request, response) {

if (request.object.get("stars") < 1) {response.error("you cannot give less than one star");

} else if (request.object.get("stars") > 5) { response.error("you cannot give more than five stars");

} else {response.success();

}});

Parse.Cloud.beforeSave(Parse.User, function(request, response) {

if (!request.object.get("email")) {response.error("email is required for signup");

} else { response.success();

}});

妥当性チェック:星の数は、1から5まで

妥当性チェック:サインアップには、e-mailが必要

保存時にデータを変更する

Parse.Cloud.beforeSave("Review", function(request, response) {

var comment = request.object.get("comment");if (comment.length > 140) {

// Truncate and add a ... request.object.set("comment", comment.substring(0, 137) + "...");

} response.success();

});

コメントの長さを、140文字以内に切り詰めてから保存する

afterSave Triggers

Parse.Cloud.afterSave("Comment", function(request) {

query = new Parse.Query("Post"); query.get(request.object.get("post").id, {

success: function(post) { post.increment("comments"); post.save();

}, error: function(error) {

console.error("Got an error " + error.code + " : " + error.message); }

});});

保存後に、コメント数を、1つ増やす。

beforeDelete Triggers

Parse.Cloud.beforeDelete("Album", function(request, response) {

query = new Parse.Query("Photo"); query.equalTo("album", request.object.id); query.count({

success: function(count) {if (count > 0) {

response.error("Can't delete album if it still has photos.");} else {

response.success(); }

}, error: function(error) {

response.error("Error " + error.code + " : " + error.message + " when getting photo count.");

}});

});

写真が含まれているアルバムは削除しない

afterDelete Triggers

Parse.Cloud.afterDelete("Post", function(request) {

query = new Parse.Query("Comment");query.equalTo("post", request.object.id); query.find({

success: function(comments) { Parse.Object.destroyAll(comments, {

success: function() {}, error: function(error) {

console.error("Error deleting related comments " + error.code + ": " + error.message);

}});

}, error: function(error) {

console.error("Error finding related comments " + error.code + ": " + error.message);

} });});

全コメント削除後の、処理

Web技術の新しい展開

Facebookのモバイル・アプリ開発技術は、ダイナミックに進化している。Parse + React, さらには、ReactのReact Nativeへの進化にも目が離せない

Part III

Part IIIWeb技術の新しい展開

MVCとFlux

Reactとは何か?

React を、サンプルから学ぶ

Parse + React

Mutating Parse Data

React Native

Relay and GraphQL

MVCとFlux

FluxでのMVCモデルの見直しは、Reactの前身と言っていいもの。データの流れは、双方向ではなく、一方向のものになる。https://facebook.github.io/flux/docs/overview.html

Flux: Structure and Data Flow

Flux: Structure and Data Flow

Flux

Reactとは何か?

Reactは、ユーザー・インターフェースを構築するためのJavaScriptライブラリーである。

Reactは、UIのみにフォーカス

多くの人は、ReactをMVCのVとして利用している。Reactは、Viewのみにフォーカスしていて、その他のテクノロジーについては、いかなる前提も行っていないため、既存のプロジェクトに容易に取り入れることができる。

Reactは、DOMを必要としない

Reactは、DOMという考え方を必要としていない。そのことで、プログラミング・モデルは単純になり、 パフォーマンスも向上する。

Reactは、Nodeサーバー上でのレンダリングも可能である。また、React Nativeを使ってネイティブ・アプリにパワーを与えることができる。

Reactでは、データは一方向に流れる

React は、一方向のreactiveなデータの流れを実装している。これによって、定型的なコードの繰り返しを少なくし、伝統的なデータのバインディングをわかりやすくする。

React を、サンプルから学ぶ

Reactでは、render メソッドの返り値に、直接、HTMLライクなタグが書ける。わざわざ、Templateを書く必要もない。それは、とても分かりやすい。

Reactの Hello World! サンプル

var CommentBox = React.createClass({

render: function() {

return (

<div className="commentBox">

Hello, world! I am a CommentBox.

</div>

);

}

});

React.render( <CommentBox />,

document.getElementById(‘content’) );// renderの第二引数は、この描画が行われるマウントポイントである。

renderメソッドとJSX記法

React コンポーネントは、入力データをとり、表示されるものを返す render() メソッドを実装する。次の例では、XMLに似た、JSXと呼ばれるシンタックスを利用している。コンポーネントに渡される入力データは、renderから、this.props.を通じてアクセスできる・

var HelloMessage = React.createClass({ render: function() {

return <div>Hello {this.props.name} </div>; }

});React.render(<HelloMessage name="John" /> , mountNode);

var HelloMessage = React.createClass({displayName: "HelloMessage", render: function() {

return React.createElement(“div”, null, “Hello ”,this.props.name);

}});

React.render(React.createElement(HelloMessage, {name: "John"}), mountNode);

JSXはオプション

JSXはオプションで、Reactを使う上では必須ではない。次のようにかける。

コンポーネントを組み合わせるvar CommentList = React.createClass({

render: function() {

return (

<div className="commentList">

Hello, world! I am a CommentList.

</div>

);

}

});

var CommentForm = React.createClass({

render: function() {

return (

<div className="commentForm">

Hello, world! I am a CommentForm.

</div>

);

}

});

コンポーネントを組み合わせるvar CommentList = React.createClass({

render: function() {

return (

<div className="commentList">

Hello, world! I am a CommentList.

</div>

);

}

});

var CommentForm = React.createClass({

render: function() {

return (

<div className="commentForm">

Hello, world! I am a CommentForm.

</div>

);

}

});

var CommentBox = React.createClass({

render: function() {

return (

<div className="commentBox">

<h1>Comments</h1>

<CommentList />

<CommentForm />

</div>

);

}

});

このCommentBoxコンポーネントは、内部で、CommentListとCommentFormというコンポーネントを利用している。クラス名は、JSXのタグの名前として利用できる。

コンポーネントのプロパティthis.propsを利用する

var Comment = React.createClass({

render: function() {

return (

<div className="comment">

<h2 className="commentAuthor">

{this.props.author}

</h2>

{this.props.children}

</div>

);

}

});

JSX内部の、{...}に囲まれた部分は、JavaScriptの値が展開される。this.propを利用すると、コンポーネントのプロパティの値が利用できる。

コンポーネントのプロパティを利用するvar CommentList = React.createClass({

render: function() { return (

<div className="commentList"><Comment author="Pete Hunt">

This is one comment</Comment><Comment author="Jordan Walke">

This is *another* comment</Comment>

</div> );

} });

二つのCommentコンポネントが利用されている。最初のコンポーネントでは、this.prop.authorは、“Pete Hunt”で二つ目のコンポーネントでは、その値は、“Jordan Walker”になる。

データ・モデルと結びつける

var CommentList = React.createClass({render: function() { var commentNodes = this.props.data.map(function (comment) { return ( <Comment author= {comment.author}>

{comment.text} </Comment> );

}); return ( <div className="commentList"> {commentNodes} </div>

);} });

ここでは、renderの内部で、もう一つの関数 commentNodesが定義されているこの関数では、mapで、複数個のエントリを持つリスト・データが処理される。

データ・モデルと結びつける

var data = [ {author: "Pete Hunt", text: "This is one comment"}, {author: "Jordan Walke", text: "This is *another* comment"}

];

var CommentBox = React.createClass({ render: function() { return (

<div className="commentBox"> <h1>Comments</h1> <CommentList data= {this.props.data} /> <CommentForm />

</div> ); } });

React.render( <CommentBox data= {data} />, document.getElementById('content') );

状態を持つコンポーネント

var CommentBox = React.createClass({ getInitialState: function() { return {data: []}; },render: function() { return (

<div className="commentBox"> <h1>Comments</h1> <CommentList data= {this.state.data} /> <CommentForm />

</div> );

} });

Reactでは、propsとstateは、区別されている。stateの変更は、renderの再描画を引き起こす。

状態を持つコンポーネントvar Timer = React.createClass({

getInitialState: function() { // 状態の初期値の設定return {secondsElapsed: 0};

}, tick: function() { // 状態が変わるとrenderが呼ばれる

this.setState({secondsElapsed: this.state.secondsElapsed + 1}); },componentDidMount: function() {

this.interval = setInterval(this.tick, 1000); }, componentWillUnmount: function() {

clearInterval(this.interval); }, render: function() {

return ( // 状態は、this.state でアクセスできる<div>Seconds Elapsed: {this.state.secondsElapsed}</div> );

}});

React.render(<Timer />, mountNode);

var TodoList = React.createClass({ render: function() {

var createItem = function(itemText, index) { return <li key={index + itemText}>{itemText}</li>;

}; return <ul>{this.props.items.map(createItem)}</ul>; }

});

var TodoApp = React.createClass({getInitialState: function() { return {items: [], text: ''}; }, onChange: function(e) { this.setState({text: e.target.value});}, handleSubmit: function(e) {

e.preventDefault(); var nextItems = this.state.items.concat([this.state.text]); var nextText = ''; this.setState({items: nextItems, text: nextText});

},

Reactサンプル ToDo アプリ

render: function() { return (

<div> <h3>TODO</h3> <TodoList items={this.state.items} /> <form onSubmit={this.handleSubmit}>

<input onChange={this.onChange} value={this.state.text} /> <button>{'Add #' + (this.state.items.length + 1)}</button>

</form></div>

); }

});

React.render(<TodoApp />, mountNode);

Parse + React

Parse + React は、React からParse APIへの簡単なアクセスを提供する Parse JS SDK上のインターフェース層である。https://github.com/ParsePlatform/ParseReact

Parse + React とは?

Parse + React は、 Reactのコンポーネントを Parseの queryにsubscribe させて、データの変化を、Flux-styleで伝えることを可能にする。

オブジェクトの生成や変更の際に、これらのコンポーネントが、バックグラウンドで、自動的に更新されるように、これらのsubscriptionは、管理される。

こうしてユーザーインターフェースは、機敏で反応良いものになることができる。

Parse DataへのSubscribe

Parse dataをReactアプリケーションに持ち込む一番いいやり方は、ReactのコンポーネントをParseのqueryにsubscribeすることである。

Parse Queryへのsubscribeには、observeメソッドが利用される。

コンポーネントがマウントすると、queryは変わったり、明示的にリフレッシュされる。その度に、新しいデータがParseから取り出されて、コンポーネントに渡され、renderによるコンポーネントの再描画 が引きおこされる。

queryの更新

コンポーネントがマウントした時に、それぞれのqueryが取り込まれる。それらの結果が受け取られ、コンポーナントに付け加えられた時にはいつも、コンポーネントは更新され、再描画される。

もし、propsやstateが変わっていれば、subscriptionは全て再計算される。結果として変化したqueryは、すべて、再取り込みされる。

queryは、いつでも、this.refreshQueries()を呼ぶことで、明示的にリフレッシュされる。

observe メソッド

ParseReact.Mixinオブジェクトを mixinのコンポーネントのリストに追加することで、observe() ライフサイクル関数のサポートが得られるようになる。

新しく提案された React のlifecycleメソッドであるobserve() によって、コンポーネントは queryに、subscribe される。

この observeメソッドは、コンポーネントが更新するたびに、描画の直前に走る。

直近の propsとstateをパラメーターとして受け取って、observeは、Parse.Queryオブジェクトを構成して、queryに対する文字列のmapを返す。この文字列のキーは、それぞれのqueryを同定する名前として利用される。

observe メソッド

observe()は、コンポーネントが更新される時、描画の前に呼ばれる。

マウント時には、初期値のpropsとstateが関数のパラメーターとして渡される。その後は、最新のpropsとstateがパラメーターとして渡される。それは、このメソッドが、componentWillUpdateから呼ばれているからである。

このメソッドは、key/valueのペアからなるオブジェクトを返す。それぞれのkeyは、subscriptionの名前を表し、そのvalueは、Parse.QueryかLocal Subscriptionである。

subscribe sample

この例では、コンポーネントを、50票以上の投票を集めたコメントを、生成日の順に並べた query に subscribe している。それは、comments という名前に関連付けられているので、この結果の集合は、 this.data.comments として利用可能になる。.

observe: function( props, state) {return {

comments: (new Parse.Query('Comment')) .greaterThan('votes', 50) .ascending('createdAt')

}; }

Local Subscription

コンポーネントを、もっとローカルなコンセプトに subscribe

することも可能である。コンポーネントは、ParseReact.currentUser を通じて、ログインしログアウトする現在のユーザーに subscribe することができる。次の例では、this.data.user は、現在のユーザーのコピーに等しくなるだろう。コンポーネントは、ユーザーが変更された時に更新される。

observe: function() { return {

user: ParseReact.currentUser};

}

observe samplevar CommentBlock = React.createClass({

mixins: [ParseReact.Mixin], // queryへの subscriptionを可能にするobserve: function() {

// 生成順に並べられた全てのCommentオブジェクトにsubscribeする// その結果は、this.data.comments で利用出来るreturn {

comments: (new Parse.Query('Comment')).ascending('createdAt') }; },

render: function() { // それぞれのcommentをリストのアイテムとして描画するreturn (

<ul> {this.data.comments.map(function(c) {

return <li>{c.text}</li>; })}

</ul> );

} });

このコンポーネントがマウントされた時にはいつも、queryが発行されて、その結果がthis.data.comments に加えられる。

queryが再発行されるか、queryにマッチしたオブジェクトがローカルに変更されるたびに、それは、こうした変化を反映するように、自分自身を更新する。

Mutating Parse Data

Parse + React では、Parseと共有されるデータは、Mutation を通じて変更される。Mutationは、dispatchされると Parse APIとクライアントのアプリの両方に、なんらかの方法でデータを更新せよと伝えるメッセージである。 https://goo.gl/EPhLPy

mutation.dispatch([options])

mutation.dispatchは、生成・削除・変更といったParseオブジェクトの変化を、Parse APIにリクエストを行うことで実行する。

サーバーからのレスポンスを待つことが、明示的に要求されない場合、オブジェクトはローカルに楽観的に更新される。オブジェクトが更新された場合、新しいバージョンが、subscribeされた全てのコンポーネントにプッシュされる。

dispatch は、Parse.Promiseを返す。このpromiseは、サーバーのリクエストが完了した時に、成功裡に解決される。 エラーが起きた場合には、このpromiseは、リジェクトされる。

Data mutation

データの変化 mutationはFluxのActionのやり方でdispatch される。それで、多くの異なるコンポーネント同士が話しかけるviewを要求することなしに、更新を同期することが可能になる。標準的なParseのデータの変化は、全てサポートされている。

// Create a new Comment object with some initial data

ParseReact.Mutation.Create('Comment', { text: 'Parse <3 React’

}).dispatch();

Mutationを生成するためには、最初に適当なコンストラクタを呼ばなければならない。これらのメソッドは、Mutations APIで見ることができる。いったん Mutation が生成されれば、それは、dispatch()を呼ぶことで実行される。

// Create a new Pizza objectvar creator = ParseReact.Mutation.Create('Pizza’, {

toppings: [ 'sausage', 'peppers' ], crust: 'deep dish'

});

// ...and execute itcreator. dispatch();

多くのMutationでは、変更されるべき特定のオブジェクトを自由に選べる。それは、Query Subscriptio から受け取るオブジェクトであるべきである。次の例は、queryから受け取ったオブジェクトをどのように更新すべきかを示している。

React.createClass({mixin: [ParseReact.Mixin], observe: function() {

return { counters: new Parse.Query('Counter');

}; }, // ... // _myClickHandler は、コンポーネントがクリックされた時に呼び出されると// としよう。この時、すべてのCounterオブジェクトの値を増やす。_myClickHandler: function() {

this.data.counters.map(function(counter) {ParseReact.Mutation.Increment(counter, 'value').dispatch();

}); }

})

// objectIdが ‘c123’であるCounterを削除したい時は、// 次のように、情報をMutationに渡せば、削除ができる

var target = { className: 'Counter', objectId: 'c123' }; ParseReact.Mutation.Destroy(target).dispatch();

queryのsubscriptionから受け取ったものでも、親オブジェクトから受け取ったpropでも、コンポーネントが受け取ったオブジェクトを変更するのは容易である。

直接は見ることのできないオブジェクトでも、それを変更することは可能である。そのオブジェクトのclassNameとobjectIdを知っていれば、これらのフィールドをmutationに渡せば、それを修正することが出来る。

var creator = ParseReact.Mutation.Create('Pizza', { toppings: [ 'sausage', 'peppers' ], crust: 'deep dish'

});

// 同じトッピングで、三つの新しいピザを作るcreator. dispatch(); creator. dispatch(); creator. dispatch();

Mutationを、わざわざdispatchする一つの理由は、Mutationを独立の操作として、後の実行のために保存したり、複数回の実行を可能にするためである。

React Native

https://github.com/facebook/react-native

React Native

React Native は、JavaScriptとReactベースの一貫した開発者の経験を利用して、ネイティブなプラットフォーム上で、世界クラスのアプリを構築することを可能とする。

React Natieのフォーカスは、開発者が関心を持つ全てのプラットフォームに渡って、「一度学べば、どこでも書ける」という、開発者の効率性に置かれている。

Facebookは、React Native を複数の製品版のアプリに利用しており、これからも、React Nativeに投資を続けていくだろう。

Native iOS Components

React Nativeで、iOS上での UITabBar やUINavigationControllerといった標準的なプラットフォームのコンポーネントを利用できる。

このことで、アプリケーションは、プラットフォームのその他の部分とのルック・アンド・フィールは整合的になり、アプリの品質の水準は、高いものに維持できる。

これらのコンポーネントは、それらのReactコンポーネントでの対応物である TabBarIOSやNavigatorIOSを利用することで、容易にアプリと一体化する。

TabBarIOS, NavigatorIOSの利用

var React = require('react-native');var { TabBarIOS, NavigatorIOS } = React;var App = React.createClass({

render: function() {return (<TabBarIOS><TabBarIOS.Item title="React Native" selected={true}>

<NavigatorIOS initialRoute={{ title: 'React Native' }} /> </TabBarIOS.Item>

</TabBarIOS>);

},});

非同期実行

JavaScriptアプリ・コードとナイティブなプラットフォームの間の操作は、全て、非同期で実行される。ネイティブなモジュールは、追加のスレッドも利用できる。このことは、メインスレッドとは別のスレッドで画像のデコードをしたり、バックグラウンドでディスクに保存したり、UIを組上げなくても、テキストの評価やレイアウトの計算等々が可能となることを意味している。その結果、React Nativeアプリは、自然になめらかに動き、反応も良いものになる。

コミュニケーションも完全にシリアライザブルなので、JavaScriptのデバッグを、完全なアプリを、シミュレーターでも実機でも実際に動かしながら、 Chrome Developer Toolsで行うことが可能になる。

Touch Handling

iOSは、Web上では一般的な対応物がないような、複雑なviewの階層でのタッチとやりとりする、非常に強力なResponder Chain というシステムを持っている。React Nativeは、同様のresponderシステムを実装し、一切設定を追加することなく、スクロール・ビューやその他のエレメントと統合される、TouchableHighlightといった高レベルのコンポーネントを提供している。

var React = require('react-native');var { ScrollView, TouchableHighlight, Text } = React;var TouchDemo = React.createClass({

render: function() {return (

<ScrollView><TouchableHighlight onPress={() => console.log('pressed')}>

<Text>Proper Touch Handling</Text> </TouchableHighlight>

</ScrollView>);

},});

Flexbox and Styling

viewのレイアウトは、容易なものでなければならない。それが、我々がWebからReact Native に、flexbox レイアウトを導入した理由である。flexlayoutは、マージンやパディングを持った積み重ねられ入れ子にされたboxのような、もっとも一般的なUIのレイアウトを構築するのを簡単にする。

React Native は、fontWeightのような、Webと共通のstyleをサポートしている。StyleSheetという抽象は、スタイルやレイアウトを、それを用いるコンポーネントと一緒に宣言し、また、内部でそれを適用するのに最適化されたメカニズムを提供している。

var React = require('react-native');var { Image, StyleSheet, Text, View } = React;var ReactNative = React.createClass({

render: function() {return (

<View style={styles.row}><Image

source={{uri: 'http://facebook.github.io/react/img/logo_og.png'}} style={styles.image}

/> <View style={styles.text}>

<Text style={styles.title}> React Native </Text> <Text style={styles.subtitle}>

Build high quality mobile apps using React </Text> </View>

</View> );

},});var styles = StyleSheet.create({

row: { flexDirection: 'row', margin: 40 }, image: { width: 40, height: 40, marginRight: 10 }, text: { flex: 1, justifyContent: 'center'}, title: { fontSize: 11, fontWeight: 'bold' }, subtitle: { fontSize: 10 },});

var React = require('react-native');var { Text } = React;var GeoInfo = React.createClass({

getInitialState: function() {return { position: 'unknown' };

}, componentDidMount: function() {

navigator.geolocation.getCurrentPosition((position) => this.setState({position}), (error) => console.error(error)

); }, render: function() {

return (<Text>

Position: {JSON.stringify(this.state.position)} </Text>

); },

});

Extensibility

React Nativeを使えば、一行もネイティブ・コードを書くことなしに、立派なアプリを作れることは確かである。しかし、React Nativeは、カスタムのネイティブ・ビューやモジュールを容易に拡張できるように設計されている。このことは、すでに作ったものは全て再利用可能で、好みのネイティブ・ライブラリーをインポートして利用できることを意味する。

iOSで単純なモジュールを作るためには、RCTBridgeModule プロトコルを実装した新しいクラスを作って、JavaScriptで利用可能にしたい関数を、RCT_EXPORT_METHOD の中にラップすればいい。さらに、クラス自身は、 RCT_EXPORT_MODULE()で、明示的にexportすればいい。

// Objective-C#import "RCTBridgeModule.h”

@interface MyCustomModule : NSObject <RCTBridgeModule>@end

@implementation MyCustomModuleRCT_EXPORT_MODULE();

// Available as NativeModules.MyCustomModule.processStringRCT_EXPORT_METHOD(processString:(NSString *)input callback:(RCTResponseSenderBlock)callback){

callback(@[[input stringByReplacingOccurrencesOfString:@"Goodbye" withString:@"Hello"]]);

}

@end

// JavaScriptvar React = require('react-native');var { NativeModules, Text } = React;var Message = React.createClass({

getInitialState() {return { text: 'Goodbye World.' };

},componentDidMount() {

NativeModules.MyCustomModule.processString(this.state.text, (text) => {

this.setState({text}); });

},render: function() {

return ( <Text>{this.state.text}</Text>

);},

});

Custom iOS viewsは、 RCTViewManagerのサブクラス化し、-viewメソッドを実装し、 RCT_EXPORT_VIEW_PROPERTYマクロでエクスポートすることで、外部から利用できるようになる。その後、JavaScriptで requireNativeComponentを用いて、アプリのコンポーネントを利用できるようにする。

// Objective-C#import "RCTViewManager.h”

@interface MyCustomViewManager : RCTViewManager@end

@implementation MyCustomViewManager- (UIView *)view{

return [[MyCustomView alloc] init];}

RCT_EXPORT_VIEW_PROPERTY(myCustomProperty, NSString);@end

// JavaScriptvar React = require('react-native');var { requireNativeComponent } = React;class MyCustomView extends React.Component {

render() {return <NativeMyCustomView {...this.props} />;

}}

MyCustomView.propTypes = {myCustomProperty: React.PropTypes.oneOf(['a', 'b']),};

var NativeMyCustomView = requireNtiveComponent('MyCustomView', MyCustomView);module.exports = MyCustomView;

Relay and GraphQL

“Introducing Relay and GraphQL”

http://goo.gl/6NCMgS

Relayとは何か?

Relayは、Reactアプリケーションに、データを取得する機能を提供する Facebookの新しいフレームワークである。それは、2015年1月のReact.js Conf で、発表された。

それぞれのコンポーネントは、GraphQLと呼ばれる検索言語を用いて、自身のデータの従属性を宣言的に指定する。データは、 this.props上のプロパティを通じて、 コンポーネントから利用可能になる。

開発者が、これらのReactコンポーネントを自然に組み合わせると、Relayは、データのクエリーを効率的なバッチへと組み合わせ、それぞれのコンポーネントには要求された正確なデータだけを(それ以上のことはしない)提供し、データが変化した時にはこれらのコンポーネントを更新し、クライアント側の全てのデータストア(キャッシュ)管理の面倒を見る。

The Relay Architecture

GraphQLとは何か?

GraphQL は、現代のアプリケーションの、複雑で、ネストしたデータの従属性を記述するために設計されたデータ検索言語である。Facebookのネイティブ・アプリでは、この数年の間、製品版に利用されてきた。

我々は、サーバー上でGraphQLシステムを、クエリーを、下位のデータ・フェッチ・コードにマップするように設定する。この設定の層は、GraphQLが任意の下位のストレージ・メカニズムと動作することを可能とする。

Relayは、GraphQL を検索言語として利用するが、それは、GraphQLの特定の実装に結びつけられてはいない。

GraphQL Query サンプル

{

user(id: 3500401) {

id,

name,

isViewerFriend,

profilePicture(size: 50) {

uri,

width,

height

}

}

}

GraphQL Response サンプル

{

"user" : {

"id": 3500401,

"name": "Jing Chen",

"isViewerFriend": true,

"profilePicture": {

"uri": "http://someurl.cdn/pic.jpg",

"width": 50,

"height": 50

}

}

}

The value proposition

Relayは、Facebookで大規模なアプリケーションを構築してきた我々の経験から生まれた。我々の主要な目的は、正確で高いパフォーマンスのアプリを、開発者が、真直ぐで明確なやり方で構築することを可能とすることである。そのデザインは、大規模なチームでも、高い程度での分離と確信を持って変更を行うことを可能にする。データの取り込みは難しく、常に変化するデータを扱うことは難しく、また、パフォーマンスも難しい。Relayは、こうした問題を単純なものに還元し、トリッキーな部分をフレームワークに移し、開発者がアプリケーションに集中できるように自由にさせる。

クエリーをビューのコードと一緒に並べること(Co-location)で、開発者は、それらを別々に見ることで、コンポーネントが何をしようとしているのかを推察できる。それを理解するためには、コンポーネントが描画されるコンテキストを考える必要はない。コンポーネントは、親コンポーネントやデータの準備をするサーバーコードの修正のカスケードを適用したりすることなしに、描画の階層の中のどんなところへも移動できる。

Relay sample Co-location

// Story.react.js

class Story extends React.Component {

render() {

var story = this.props.story;

return (

<View>

<Image uri={story.author.profile_picture.uri} />

<Text>{story.author.name}</Text>

<Text>{story.text}</Text>

</View>

);

}}

module.exports = Story;

// Story.react.js

class Story extends React.Component { ... }

module.exports = Relay.createContainer(Story, {

queries: {

story: graphql`

Story {

author {

name,

profile_picture { uri }

},

text

}

`

}

});

Co-location は、開発者を、「成功の陥穽」に陥るように導く。というのも、まさに望んだデータが手に入り、また、望んだデータは、それが利用されるところのすぐ近くで、はっきりと定義されているからである。 このことは、パフォーマンスはデフォールトで(間違ってもデータを取りすぎることはほとんどなくなる)、コンポーネントはもっと頑健なものになることを意味する。 (データの取り損ねは、同じ理由でありそうもなくなるし、コンポーネントは、なくなったデータを描画しようとしたり、実行時に大量のデータで爆発が起きたりしない)

Relayは、開発者が、変わらないこと、すなわちコンポーネントは要求された全てのデータが利用可能になるまで描画はしないということを維持することで、開発者に予測可能な環境を提供する。さらに、クエリーは、静的に定義されていて(すなわち、描画の前に、コンポーネント・ツリーのクエリーを引き出すことができる)、GraphQLのスキーマは、どのクエリーが妥当であるかの正当化された記述を提供しているので、開発者が間違った時には、クエリーの妥当性を早くチェックして、すぐに間違いに気づくことができる。

たとえ、他のフィールドが知られていてストアにキャッシュされていても(他のコンポーネントがそれらを要求しているから)、コンポーネントが明示的に要求しているオブジェクトのフィールドのみが、このコンポーネントでアクセス可能である。このことは、システムに、気づかないデータの従属性のバグが存在することを不可能にする。全てのデータ取得を一つの抽象を通じて処理することで、そうでなければ、アプリのいたるところで繰り返し扱わなければいけなくなるだろう山ほどの問題を、処理することができる。

Performance

パフォーマンス: 全てのクエリーは、フレームワークのコードを通じて流れる。そこでは、そうでなければ、不十分で繰り返しになるだろうクエリー・パターンが、自動的に崩れて、効率的で最小のクエリーにまとめられる。同様に、フレームワークは、どのデータが以前に要求されていて、あるいは、どのリクエストが現在実行中であるかを知っているので、クエリーは自動的に重複を避けて、最小のクエリーが生み出されることが可能となる。

Subscriptions

Subscriptions: 全てのデータは、一つのストアに流れる。ストアから読まれたものは全てフレームワークを経由する。このことは、フレームワークはどのコンポーネントがどのデータを気にしていて、データが変わった時には、何が再描画されるべきかを知っていることを意味する。コンポーネントは、個別にsubscriptionを設定すべきではない。

Common patterns

Common patterns: 共通なパターンを簡単に作ることができる。カンファレンスでJingが与えたページ処理はその例である。最初に10個のレコードを持っていて、次のページでは、全体で15個のレコードを表示したいと宣言するとしよう。フレームワークは、現在持っているデータと必要なデータの差分を把握して、自動的に最小のクエリーを構築し、それを要求し、データが利用可能いなった時、ビューを再描画する。

Simplified server implementation

Simplified server implementation: (actionやrouteごとに)たくさんのエンドポイントを増殖させるよりも、一つのGrapfQLのエンドポイントで、下層のたくさんのリソースの一つの正面口としてサービスを提供できる。

Uniform mutations

Uniform mutations: mutation(write)を実行するための整合的なパターンが一つある。それは、概念的にはデータ・クエリー・モデル自身と一体のものである。mutationは、副作用があるクエリーと考えることができる。実行されるべき変化(レコードにコメントを追加するといった)を記述するいくつかのパラメーターを与えて、mutationが終わった後でビューを更新するのに必要なデータを指定するクエリー(レコード上のコメントのカウントなど)を行い、データは通常のフローを用いてシステムの中を流れる。クライアント上で、直ちに「楽観的」な更新を行うことができ、最終的には、それをコミットし、エラーの場合には、リトライやロールバックが可能となる。

How does it relate to Flux?

ある意味で、RelayはFluxにインスパイアされたものである。しかし、メンタル・モデルは、もっと簡単である。複数のストアの代わりに全てのGraphQLデータをキャッシする一つのストアが中央にある。明示的なsubscriptionの代わりに、フレームワーク自身が、どのデータをコンポーネントが要求し、どのコンポーネントがデータが変わった時に更新されるべきか解析する。actionの代わりに、modificationが、mutationの形をとる。Facebookでは、アプリは、Fluxを使って作られているか、Relayを使って作られているか、その両方を使っているかのどちらかである。一つのよく使われるようになっているパターンは、アプリの大量のデータフローの処理にはRelayを使い、アプリの状態の管理にFluxを使うというものである。

Open source plans

現在 GraphQL(スペックと参照実装)とReray両方の公開に向けて、全力で取り組んでいる。具体的な日程については未定だが、我々は、これらを公開できることに、とても興奮している。

当面は、blogポストの形で、もっともっと多くの情報を提供していきたい。オープンソース・リリースが近づくにつれ、もっと具体的で詳細な情報、シンタックスやAPI等が出てくること期待してもらっていい。

期待してほしい。

次回マルレク予告

「大規模分散システムの現在--- Titter Aurora --- 」

日 時: 6月17日 18:30-20:30

場 所: 蒲田 富士通ソリューションスクウェア

受 付: 6月10日 12:00~