SiNBLOG

140文字に入らないことを、極稀に書くBlog

grunt-contrib-requirejsを試してみた

ちょっと前から、JS力を上げようと足掻いてます

今回はその中でも以下の内容を模索してみました


1.package管理

2.ミニファイ&ファイル連結
3.ファイル監視して、自動で2をやる


1をやりたいのは、以下の様な理由から

  • そのページに必要なJSだけを読み込みたかった
  • 他にも依存関係をぱっと見で分かるようにしたかった
  • global領域を汚染しないようにしたかった

2をやりたいのは、以下の様な理由から

  • 役割毎にファイルを分けたいが、jsファイルを読み込むオーバーヘッドは極力少なくしたい

そして、RequireJSがそれを解決してくれそう


3をやりたいのは、以下の様な理由から

  • 開発時にjsファイルを修正する度に、commandやらを打つのは面倒・・・


ClosureLibraryでも上記は満たせるのだけど、流石に仕事に使うにはマニアックな気もするので、
一般的?にはどうやるのか調べてみることに

そして、Grunt: The JavaScript Task Runnerがそれを解決してくれそう


gruntjsはtwitterのTLでちらほら見かけていたので、気になっていました
この2つを組み合わせればいけるか!?などと思っていたら、grunt-contrib-requirejs - npmがあることに気付きました
因みにこいつはGrunt0.4系でないと動かないみたいです
ちょっと前にgrunt.js試した時に、0.3系入れていたので、今回VersionUp


というわけで、上記を満たすために、挑戦してみました
挑戦した結果はGithubに置いてます
GitHub - sinmetal/webproject-template at develop


まず、grunt.jsを動かすためにはNode.jsが必要なので、適当に入れます
Macだと公式ページから一発でInstallできるけど、他のOSだとどうなのかな?
以下の手順も全てMacでやったことなので、他OSだと動くのかは分かりません


次にgruntを入れます
Getting started - Grunt: The JavaScript Task Runner
公式に書いてある通り、


npm install -g grunt-cli
でいれます
Macだとsudoが必要になると思います


grunt.jsの設定ファイルをプロジェクトの直下に置きます。
Gruntfile.js
webproject-template/Gruntfile.js at develop · sinmetal/webproject-template · GitHub


module.exports = function(grunt) {

grunt.initConfig({
// 監視用の設定
watch: {
files: './src/**',
tasks: ['requirejs']
},

// requirejs用の設定
requirejs: {
compile_top: {
options: {
name : 'main', // mainで読み込むjsのpath
baseUrl: "./src/js",
mainConfigFile: './src/js/main.js',
out: "./build/toppage.js"
}
},

compile_page1: {
options: {
name : 'page1/main', // mainで読み込むjsのpath
baseUrl: "./src/js",
mainConfigFile: './src/js/page1/main.js',
out: "./build/page1.js"
}
}
},
});

//matchdepでpackage.jsonから"grunt-*"で始まる設定を読み込む
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
//grunt.loadNpmTasks('grunt-contrib-watch');
//grunt.loadNpmTasks('grunt-contrib-requirejs');
}

いくつかの設定を書いているので、ちょっとずつ説明していきます
まずは、先頭のwatch

watch: {
files: './src/**',
tasks: ['requirejs']
}
これは、ファイル監視用の設定です
grunt-watch
'./src/**'src以下のファイルに変更があった場合、'requirejs'を実行しろ!となります
僕がjsファイルをsrcの下に置いていて、requirejsのビルドのタスク名が'requirejs'なので、こうなります
これで、jsファイル修正時に、自動でビルドが走るように設定できました


次のrequirejs


// requirejs用の設定
requirejs: {
compile_top: {
options: {
name : 'main', // mainで読み込むjsのpath
baseUrl: "./src/js",
mainConfigFile: './src/js/main.js',
out: "./build/toppage.js"
}
},

compile_page1: {
options: {
name : 'page1/main', // mainで読み込むjsのpath
baseUrl: "./src/js",
mainConfigFile: './src/js/page1/main.js',
out: "./build/page1.js"
}
}
},
});

これは、grunt-contrib-requirejs - npmのための設定です
'compile_top', 'compile_page1'の2つのページ用の設定を書いています
各ページのメインとなるjsのPathをnameで指定します
name:'page1/main'で、baseUrl:'./src/js'なので、'./src/js/page1/main.js'を見に行きます
mainConfigFileが同じファイルを指しているのは、依存関係をそのファイルに書いているからです
これについては、そのファイルが出た時に説明します
out: './build/page1.js'はミニファイ&ファイル連結されたファイルが出力されるPathです
これで設定は完了です


最後にnpm installで入れる設定の一覧


//matchdepでpackage.jsonから"grunt-*"で始まる設定を読み込む
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
//grunt.loadNpmTasks('grunt-contrib-watch');
//grunt.loadNpmTasks('grunt-contrib-requirejs');
4行あるけど、実際に動いているのは1行だけ
ここの部分は、matchdepを使ってGrunt.jsのプラグインを自動ロードするを参考にさせてもらいました
npm installの設定をGruntfile.jsとpackage.jsonの両方に書くのが面倒だから、
package.jsonを見に行くようにするってのが、metchdepです
入れ方なんかは、上記のリンク先を参照してください


これで、Gruntfile.jsの設定はおしまいです。
次は、package.jsonです


{
"name": "webproject-template",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-watch": "~0.3.1",
"grunt-contrib-requirejs": "~0.4.0",
"matchdep": "~0.1.1"
}
}
ここに書いてある"grunt-*"が利用しているgruntのpluginの一覧です
Projectの直下で

npm install
すると、node_modulesというディレクトリの下にインストールされます


これで準備は完了です
ここからは実際にどんな感じで作っていくかというのを書いていきます
まず、途中で少し出てきたけど、ディレクトリ構成は以下の様な感じ


build // buildされた結果のjs
+-page1.js
-toppage.js
page1
+-index.html
src
+-js
+-lib // OSSのLibraryとか
+-jquery.js // jQuery公式からDLしてきた
-require.js // requirejs公式からDLしてきた
-module // 自分で作ったutilとか置く場所
+-fuga.js
-hoge.js
-page1 // page1固有
+-main.js
-main.js // toppageのmain.js
Gruntfile.js
index.hml
package.json


まずは、moduleの中にあるhoge.jsです


define([
'lib/jquery'
], function($) {
var Hoge = function() {};
Hoge.prototype.awesomeThod = function() {
console.log('Hellow RequireJS!');
};

Hoge.prototype.outputLog = function(text) {
console.log(text);
};

return Hoge;
});

defineで依存しているjsのpathを書きます
ここではjQueryを指定しています
・・・使ってはいませんけど
後は、定義したいメソッドやらを作っていけば、OKです


次に、moduleを呼び出しているmain.jsです


//main.js
require.config({
baseUrl: 'js',
paths: {
'jquery': 'lib/jquery.js'
}
});

require([
'module/hoge',
'module/fuga'
], function(Hoge, Fuga) {
// Hogeにはhoge.jsで定義されたコンストラクタが渡る
var hoge = new Hoge();
hoge.awesomeThod();
});

require.configが上の方に書いたmainConfigFileに指定した設定です
設定できる項目についてはhttp://d.hatena.ne.jp/maneater_rhythm/20130219/1361282887で詳しく書いてくれてます

require〜〜の部分が、処理の内容を書くところです
'module/hoge'でmodule/hoge.jsを読み込んでます
こいつの読み込みが終わると、Hogeに入れてくれます
後は使うだけ!

最後にindex.htmlです

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>Web Project Template for Sinmetal</title>
</head>
<body>
	<p>Used Libraries</p>
	<ul>
		<li><a href="http://jquery.com/">jQuery</a></li>
		<li><a href="http://requirejs.org/">RequireJS</a></li>
	</ul>
	<p>Other Page</p>
	<ul>
		<li><a href="page1/index.html">page1</a></li>
	</ul>
	<script src="src/js/lib/require.js"></script>
	<script src='build/toppage.js'></script>
</body>
</html>

require.jsと、buildされたtoppage.jsを読み込んでいます
html側は特出することはないので、これだけです


後は、commandについても紹介しておきます
上記のプロジェクトをビルドするには


grunt requirejs
でできます
これは、Gruntfile.jsに書いてある設定の名前が、requirejsだからです
デフォルトを指定しておけば、引数無しで行けるとだと思うのだけど、デフォルト設定してないので・・・

後、ファイル監視


grunt watch
こいつもGruntfile.jsに書いてある設定の名前が、watchだから
これを起動しておけば、jsに修正が入った時に、自動でrequirejsを実行してくれます


以上でgrunt-contrib-requirejsを試してみた!は終わりです
そして、ここまで読んだ聡明な方なら、気付いてしまったと思うのですが・・・テストの設定が無い
まだ、JSのテストをいまいちどうやれば良いのか分かってないので、設定できてないorz

Slim3でApachePoiを使ってExcelファイルを操作する

Slim3Excelを操作してみようと思ったので、ApachePoiが使えるのか試してみた。

ついでに、ファイルの出力先として、GoogleCloudStorageを使ってみた。

やる前にちょっとGoogle先生に聞いてたら、以下の記事を見つけました。

How to generate excel files on Google App Engine (GAE) using GWT ? | z80.fr

ApachePoiはGAEのWhiteListにないJREクラスの依存関係が多いのでいまいちという話のよう。


2010年の記事らしいですが、ちょっとドキドキしながらもチャレンジ!


public Navigation run() throws Exception {
response.setContentType("text/plain");
response.getWriter().println("Hello, world from java");

//ファイルの情報 (ExcelファイルのMimeTypeが以下で合っているのかは、ちょっと怪しい・・・
GSFileOptionsBuilder optionsBuilder =
new GSFileOptionsBuilder()
.setBucket(BUCKETNAME)
.setKey(FILENAME)
.setMimeType("application/excel")
.setAcl("public_read");

try {
//Google Cloud Storage用のファイルを作成
AppEngineFile file = fileService.createNewGSFile(optionsBuilder.build());

//ファイルに対する書き込み用チャンネルを開く。第2引数はロックをするかどうか
FileWriteChannel writeChannel = fileService.openWriteChannel(file, true);

//Poiの書き込みがOutputStreamで行われるので、チャンネルからOutputStreamを作成
OutputStream outputStream = Channels.newOutputStream(writeChannel);

//PoiでExcelファイルを作成
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("hogeSheetName");
XSSFRow row = sheet.createRow(0);
XSSFCell cell = row.createCell(0);
cell.setCellValue("testValue");
workbook.write(outputStream);

//outputStream.flush();
outputStream.close();
writeChannel.closeFinally();

} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (FinalizationException e) {
throw new RuntimeException(e);
} catch (LockException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}

response.getWriter().println("finish");
return null;
}

ふぅ、例外処理は適当だけど、とりあえず動いた。

そんなに長いコードでもないし、やっていることは、コメントに書いてあるままだ。

実際にGAE上で動かすには、GoogleCloudStorage側で、GAEからのアクセスを許可するよう設定する必要がある。

そのやり方は、Google Developer Group 中国のマネージャーの横山さんのBlogに書いてあります。

T.Yokoyamaのブログ: GAE/GでGoogle Cloud Storageを利用するには(1)

GAE/Gで作られていますが、Google API Consoleでの設定方法は同じです。

ただ、Google Cloud Storage は Billing設定が登録されていないと使えませんので、そこはご注意ください。


Localで実行した場合は、Google Cloud Storageに登録したファイルの情報がDatastoreに保存されます。

___GsFileInfo___というKindがそれです。

Fileの中身などは見れませんが、ファイル名やサイズは見れるので、何となく保存されている雰囲気が分かりますw

実際のファイルは war/WEB-INF/appengine-generated以下に保存されます。

この辺りは、Blobstoreと同じような感じですね。

以上で、終わりです。


Poiに関しては、新規ファイルを作成して、Cellに文字列を書き込んだだけなので、全部の機能が使えるかは未知数です。

何か分かった方は、是非Blogに書いてくださいね!


実際のソースは以下

Google Code Archive - Long-term storage for Google Code Project Hosting.

Slim3でModelを便利に扱う

Slim3のModelを使うと便利になることがたくさんある。
プロパティをタイプセーフに扱うのは言わずもがなだけど、他にも色々と用意してくれている!


まずは、ModelMetaの機能として、以下のような感じ。


modelToJson()
jsonToModel()

modelsToJson()
jsonToModels()

上記はModelとJSONの変換をやってくれるメソッド達。
Modelのgetter/setterがあるフィールドを、相互に変換してくれる。
KeyをBase64で変換してくれたりする心遣いもある。


次はDatastoreクラスの機能。

DatastoreクラスはSlim3でDatastoreにアクセスするために利用するクラスだけど、
filterInMemory, sortInMemoryはDatastoreにアクセスしなくとも使える事に気付いた!


//sortNumberフィールドの昇順にソート
Datastore.sortInMemory(models, ModelMeta.get().sortNumber.asc);

上記のような感じで使える。
これなら、1ユーザ辺りの件数があまり多くないものならば、
Modelとして作ったクラスをJSONにして、Datastoreに保存しておき、
filterInMemory, sortInMemoryで絞り込みや並び順の変更をするということもできる。


全件取得したListをMemcacheに入れておき、
ユーザが入力した条件で、絞り込みや並び順を変更するというのも良いかも。
特に条件が動的になるものは、カスタムインデックスがえらいことになるので・・・。


でも、Datastore~~で使うとDatastoreにアクセスしているように見えるので、
実際に使う時は、ModelUtilみたいなのを作って、くるんであげた方が良さそう。


件数が多くて無理!って場合は、FullTextSearchを使うのが良いのかなぁ?
その辺りはshin1ogawa殿のBlogを見ていただければ!

404 shin1のつぶやき ないわー Not Found: App Engine Full-text Search API の使いどころ


文字ばかりになってしまったので、また今度サンプルソースを書こう・・・。

Slim3でModelをJsonに加工する方法を考える

Slim3Ajax用のレスポンスをJsonで返す時の方法について、あれこれと悩んでた。

Slim3にはMeta.modelToJson()があるので、始めはこれを使えば良いと思っていた。
しかし、いくつかやりたいことで、できないことがあることが分かった。

・getter,setterだけでは値を取得しない
・persistent = falseの値を取得しない


1つ目のgetter,setterだけでは値を取得しない
フィールドの値を表示用に加工したりしたかったのだけど、こいつができないorz
JSPのノリでgetterを作っていたのだけど、ダメだった。

また、Keyの値に入れているものを表示用に出したりもしてやりたかったのだけど、同じ理由でできない。


2つ目のpersistent=falseの値を取得しない
getter,setterだけでダメなら、Datastoreに入れないフィールドにしてやろう!と思ったけど・・・。
これでも値を取得してくれなかった。
どうやらEntityとして生成されるフィールドのみ取得するみたいだ。


それらを試してみたソースは以下の通り。

Model


package org.sinsandbox.model;

import java.io.Serializable;
import java.util.Date;

import com.google.appengine.api.datastore.Key;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Model;

/**
* Json変換時の振る舞いを確認するためのModel
*
* @author sinmetal
*
*/
@Model(schemaVersion = 1)
public class JsonConduct implements Serializable {

private static final long serialVersionUID = 1L;

@Attribute(primaryKey = true)
private Key key;

@Attribute(version = true)
private Long version;

@Attribute(persistent = false)
private String value;

private Date entryDate;

/**
* Returns the key.
*
* @return the key
*/
public Key getKey() {
return key;
}

/**
* Sets the key.
*
* @param key
* the key
*/
public void setKey(Key key) {
this.key = key;
}

/**
* Returns the version.
*
* @return the version
*/
public Long getVersion() {
return version;
}

/**
* Sets the version.
*
* @param version
* the version
*/
public void setVersion(Long version) {
this.version = version;
}

/**
* Get name
*
* @return key name
*/
public String getName() {
if (key == null) {
return "";
}
return key.getName();
}

/**
* Set name
*
* @param name
*/
public void setName(String name) {

}

/**
* @return the value
*/
public String getValue() {
return value;
}

/**
* @param value the value to set
*/
public void setValue(String value) {
this.value = value;
}

/**
* @return the entryDate
*/
public Date getEntryDate() {
return entryDate;
}

/**
* @param entryDate the entryDate to set
*/
public void setEntryDate(Date entryDate) {
this.entryDate = entryDate;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
JsonConduct other = (JsonConduct) obj;
if (key == null) {
if (other.key != null) {
return false;
}
} else if (!key.equals(other.key)) {
return false;
}
return true;
}
}


ModelのUnitTest


package org.sinsandbox.model;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.lang.reflect.Type;
import java.util.Date;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;
import org.sinsandbox.meta.JsonConductMeta;
import org.slim3.datastore.Datastore;
import org.slim3.tester.AppEngineTestCase;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
* Json変換時の振る舞いを確認
*
* @author sinmetal
*
*/
public class JsonConductTest extends AppEngineTestCase {

private JsonConduct model = new JsonConduct();

private JsonConductMeta meta = JsonConductMeta.get();

private Map map;

@Before
public void setUp() throws Exception {
super.setUp();

Key key = Datastore.createKey(meta, "hoge");
model.setKey(key);
model.setVersion(1L);
model.setValue("fuga");
model.setEntryDate(new Date());

String json = meta.modelToJson(model);

Gson gson = new Gson();
Type mapType = new TypeToken>() {
}.getType();
map = gson.fromJson(json, mapType);
}

@Test
public void Json変換テスト() throws Exception {
String keyString = KeyFactory.keyToString(model.getKey());
assertThat(map.get("key").toString(), is(keyString));
assertThat(
keyString,
is("agpVbml0IFRlc3RzchULEgtKc29uQ29uZHVjdCIEaG9nZQw"));
assertThat(map.get("version").toString(), is("1.0"));
assertThat(map.containsKey("name"), is(false)); //getter, setterだけでは生成されない
assertThat(map.containsKey("value"), is(false)); //persistent=falseでも生成されない
}
}


さてさて、どうしてやろうかと思って考えたのだけど、表示用のBeanを作ってやるしか思いつかなかった。
そうして、作ったのが以下。

Modelを表示するために加工するView


package org.sinsandbox.view;

import org.sinsandbox.model.JsonConduct;
import org.sinsandbox.util.KeyConverter;
import org.slim3.util.BeanUtil;
import org.slim3.util.CopyOptions;

/**
* JsonConductModelを表示する時に加工するためのView
*
* @author sinmetal
*
*/
public class JsonConductView {

/** key */
private String key;
/** version */
private Long version;
/** name */
private String name;
/** value */
private String value;
/** 登録日時 */
private String entryDate;

/**
* @return the key
*/
public String getKey() {
return key;
}

/**
* @param key
* the key to set
*/
public void setKey(String key) {
this.key = key;
}

/**
* @return the version
*/
public Long getVersion() {
return version;
}

/**
* @param version
* the version to set
*/
public void setVersion(Long version) {
this.version = version;
}

/**
* @return the name
*/
public String getName() {
return name;
}

/**
* @param name
* the name to set
*/
public void setName(String name) {
this.name = name;
}

/**
* @return the value
*/
public String getValue() {
return value;
}

/**
* @param value
* the value to set
*/
public void setValue(String value) {
this.value = value;
}

/**
* @return the entryDate
*/
public String getEntryDate() {
return entryDate;
}

/**
* @param entryDate
* the entryDate to set
*/
public void setEntryDate(String entryDate) {
this.entryDate = entryDate;
}

/**
* インスタンス生成
*
* @param model
* @return modelの情報を設定したview
*/
public static JsonConductView getInstance(JsonConduct model) {
JsonConductView instance = new JsonConductView();
CopyOptions options = new CopyOptions();
options.converter(new KeyConverter(), "key");
options.dateConverter("yyyy/MM/dd", "entryDate");
BeanUtil.copy(model, instance, options);
return instance;
}
}


BeanUtil.copy()の時にKeyを表示用に変換するためのConverter
Slim3のBeanUtilには、Convertする時の振る舞いを追加できるので、KeyのためのConverterを作った。


package org.sinsandbox.util;

import org.slim3.util.Converter;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

/**
* The converter for key.
*
* @author sinmetal
*
*/
public class KeyConverter implements Converter {

@Override
public Key getAsObject(String value) {
return KeyFactory.stringToKey(value);
}

@Override
public String getAsString(Object value) {
if (value == null) {
return null;
}
if (!(value instanceof Key)) {
throw new IllegalArgumentException("The class("
+ value.getClass().getName()
+ ") can not be assigned to date.");
}
return KeyFactory.keyToString((Key) value);
}

@Override
public boolean isTarget(Class clazz) {
return Key.class.isAssignableFrom(clazz);
}

}


ViewのUnitTest


package org.sinsandbox.view;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.lang.reflect.Type;
import java.util.Date;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;
import org.sinsandbox.meta.JsonConductMeta;
import org.sinsandbox.model.JsonConduct;
import org.slim3.datastore.Datastore;
import org.slim3.tester.AppEngineTestCase;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
* Json変換時の振る舞いを確認するためのModelを表示用に加工するView
*
* @author sinmetal
*
*/
public class JsonConductViewTest extends AppEngineTestCase {
private JsonConduct model = new JsonConduct();

private JsonConductMeta meta = JsonConductMeta.get();

private Map map;

@Before
public void setUp() throws Exception {
super.setUp();

Key key = Datastore.createKey(meta, "hoge");
model.setKey(key);
model.setVersion(1L);
model.setValue("fuga");
model.setEntryDate(new Date());

Gson gson = new Gson();
JsonConductView view = JsonConductView.getInstance(model);
String json = gson.toJson(view);

Type mapType = new TypeToken>() {
}.getType();
map = gson.fromJson(json, mapType);
}

@Test
public void Json変換テスト() throws Exception {
String keyString = KeyFactory.keyToString(model.getKey());
assertThat(map.get("key").toString(), is(keyString));
assertThat(
keyString,
is("agpVbml0IFRlc3RzchULEgtKc29uQ29uZHVjdCIEaG9nZQw"));
assertThat(map.get("version").toString(), is("1.0"));
final String NAME = "name";
assertThat(map.containsKey(NAME), is(true));
assertThat(map.get(NAME).toString(), is(model.getName()));
final String VALUE = "value";
assertThat(map.containsKey(VALUE), is(true));
assertThat(map.get(VALUE).toString(), is(model.getValue()));
}
}


ちょっと作ってやるのが面倒だけど、とりあえずこんな感じで切り抜けようかと思う!

GAEでSPDY対応サイトを作る!

SPDYを利用すると、ServerとClient間の通信が早くなる。
銀の弾丸のようなものではないけど、使えるなら、使って損は無いだろう。
僕もあんまり詳しいところは分かってないので、以下を参照して欲しい。

Google提唱の新プロトコル「SPDY」とは--SPDYで変わること、変わらないこと - builder by ZDNet Japan


GAEはSPDYに対応しているので、Webサイトを作る時に、ちょっと手を加えるだけでSPDY対応サイトになる。
というか、やることは1つだけなのではないだろうか?
htmlにフルパスでURLを書く時に、先頭からプロトコルを排除して、//で始まるようにするだけだ。

google からjqueryを読み込む場合は以下のような感じだ。

こうしておけば、httpで通信していればhttpで、httpsで通信していればhttpsで通信してくれる。

SPDYはhttpsでしか利用できないので、httpがハードコーディングされていると、邪魔になるからだ。


後は、httpsでアクセスすれば、SPDYで通信してくれる!

とは言え、ぱっと見ではSPDYなのかどうかが分からない。
それを知りたければ、以下のBlogで紹介されているアドオンを使うと、ひと目で分かる!

こてさきAjax:GAE で作ろう!! 簡単にSPDY対応サイト - livedoor Blog(ブログ)

以下は、僕のWebサイト(まだ作り中で、動いてないけどw)

https://sinpkmnms.appspot.com/

緑色の雷マークが輝いているはずだw


ただ、ちょっとはまるのは、defaultでないversionにアクセスした時。
AdminConsoleのVersions一覧からアクセスすると、以下のURLになるので、ドメインが変わってしまう。

https://2012-0609-1612.sinpkmnms.appspot.com/

これでは、セキュリティ警告が出てしまう。
そのため、以下のURLにする。

https://2012-0609-1612-dot-sinpkmnms.appspot.com/

sinpkmnmsがAppId、2012-0609-1612がversionなので、この2つをあなたのものに置き換えていただければ、良いはずだ。
これでばっちし!

goog.net.XhrManagerを使ってみた!

Closure LibraryでXMLHttpRequestを扱うgoog.net.XhrManagerを使ってみました。
これは前回のgoog.net.XhrIoを内部に持ち、複数リクエストを簡単に扱えるようにしたものらしいです。
WEB開発メモ: Closure Library - XhrManager


まずはコンストラクタ実行時に、インスタンスを生成します。
app.js

    /**
     * initialize.
     * @private
     */
    xhrmanagerSample.App.prototype.initialize_ = function(){
        this.eventHandler_ = new gevents.EventHandler(this);
        
        this.xhrManager_ = new goog.net.XhrManager();  //ここで生成
        this.nextXhrId_ = 1;                           //リクエスト管理用のID
        
        var viewBookListButton = gdom.getElement('view-book-list-button');
        this.eventHandler_.listen(viewBookListButton, EventType.CLICK, goog.bind(this.onClickViewBookListButton_, this));
        var bookEntryForm = gdom.getElement('book-entry-form');
        this.eventHandler_.listen(bookEntryForm, EventType.SUBMIT, goog.bind(this.onSubmit_, this));
    };

this.nextXhrId_はXMLHttpRequestを管理するためのIDです。
リクエスト毎に一意になるように、連番で採番します。

残りのコードは、以下の2つの機能を実現するためのものです。
書籍の登録
ボタンを押すと書籍の一覧が出てくる


XMLHttpRequestを送信するための関数です。
app.js

    /**
     * ajax send request.
     * @param {Object} path
     * @param {Object} query
     * @param {Object} callback
     * @param {Object} method
     * @param {Object} opt_content
     */
    xhrmanagerSample.App.prototype.sendRequest = function(path, query, callback, method, opt_content){
        var url = goog.Uri.parse('/closurelibrary/xhriosample/' + path);
        goog.object.forEach(query || {}, function(value, key){
            url.setParameterValue(key, value);
        }, this);
        var headers = {}, body = null;
        if (opt_content) {
            body = goog.json.serialize(opt_content);
            headers['Content-Type'] = 'application/json';
        }
        var error = gdom.getElement('error-message');
        gdom.removeChildren(error);
        return this.xhrManager_.send(this.nextXhrId_++, url, method, body, headers, 0, goog.bind(this.processRequest_, this, callback), 5);
    };

XmlHttpRequestを送信しているのは最後のsend()です。
send()の仕様については、以下の通りです。

id         {string} リクエストのID。他のリクエストと重ならない文字列を指定する
url        {string} リクエスト送信先のURL
method     {string=} リクエストするメソッド。デフォルトはGET
content    {string=} Postするデータ
headers    {Object|goog.structs.Map=} リクエストのヘッダー
priority   {*=} リクエストの優先順位。小さいほど優先順位が高い
callback   {Function=} リクエスト完了時に呼ばれるコールバック関数
maxRetries {number=} リトライの最大回数

GAEでは色々な理由でErrorが発生します。
その場合、リトライを行いたいのですが、XhrManagerを使えば、maxRetriesを指定するだけで実現できます。


リクエストの返答があった時に呼ばれる関数です。
これは標準で呼ばれるものではなく、XhrMnager.send()で私が指定したため、呼ばれます。
app.js

    /**
     * ajax request process method.
     * @param {Object} callback
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.processRequest_ = function(callback, e){
        var xhr = e.target;
        var error = gdom.getElement('error-message');
        if (xhr.isSuccess()) {
            callback && callback(xhr.getResponseJson('while(1);'));
        }
        else {
            gdom.removeChildren(error);
            if (xhr.getStatus() == '404') {
                gdom.append(error, gsoy.renderAsElement(templates.alert.error, {
                    'message': 'ページが見つかりませんでした。'
                }));
            }
            else {
                gdom.append(error, gsoy.renderAsElement(templates.alert.error, {
                    'message': 'エラーが発生しました。再度、試してみてください。'
                }));
            }
        }
    };

手抜きですが、Error処理なんかもちょこっと入れたりしています。
xhr.isSuccess()で結果が成功していた場合のみ、送信時に指定したコールバック関数を呼ぶようにしています。
xhrオブジェクトから、リクエスト時に指定したIDも取得できるので、ここで処理を分岐することもできると思います。


書籍一覧を取得するための関数です。
ボタン押下時に呼ばれる関数です。
app.js

    /**
     * book list get.
     */
    xhrmanagerSample.App.prototype.getBookList = function(){
        var bookList = gdom.getElement('book-list');
        var loadingIcon = gdom.createDom('span', {
            'class': 'icon-loading16'
        }, '');
        gdom.append(bookList, loadingIcon);
        
        this.sendRequest('list', {}, goog.bind(this.onGetBookListComplete, this), 'GET', {});
    };

リクエストが返ってくるまでの間、くるくる回るアイコンを表示しています。
'list'がURLの最後のPath。
onGetBookListCompleteがリクエスト成功時に呼ばれるコールバック関数です。


書籍一覧取得成功時に呼ばれるコールバック関数です。
app.js

    /**
     * ajax get book list compete.
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.onGetBookListComplete = function(e){
        var bookList = gdom.getElement('book-list');
        gdom.removeChildren(bookList);
        
        var data = {
            bookList: []
        };
        for (var i = 0, len = e['bookList'].length; i < len; i++) {
            data.bookList[i] = {
                isbn: e['bookList'][i]['isbn'],
                name: e['bookList'][i]['name'],
                link: e['bookList'][i]['link']
            };
        }
        gdom.append(bookList, gsoy.renderAsElement(templates.book.bookList, data));
    };

Closure Templatesを使っています。
この辺りは、以下のエントリーを見ていただくと分かりやすいです。
Closure Templatesを使ってみた! - SinDiary


書籍情報登録時に呼ばれる関数です。
app.js

    /**
     * ajax book form put.
     * @param {Object} param
     */
    xhrmanagerSample.App.prototype.putBook = function(param){
        this.sendRequest('entry', param, goog.bind(this.onPutBookComplete, this), 'PUT', param);
    };

'entry'がURLの最後のPath。
onPutBookCompleteがリクエスト成功時に呼ばれるコールバック関数です。
paramの中に登録する書籍の情報が入っています。


書籍情報登録成功時に呼ばれる関数です。
app.js

    /**
     * ajax book form put complete.
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.onPutBookComplete = function(e){
        var form = gdom.getElement('book-entry-form');
        form.reset();
    };

フォームをリセットしているだけです。


フォームSubmit時に呼ばれる関数です。
app.js

    /**
     * book entry form submit.
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.onSubmit_ = function(e){
        e.preventDefault();
        var form = gdom.getElement('book-entry-form');
        var param = gdom.forms.getFormDataMap(form).toObject();
        this.putBook(param);
    };

initialize_()でイベントハンドラーに登録した関数です。
フォームのSubmit時に呼ばれ、実際のSubmitを止めた後、値をXMLHttpRequestで投げるようにしています。


まだまだ、改善点はありますが、とりあえずはこんな感じです。
後、UnitTestをXhrIoの時のように作れれば良いのですが、まだ出来ていません・・・。
goog.net.XhrIoを使ってみた! - SinDiary
伊藤殿に質問して回答していただいたのですが、まだ動くものはできずorz
https://plus.google.com/u/0/114061805681880927213/posts/ZA6B9wthi3D
なんとかしたいと思ってます。


今回のソースはこちら。
app.js
Google Code Archive - Long-term storage for Google Code Project Hosting.

goog.net.XhrIoを使ってみた!

Closure LibraryでXMLHttpRequestを扱う。goog.net.XhrIoを使ってみました。

app.js


xhrioSample.App.prototype.getBookList = function(xhrio){
//取得成功
gevents.listen(xhrio, goog.net.EventType.SUCCESS, function(e){
var res = e.target.getResponseJson('while(1);');
var bookList = gdom.getElement('book-list');
var data = {
bookList: []
};
for (var i = 0, len = e['bookList'].length; i < len; i++) {
data.bookList[i] = {
isbn: e['bookList'][i]['isbn'],
name: e['bookList'][i]['name'],
link: e['bookList'][i]['link']
};
}
gdom.append(bookList, gsoy.renderAsElement(templates.book.bookList, data));
});

//取得失敗
gevents.listen(xhrio, goog.net.EventType.ERROR, function(e){
console.log('ERROR');
console.log(e);
});

//レスポンスが返ってきた
gevents.listen(xhrio, goog.net.EventType.COMPLETE, function(e){
console.log('COMPLETE');
var bookList = gdom.getElement('book-list');
gdom.removeChildren(bookList);
});

var bookList = gdom.getElement('book-list');
xhrio.send('/closurelibrary/xhriosample/list');
}

直接関係ない部分を省略していますが、こんな感じです。
Server側からjsonでデータを受け取って、表示しています。
Error処理なんか手抜きですが、上記のような感じでイベントをハンドリングできます。
公式のサンプルを見ると、上記以外にも幾つかのハンドリング可能なイベントがありますね。
Asynchronous XMLHttpRequests with XhrIo  |  Closure Library  |  Google Developers


後、これのUnitTestも頑張って書いてみました。
test.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>XhrIO Sample Test</title>
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script>
            goog.require('goog.testing.jsunit');
            goog.require('goog.testing.TestQueue');
            goog.require('goog.testing.net.XhrIo');
        </script>
        <script src="deps.js">
        </script>
    </head>
    <body>
        <div id="main">
            <button id="view-book-list-button" class="goog-custom-button">
                view book list
            </button>
            <div id="book-list">
            </div>
        </div>
        <script>
            goog.require('xhrioSample.App');
        </script>
        <script>
            function testXhrIoSuccess(){
                var queue = new goog.testing.TestQueue();
                var xhrio = new goog.testing.net.XhrIo(queue);
                
                xhrioSample.App.getInstance().getBookList(xhrio);
                
                assertFalse('リクエストが発行されなかった', queue.isEmpty());
                var req = queue.dequeue();
                assertEquals('URLが不正', req[1], '/closurelibrary/xhriosample/list');
                xhrio.simulateResponse(200, '{"status":"OK","bookList":[{"key":{"kind":"Book","id":0,"name":"9999999999999"},"version":1,"isbn":"9999999999999","name":"Test Book Name!!"}]}', {});
                
                var main = goog.dom.getElement('main');
                var isbn = goog.dom.getElementByClass("isbn", main);
                var isbnText = goog.dom.getTextContent(isbn);
                assertNotNullNorUndefined('ISBN表示失敗', isbnText);
                assertEquals('ISBN表示失敗', '9999999999999', isbnText);
                
                var name = goog.dom.getElementByClass("name", main);
                var nameText = goog.dom.getTextContent(name);
                assertNotNullNorUndefined('書籍名表示失敗', nameText);
                assertEquals('書籍名表示失敗', 'Test Book Name!!', nameText);
                
                var amazon = goog.dom.getElementByClass("amazon", main);
                var amazonText = goog.dom.getTextContent(amazon);
                assertNotNullNorUndefined('AmazonLink表示失敗', amazonText);
                assertEquals('AmazonLink表示失敗', 'amazon', amazonText);
            }
        
        function testXhrIoError(){
                var queue = new goog.testing.TestQueue();
                var xhrio = new goog.testing.net.XhrIo(queue);
                
                xhrioSample.App.getInstance().getBookList(xhrio);
                
                assertFalse('リクエストが発行されなかった', queue.isEmpty());
                var req = queue.dequeue();
                assertEquals('URLが不正', req[1], '/closurelibrary/xhriosample/list');
                xhrio.simulateResponse(404, '', {});
                
                var bookList = goog.dom.getElement('book-list');
                var bookListText = goog.dom.getTextContent(bookList);
                assertEquals('Error表示', 'Error', bookListText);
            }
        </script>
    </body>
</html>

見本となるようなコードなのかは自信はないですが・・・。
goog.net.XhrIoをテストするためには、以下の2つを利用しています。

goog.testing.TestQueue
goog.testing.net.XhrIo

この2つを利用することで、実際にServerを起動させること無く、Serverのレスポンスをシミュレートさせています。
本当は、getBookList()の引数にXhrIoを渡すのではなく、getBookList()の中でXhrIoを作成し、テスト時にはgoog.net.XhrIoコンストラクタをモックにすることで、テストを行いたかったのですが、モックへの置き換え方法がよく分からず、とりあえずここまでで断念・・・。


最後に今回のソースはこちら!
app.js
Google Code Archive - Long-term storage for Google Code Project Hosting.
test.html
Google Code Archive - Long-term storage for Google Code Project Hosting.