Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

Object Oriented JavaScriptの入門

謝罪

Object Oriented JavaScriptの入門 - Yuta.Kikuchiの日記 はてなブックマーク - Object Oriented JavaScriptの入門 - Yuta.Kikuchiの日記


2013/08/27に公開したObject Oriented JavaScriptの記事ですが、prototype周りの説明がいい加減だったため内容を一旦削除することにしました。多くの方にBookmarkして頂いたお陰でホットエントリーにも掲載されたのですが、正直それに見合う内容ではありませんでした。


あやふやな知識を書いてしまったのは僕の勉強不足が原因です。ネットで収集した情報だけで自分の知識を固めて行くのは危険だという事を身にしみて感じました。同時に今回とても悔しく情けない思いもしたので、必ずや正しいJavaScriptオブジェクト指向知識を身につけて再投稿することをここに宣言します。 "I shall return!"

(function() {
   console.log( "I shall return!" );
}());

> 後で読むタグをつけた方、もう記事はありません(笑)。修正版の再投稿を期待していてください。
> ブックマークをしていただいた方、申し訳ありませんがブックマークの削除をお願いします。
id:yuisekiさん はい、仰せの通りCoffeeScriptで書くというのも一つの手だと感じています。
id:anemoさん もしまた変な知識を書いていたら今度は容赦なく突っ込みをお願いします。

JSONを見やすく表示するにはPythonの-mjson.toolを使うと良いよ

Python クックブック 第2版

Python クックブック 第2版

JSONを見やすく表示する

WebツールやExtension
{"Compile":["C","C++","Objective-C"],"Script":["JavaScript","PHP","Perl","Python"]}

JSONJavaScriptのObjectを文字列化したもので、1行にまとまってしまうため非常に見づらいです。「JSON 整形」というキーワードでぐぐると上のようにWebサービスからChrome/FirefoxのExtensionなどが出てきます。今回はサーバサイドでJSONをechoする処理を書いていて、ツールを用いる事無く簡単に見やすくする方法を調べていたのですが、Pythonの-mjson.toolを使うとコマンドライン上で整形できることがわかりました。

Python pretty-print-json

Stack Overflowに How to pretty-print JSON script?というコラムがあり、そこでPythonの-mjson.toolを指定する方法を知りました。例えば以下のようなPerlコードがあって、実行処理に対してパイプ(|)でpython -mjson.toolを指定するだけで整形してくれます。下では生のJSON文字列と整形した内容を比較していますが、見やすさは歴然だと思います。特にHashを表す{}と配列を表す[]がそれぞれ分かりやすいかと。

#!/usr/bin/perl

use strict;
use warnings;
use JSON;

my %array = ();
push( @{$array{Script}}, ( 'JavaScript', 'PHP', 'Perl', 'Python' ) );
push( @{$array{Compile}}, ( 'C', 'C++', 'Objective-C' ) );

print encode_json( \%array );
$ perl echo_json.pl
{"Compile":["C","C++","Objective-C"],"Script":["JavaScript","PHP","Perl","Python"]}
$ perl echo_json.pl |  python -mjson.tool
{
    "Compile": [
        "C", 
        "C++", 
        "Objective-C"
    ], 
    "Script": [
        "JavaScript", 
        "PHP", 
        "Perl", 
        "Python"
    ]
}

「魔法少女まどか☆マギカ」の台詞をJavaScriptでMapReduceしてGoogle Chart APIでグラフ出力したよ!

Hadoop 第2版

Hadoop 第2版

概要

以前に「魔法少女まどか☆マギカ」の台詞をNLTK(Natural Language Toolkit)で解析することに挑戦しましたが、解析結果の集計グラフが奇麗に表示されませんでした。今回はそれを改善すべく手法を変えて挑戦します。グラフ化はNLTKではなくGoogle Chart APIを利用します。Google Chart Tools - Google Code はてなブックマーク - Google Chart Tools - Google Code またHadoopMapReduceJavaScriptを用いGoogle Chart APIとのデータ連携をしやすいようにします。以下に大まかな処理の流れを記述します。

  1. SpiderMonkeyCentOSに設定
  2. 魔法少女まどか☆マギカの台詞をPythonスクレイピング
  3. NLTKによる分かち書き
  4. JavaScriptによるMapReduce
  5. HadoopMapReduce
  6. Google Chart APIMapReduce結果をグラフ化

SpiderMonkeyCentOSに設定

参考

HadoopMapReduceJavaScriptで行うためにCentOSSpiderMonkeyを設定します。本家サイトのinstall手順を参考にしました。https://developer.mozilla.org/en/Building_only_SpiderMonkey

ソースコード取得/解凍
$ wget http://ftp.mozilla.org/pub/mozilla.org/js/js-1.8.0-rc1.tar.gz
$ tar -xzf js-1.8.0-rc1.tar.gz
build/install
$ cd js/src/
$ make BUILD_OPT=1 -f Makefile.ref
$ sudo make BUILD_OPT=1 JS_DIST=/usr/local -f Makefile.ref export

魔法少女まどか☆マギカ」の台詞をPythonスクレイピング

スクレイピングPythonコード

pythonコードで魔法少女まどか☆マギカ台詞をスクレイピングします。以前より台詞掲載ページが増えていたのでURLを追加しました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys,re,urllib,urllib2
urls = ( 'http://www22.atwiki.jp/madoka-magica/pages/170.html',
         'http://www22.atwiki.jp/madoka-magica/pages/175.html',
         'http://www22.atwiki.jp/madoka-magica/pages/179.html',
         'http://www22.atwiki.jp/madoka-magica/pages/180.html',
         'http://www22.atwiki.jp/madoka-magica/pages/200.html',
         'http://www22.atwiki.jp/madoka-magica/pages/247.html',
         'http://www22.atwiki.jp/madoka-magica/pages/244.html',
         'http://www22.atwiki.jp/madoka-magica/pages/249.html',
         'http://www22.atwiki.jp/madoka-magica/pages/250.html',
         'http://www22.atwiki.jp/madoka-magica/pages/252.html',
         'http://www22.atwiki.jp/madoka-magica/pages/241.html',
         'http://www22.atwiki.jp/madoka-magica/pages/254.html'
         )   
f = open( './madmagi.txt', 'w' )
opener = urllib2.build_opener()
ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/    534.51.22'
referer = 'http://www22.atwiki.jp/madoka-magica/'
opener.addheaders = [( 'User-Agent', ua ),( 'Referer', referer )]
for url in urls:
    content = opener.open( url ).read()
    if re.compile( r'<div class="contents".*?>((.|\n)*?)</div>', re.M ).search( content ) is not None:
        data = re.compile( r'<div class="contents".*?>((.|\n)*?)</div>', re.M ).search( content ).group()
        if re.compile( r'「(.*?)」', re.M ).search( data ) is not None: 
            lines = re.compile( r'「(.*?)」', re.M ).findall( data )
            for line in lines:
                f.write( line + "\n" )
f.close()
抽出データ

以下は抽出したデータの一部です。全部で1259行あります。

んっん…あっ…!
あっ…!
ひどい…
仕方ないよ。彼女一人では荷が重すぎた
でも、彼女も覚悟の上だろう
そんな…あんまりだよ、こんなのってないよ
諦めたらそれまでだ
でも、君なら運命を変えられる
避けようのない滅びも、嘆きも、全て君が覆せばいい
そのための力が、君には備わっているんだから
本当なの?
私なんかでも、本当に何かできるの?こんな結末を変えられるの?
もちろんさ。だから僕と契約して、魔法少女になってよ!
私は巴マミ
あなたたちと同じ、見滝原中の3年生
そして
キュゥべえと契約した、魔法少女よ

NLTKによる分かち書き

NLTKによる分かち書きを行います。分かち書きとは語の区切りに空白を入れて分かりやすくする表記です。正規表現による単語ベースでの分かち書きMeCabを用いた形態素ベースでの分かち書きの両方をやります。

単語区切り

台詞の単語をスペースで区切ります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

import nltk
from nltk.corpus.reader import *
from nltk.corpus.reader.util import *
from nltk.text import Text

jp_sent_tokenizer = nltk.RegexpTokenizer(u'[^ 「」!?。]*[!?。]')
jp_chartype_tokenizer = nltk.RegexpTokenizer(u'([ぁ-んー]+|[ァ-ンー]+|[\u4e00-\u9FFF]+|[^ぁ-んァ-ンー\u4e00-\u9FFF]+)')
data = PlaintextCorpusReader( './', r'madmagi.txt',
                              encoding='utf-8',
                              para_block_reader=read_line_block,
                              sent_tokenizer=jp_sent_tokenizer,
                              word_tokenizer=jp_chartype_tokenizer )

#ファイル保存
f = open( './word.txt', 'w' )
for i in data.words():
    f.write( i + " " )
f.close

抽出された状態は以下のようになります。最初にスクレイプした状態からスペース区切りになっている事が分かると思います。これをword.txtとして保存します。

 んっん … あっ …!
 あっ …!
 ひどい …
 仕方 ないよ 。 彼女一人 では 荷 が 重 すぎた 
 でも 、 彼女 も 覚悟 の 上 だろう 
 そんな … あんまりだよ 、 こんなのってないよ 
 諦 めたらそれまでだ 
 でも 、 君 なら 運命 を 変 えられる 
 避 けようのない 滅 びも 、 嘆 きも 、 全 て 君 が 覆 せばいい 
 そのための 力 が 、 君 には 備 わっているんだから 
 本当 なの ?
 私 なんかでも 、 本当 に 何 かできるの ? こんな 結末 を 変 えられるの ?
 もちろんさ 。 だから 僕 と 契約 して 、 魔法少女 になってよ !
 私 は 巴 マミ 
 あなたたちと 同 じ 、 見滝原中 の 3 年生 
 そして 
 キュゥ べえと 契約 した 、 魔法少女 よ
MeCabによる形態素解析

MeCabを利用して形態素解析を行い、形態素毎にスペース区切りでデータを保存するようにします。抽出するサンプルコードは次のようになります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

import MeCab
mecab = MeCab.Tagger('-Ochasen')

data = open( './madmagi.txt' ).read()

f = open( './ma.txt', 'w' )

node = mecab.parseToNode( data )
phrases = node.next
while phrases:
    try:
        k = node.surface
        f.write( k + " " )
        node = node.next
    except AttributeError:
       break 
f.close()

抽出した結果は以下のようになります。これをma.txtとして保存します。

 ん っ ん … あっ … ! あっ … ! ひどい … 仕方 ない よ 。 彼女 一 人 で は 荷 が 重 すぎ た でも 、 彼女 も 覚悟 の 上 だろ う そんな … あんまり だ よ 、 こんな の って ない よ 諦め たら それ まで だ で も 、 君 なら 運命 を 変え られる 避け よう の ない 滅び も 、 嘆き も 、 全て 君 が 覆せ ば いい その ため の 力 が 、 君 に は 備わっ て いる ん だ から 本当 な の ? 私 なんか でも 、 本当に 何 か できる の ? こんな 結末 を 変え られる の ? もちろん さ 。 だから 僕 と 契約 し て 、 魔法 少女 に なっ て よ ! 私 は 巴 マミ あなた たち と 同じ 、 見滝 原中 の 3 年生 そして キュゥ べ えと 契約 し た 、 魔法 少女 よ

JavaScriptによるMapReduce

MapRuduceクラスの定義

NLTKによって分かち書きされた単語をJavaScriptMapReduceします。
簡単なMapReduceクラスを以下のように定義します。mapメソッドは単語を整形、reduceは畳み込みを行っています。

var MapReduce = function() {};

MapReduce.prototype = {
   map : function( data ) {
        var lines = [];
        re = new RegExp( "\n" );
        if( re.test( data ) ) {
            lines = data.split( "\n" );
        } else {
            lines[0] = data;
        }
        for( var j=0; j<lines.length; j++ ) {
            var words = lines[j].split( " " );
            for( var i=0; i<words.length; i++ ) {
                print( words[i] + " ," + 1 );
            }
        }
   },

   reduce : function( lines ) {
        var nodes = {};
        for( i=0; i<lines.length; i++ ) {
            var words = lines[i].split( " ," );
            var k = words[0];
            if( !nodes[k] ) {
                nodes[k] = 1;
            }
            nodes[k] = nodes[k] + 1;        
        }
        for( var k in nodes ) {
            print( k + ' : ' + nodes[k] );
        }
    },
};
mapper.js

上のMapReduceクラスを呼び出すmapper.jsを次のように定義します。load関数を使って上で定義した外部ファイル(MapReduce.js)を読み込みます。またSpiderMonkeyreadlineを使って標準入力を読み取ります。

#!/usr/local/bin/js
load( "MapReduce.js" );
var MR = new MapReduce();
while( ( line = readline() ) !== null ) {
    MR.map( line );
}

NLTKで抽出したword.txtに対してmapperをかけると以下のような結果を得る事ができます。

$ cat data/word.txt | js mapper.js
んっん ,1
… ,1
あっ ,1
…! ,1
 ,1
あっ ,1
…! ,1
 ,1
ひどい ,1
… ,1
 ,1
仕方 ,1
ないよ ,1
。 ,1
彼女一人 ,1
では ,1
荷 ,1
が ,1
重 ,1
すぎた ,1
reducer.js

上のMapReduceクラスを呼び出すreducer.jsを次のように定義します。

#!/usr/local/bin/js
load( "MapReduce.js" );
var MR = new MapReduce();
lines = [];
i = 0;
while( ( line = readline() ) !== null ) {
    lines[i] = line;
    i++;
}
MR.reduce( lines );

mapperで得た結果に対してreducerをかけると以下のようになります。

$ cat data/word.txt | js mapper.js | js reducer.js
んっん : 2
… : 237
あっ : 19
…! : 10
 : 2080
ひどい : 4
仕方 : 11
ないよ : 8
。 : 457
彼女一人 : 3
では : 9
荷 : 3
が : 127
重 : 3
すぎた : 3
でも : 23
、 : 1009
彼女 : 18
も : 61
覚悟 : 3

HadoopMapReduce

HDFSへの登録

折角mapper/reducerを書いたのでHadoop上で実行します。NLTKで解析した単語、形態素の二つのテキストデータをHDFSに登録します。HDFSのディレクトリをmadmagiとして新たに作成します。またHDFSコマンドは長いのでaliasを張ります。ここではalias hdfs='hadoop dfs'としました。

$ alias hdfs='hadoop dfs'
$ hdfs -mkdir madmagi
$ hdfs -put data/ma.txt madmagi/
$ hdfs -put data/word.txt madmagi/
$ hdfs -ls madmagi/
Found 2 items
-rw-r--r--   1 yuta supergroup          0 2012-03-21 08:04 /user/yuta/madmagi/ma.txt
-rw-r--r--   1 yuta supergroup          0 2012-03-21 08:04 /user/yuta/madmagi/word.txt
Hadoop実行

HadoopStreamingを使ってMapReduceします。基本的には上で実行したmapper.js/reducer.jsをHadoopで実行しているだけです。まずは単語の結果に対して(word.txt)に対して実行します。

$ hadoop jar /usr/lib/hadoop-0.20/contrib/streaming/hadoop-streaming-0.20.2-cdh3u3.jar -input madmagi_in/word.txt -output madmagi_out_word -mapper /home/yuta/work/dev/hadoop/map_reduce/mapper.js  -reducer /home/yuta/work/dev/hadoop/map_reduce/reducer.js -file /home/yuta/work/dev/hadoop/map_reduce/mapper.js -file /home/yuta/work/dev/hadoop/map_reduce/reducer.js

/yuta/work/dev/hadoop/map_reduce/map.py, /home/yuta/work/dev/hadoop/map_reduce/reduce.py, /var/lib/hadoop-0.20/cache/yuta/hadoop-unjar2165458985493486154/] [] /tmp/streamjob8394427362728426211.jar tmpDir=null
12/03/26 02:09:13 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
12/03/26 02:09:13 WARN snappy.LoadSnappy: Snappy native library not loaded
12/03/26 02:09:13 INFO mapred.FileInputFormat: Total input paths to process : 1
12/03/26 02:09:14 INFO streaming.StreamJob: getLocalDirs(): [/var/lib/hadoop-0.20/cache/yuta/mapred/local]
12/03/26 02:09:14 INFO streaming.StreamJob: Running job: job_201203260111_0017
12/03/26 02:09:14 INFO streaming.StreamJob: To kill this job, run:
12/03/26 02:09:14 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job  -Dmapred.job.tracker=localhost:8021 -kill job_201203260111_0017
12/03/26 02:09:14 INFO streaming.StreamJob: Tracking URL: http://localhost.localdomain:50030/jobdetails.jsp?jobid=job_201203260111_0017
12/03/26 02:09:15 INFO streaming.StreamJob:  map 0%  reduce 0%
12/03/26 02:09:41 INFO streaming.StreamJob:  map 50%  reduce 0%
12/03/26 02:09:42 INFO streaming.StreamJob:  map 100%  reduce 0%
12/03/26 02:10:08 INFO streaming.StreamJob:  map 100%  reduce 100%
12/03/26 02:10:16 INFO streaming.StreamJob: Job complete: job_201203260111_0017
12/03/26 02:10:16 INFO streaming.StreamJob: Output: madmagi_out_word

同様に形態素解析の結果(ma.txt)もHadoopMapReduceします。

$ hadoop jar /usr/lib/hadoop-0.20/contrib/streaming/hadoop-streaming-0.20.2-cdh3u3.jar -input madmagi_in/ma.txt -output madmagi_out_ma -mapper /home/yuta/work/dev/hadoop/map_reduce/mapper.js  -reducer /home/yuta/work/dev/hadoop/map_reduce/reducer.js -file /home/yuta/work/dev/hadoop/map_reduce/mapper.js -file /home/yuta/work/dev/hadoop/map_reduce/reducer.js

59202/] [] /tmp/streamjob2162462095464428994.jar tmpDir=null
12/03/26 02:10:52 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
12/03/26 02:10:52 WARN snappy.LoadSnappy: Snappy native library not loaded
12/03/26 02:10:52 INFO mapred.FileInputFormat: Total input paths to process : 1
12/03/26 02:10:53 INFO streaming.StreamJob: getLocalDirs(): [/var/lib/hadoop-0.20/cache/yuta/mapred/local]
12/03/26 02:10:53 INFO streaming.StreamJob: Running job: job_201203260111_0018
12/03/26 02:10:53 INFO streaming.StreamJob: To kill this job, run:
12/03/26 02:10:53 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job  -Dmapred.job.tracker=localhost:8021 -kill job_201203260111_0018
12/03/26 02:10:53 INFO streaming.StreamJob: Tracking URL: http://localhost.localdomain:50030/jobdetails.jsp?jobid=job_201203260111_0018
12/03/26 02:10:54 INFO streaming.StreamJob:  map 0%  reduce 0%
12/03/26 02:11:17 INFO streaming.StreamJob:  map 50%  reduce 0%
12/03/26 02:11:21 INFO streaming.StreamJob:  map 100%  reduce 0%
12/03/26 02:11:39 INFO streaming.StreamJob:  map 100%  reduce 100%
12/03/26 02:11:46 INFO streaming.StreamJob: Job complete: job_201203260111_0018
12/03/26 02:11:46 INFO streaming.StreamJob: Output: madmagi_out_ma
HDFSからローカルへコピー

HDFS上の結果をローカルにコピーします。成功結果のpart-00000ファイルが出来ている事を確認します。

$ hdfs -get madmagi_out_word madmagi_out_word
$ hdfs -get madmagi_out_ma madmagi_out_ma
$ ls madmagi_out_word
drwxr-xr-x 3 yuta yuta  4096  3月 26 02:48 .
drwxr-xr-x 6 yuta yuta  4096  3月 26 02:46 ..
-rw-r--r-- 1 yuta yuta     0  3月 26 02:46 _SUCCESS
drwxr-xr-x 3 yuta yuta  4096  3月 26 02:46 _logs
-rw-r--r-- 1 yuta yuta 59377  3月 26 02:46 part-00000
$ ls madmagi_out_ma
drwxr-xr-x 3 yuta yuta  4096  3月 26 02:46 .
drwxr-xr-x 6 yuta yuta  4096  3月 26 02:46 ..
-rw-r--r-- 1 yuta yuta     0  3月 26 02:46 _SUCCESS
drwxr-xr-x 3 yuta yuta  4096  3月 26 02:46 _logs
-rw-r--r-- 1 yuta yuta 24294  3月 26 02:46 part-00000

part-0000の中身は以下のようになっています。ここではword.txtに対して実行した結果のpart-00000を載せます。

あ  ,15   
あぁ ,1
あぁっ  ,2
ああ  ,5
ああそうだったの  ,1
ああでもしなきゃ  ,1
あいつは  ,3
あけみさんは  ,1
あげるよ  ,1
あたし  ,3
あたしが  ,1
あたしがこんな  ,1
(略)

抽出で目立った単語としては以下のようになりました。

Google Chart APIMapReduce結果をグラフ化

データをJSON形式に変換

part-00000といった結果をJSON形式に変換します。JSONに変換するPythonコードは以下の通りです。結果をtext.jsとして保存します。

#!/usr/bin/env python
import json,codecs
map = {}
for line in codecs.open( 'word.txt', 'r', 'utf-8' ):
    data = line.split( ':' )
    key = data[0]
    value = data[1]
    map[ key ] = value
str = json.dumps( map )
f = open( 'text.js', 'w' )
f.write( str )
f.close()
JSON形式をGoogleChartAPIで出力

GoogleChartAPIに対してimgタグで読み込むスクリプトを次のように定義します。上で保存したtext.jsにて下で定義するdrawImage関数にjsonデータを渡すように修正が必要です。データ量が多いとURL長の制限を超えるので15個データを表示するようにしました。

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
</head>
<body>
<img src='' id='chart' />
</body>
<script>
var drawImage = function( json ) {
    var base = 'http://chart.apis.google.com/chart?cht=bhg&chs=500x500';
    var count = [];
    var title = [];
    var j = 0;
    for( var i in json ) {
        title[j] = 't' + encodeURI ( i ) + ',333333,0,' + j + ',13,1';
        count[j] = json[i];
        j++;
        if( j == 15 ) {
            break;
        }
    }
    var titles = title.join( '|' );
    var counts = count.join( ',' );
    url = base + '&chd=t:' + counts + '&chdlp=tv|r&chxt=y,x&chm=' + titles;
    var img = document.getElementById( 'chart' );
    img.setAttribute( 'src', url );
}
</script>
<script src='./text.js'></script>
</html>
GoogleChartAPIの出力結果



文字が化けてしまって巧く表示されないケースもありますが、以前のNLTKで表示したグラフよりは奇麗に書けていると思います。単純にエクセルなどに落とし込んだ方がさらに奇麗に書けそうですが、今回はWebで試す事が目的だったのでこれにて終了です。

超絶簡単!JavaScriptの性質を10分で理解するための重要なポイントのまとめ

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScriptのニーズ

NodeJSやTitaniumMobileの普及によりサーバサイド/スマフォアプリの作成をJavaScriptで書こうとする動きが盛んです。それだけ注目を集めているせいかブログの記事でもJavaScriptのネタを書くとはてぶ登録されやすい傾向が現れます。一応今までJavaScript系の記事をいくつか書いてきたのでリンクを紹介します。

JavaScriptは記述の制限が緩い言語で記述者が自由に定義できてしまいます。自由度が高いという言葉は良いように聞こえますが、各人それぞれの志向に左右されることが多く、特に他人のコードを読む時に苦労します。今日はJavaScriptの性質を理解するためにdebugを仕込みながら勉強を進めたいと思います。

JavaScriptを理解する上で大切な事

個人的にJavaScriptの性質を理解する上で重要な事はObjectと関数を理解することだと認識しています。JavaScriptのほとんどのデータ定義がObjectで表現されます(関数、文字列、数値以外)。JavaScriptにはClassといった概念が無く、関数定義を他言語でのClassのConstructorのように扱い、Classメソッドのようなものをprototypeといった暗黙参照のObjectで定義します。左の言葉に少し違和感を感じられるかもしれません。後にコード例を示すのでそちらも参照してください。

配列/連想配列の定義

配列と連想配列は当然ながら別物です。配列はList型のデータに対して連想配列はKeyからValueを得るHashになります。JavaScriptの表記としても別々で扱いますが、両方ともObjectとしての性質を持ちます

配列の定義

配列の宣言はArrayもしくは[]を利用します。私はArrayで宣言か添字で入れる書き方をします。newを使っても使わなくても同じということであれば使用しない事にしています。また添字を文字列で入れるまたhashで一度宣言という方法は使った事が無いです。

// Arrayで宣言
var a = Array( 'a', 'b' );

// Arrayをnew
var a = new Array( 'a', 'b' );

// []に入れちゃう
var a = [ 'a', 'b' ];

// 添字で入れる
var a = Array();
a[ 0 ] = 'a';
a[ 1 ] = 'b';

// 添字を文字列で入れる
var a = [];
a[ '0' ] = 'a';
a[ '1' ] = 'b';

// hashで入れてArrayを継承させる
var a = { 0 : 'a', 1 : 'b', length : 2 };
a.__proto__ = Array.prototype;
連想配列の定義

連想配列の定義はObjectもしくは{}を利用します。。当然ながらKeyとValueが必要でKey:ValueやKeyを添字にしてValueを格納といった表現をします。配列のパターンと同じでnewをするしないで結果が同じであるのであればnewをしないようにしています。

// Objectで宣言
var o = Object();
o = { 0 : 'a', 1 : 'b' };

// Objectをnew
var o = new Object();
o = { 0 : 'a', 1 : 'b' };

// {}に入れちゃう
var o = { 0 : 'a', 1 : 'b' };

// keyを指定して入れる
var o = {};
o[ '0' ] = 'a';

// property参照させる
var o = {};
o.0 = 'a'; //これはエラーになる propertyはkeyが文字列でないと駄目な様子
o.a = 'a';  //これはOK
typeofで表示してみると

ArrayもObjectも両方typeofで表示するとObjectになります。int、string、functionも調べた結果を以下に載せます。

var a = Array();
alert( typeof a ); //object

var o = Object();
alert( typeof o ); //object

var i = 1;
alert( typeof i ); //number

var s = 'string';
alert( typeof s ); //string

var f = function(){};
alert( typeof f ); //function
Objectの拡張

上で示したObject(連想配列)を拡張した定義を行います。ObjectはKey:Valueの表現を利用して変数だけでなく関数の定義も行えます。拡張定義したObjectの関数をproperty参照を利用する事でメソッドのように利用できます。以下に例を記します。

var newObject = {
    property : 'Object', //連想配列の定義なのでカンマが必要です。
    echo : function() {   // 関数だって定義できます。
        alert( this.property );
    }
};
newObject.echo(); //Object

これを覚える上で忘れてはいけないのが連想配列だと言う事です。連想配列はKey:Valueのペアをカンマ区切りで格納します。関数も連想配列のルールに従えば定義できるという事です。私は上のような表記をしますがpropertyを使って以下のように記述する事も出来ます。

var newObject = Object();
newObject.property = 'Object';
newObject.echo = function() {
    alert( this.property );
}
newObject.echo();

関数

通常の定義

関数はfunctionで定義します。varで宣言するか関数をそのまま宣言するかの方法がありますが、JavaScriptでより多く使われるのは前者の書き方だと思っています。(他言語だと後者の書き方しかできないと思いますが。)

// varで宣言する
var f = function(){};

// 関数としてfを宣言する
function f(){}
無名関数

無名関数とはその名の通り名前の無い関数を定義できる仕組みです。まずは単純な例を記述しますがfunctionとだけ宣言して();にて実行します。Bookmarkletを作る時も良くこの記述をします。

(function(){
    alert( 'OK' );
})();

この無名関数を使うとクロージャが定義できます。クロージャの簡単な説明としてはグローバル空間に影響を与えないようにする仕組みです。よくある使い方としては無名関数で名前空間を一つ定義して、名前空間の下にObjectやクラスを定義するとincludeするJSファイル間で名前のバッティングを回避できます。以下は名前空間の下にObjectを定義して実行する例です。

var NameSpace = (function(){
    return {
        property : 'NameSpace Object',  
        echo : function() {
            alert( this.property );
        },
    };    
}
)();
NameSpace.echo(); // NameSpace Object
Class定義

JavaScriptにはClassという概念がありません。正確に言うとfunctionを基にClassっぽいものを表現しているだけです。functionでの宣言がClassのConstructorのような働きをします。

var Class = function() {      //Constructor
    this.property = 'Class';  //property
    this.echo = function() {  //method
        alert( this.property );
    }
};
new Class().echo(); //Class

実は上の書き方は良い例とは言えません。なぜならばConstructorの中にmethodを書いてしまっているからnewされる度にメソッドまでもが再定義されてしまいます。これを防ぐためにConstructorとmethodの定義を分離します。更にJavaScript特有のprototype属性(Object)を定義する事で暗黙的な参照を利用します。上の例を書き換えると以下のようになります。

var Class = function() {}; //Constructorだけ定義
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this.property );
    },
};
new Class().echo(); // Class

基本的にprototypeはConstructorの外に書きます。以下はprototypeが参照がうまく利用できない例と非推奨の書き方です。errorが出てしまうのはメソッドを呼び出すタイミングでundefinedとなります。Constructor内部でprototypeを定義すると新たにprototypeというオブジェクトを生成してしまう事になります。これだと暗黙参照ができません。それを回避する方法としてprototypeをConstructor内部でreturnすると思うように動作しますが、prototypeの暗黙参照の役割を考えるとConstructor内部にprototypeを定義すべきではありません。

// errorが発生。Constructor内部でprototypeを定義すると暗黙参照とは別物になってしまう。
var Class = function() { 
    this.prototype = {
        property : 'Class',    //propertyを定義
        echo : function() {    //methodを定義
            alert( this.property );
        },
    }; 
};
new Class().echo(); // undefined 
new Class().prototype.echo(); // これは動作する。新しいprototypeというpropertyを作ってしまっている

// 非推奨の書き方 Constructor内部にprototypeを記述している。
var Class = function() { 
    this.prototype = {
        property : 'Class',    //propertyを定義
        echo : function() {    //methodを定義
            alert( this.property );
        },
    }; 
    return this.prototype;
};
new Class().echo(); // Class
Classの継承

親クラスの継承は子クラスのprototypeにnewして定義するだけです。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property ); 
    },
};

//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
ChildClass.prototype.property = 'Child';
new ChildClass().echo(); //Child

上の例だと子Class側でChild.prototype.〜という定義を繰り返し記述しなければなりません。私はそれが面倒だと思うので無名関数とapplyを使ってChildClass.prototypeに追加します。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property );
    },
};
//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
(function(){
    this.property = 'Child';
    this.call = function() {
        alert( 'Call ' + this.property );
    };
}).apply( ChildClass.prototype );
new ChildClass().call();

本当は上の無名関数とapplyを使った例を以下のように書きたいのですがerrorが出ます。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property );
    },
};

//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
(function(){
    return {
        property : 'Child',
        call : function() {
            alert( 'Call ' + this.property );
        },
    };
}).apply( ChildClass.prototype );
new ChildClass().call(); //callが定義されていないというerrorが出ます。
Singletonで記述

一つのinstance生成で実行できるClassはnewにより余計なinstanceを生成させないようにします。Singletonはnewされても同一のObjectを返すようにClass側で制御するデザインパターンです。JavaScriptでも簡単に記述できます。arguments.calleeを利用するだけです。2回instanceを生成しようとしても同じものがClassから返されるので比較してみると同一のものを示します。

// SingletonClassの定義
var SingletonClass = function() {
    if( arguments.callee.singletonInstance ) {
        return arguments.callee.singletonInstance;
    }
    arguments.callee.singletonInstance = this;
};
// prototype
SingletonClass.prototype = {
    property : 'Singleton Class',
    echo : function() {
        alert( this.property );
    },
};
var i = new SingletonClass();
var i2 = new SingletonClass();
alert( i === i2 ); //true
Object.createによる生成

newの要/不要が色々なブロブで議論されていますが、crockfordさんが言うようにnewを出来る限り避けるという方針を考えますECMAScript 5th EditionからObject.createというメソッドが利用できるようになっています。これを基にObjectを生成します。Object.createのサポートが各種ブラウザで異なるので、定義されていない場合は自前で準備した関数を利用するように切り替えます。Object.createメソッドにClass.prototypeを入れます。

if (!Object.create) {  
    Object.create = function (o) {  
        if (arguments.length > 1) {  
            throw new Error('Object.create implementation only accepts the first parameter.');  
        }  
        function F() {}  
        F.prototype = o;  
        return new F();  
    };  
}  
var Class = function(){};
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this.property );
    },
};
Object.create( Class.prototype ).echo();

気をつけて欲しいのが上で紹介したSingletonパターンに対してObject.createを挟んでしまうと別Instanceとなってしまいます。上の例では生成instanceの比較が同じでしたがObject.createを入れるとfalseとして認識されます。原因はObject.create自体がSingleton化していないだけだと思います。

// SingletonClassの定義
var SingletonClass = function() {
    if( arguments.callee.singletonInstance ) {
        return arguments.callee.singletonInstance;
    }
    arguments.callee.singletonInstance = this;
};
// prototype
SingletonClass.prototype = {
    property : 'Singleton Class',
    echo : function() {
        alert( this.property );
    },
};
var i = Object.create( SingletonClass.prototype );
i.echo();
var i2 = Object.create( SingletonClass.prototype );
i2.echo();
alert( i === i2 ); //false
prototypeによる機能拡張

prototypeを利用すると既存Objectの機能を拡張する事が可能です。例えば配列に対して合計値を計算するようなメソッド(sum)を追加します。

Array.prototype.sum = function() {
    var sum = 0;
    for(var i = 0; i < this.length; i++ ) {
        sum += this[i];
    }
    return sum;
};
var a = [0,1,2,3,4];
alert( a.sum() ); // 10
thisについて

thisを指し示すものが非常にややこしいのがJavaScriptの特徴でもあります。結論を簡単に言うとthisを読み込む箇所によって全く性質が異なるものを示します。thisが参照するパターンは大きく分けて2つで、イベント発生源のObjectかObject自身のインスタンスです。何も定義されていない、もしくは関数の内部でthisを呼び出すとDOMWindowを示します。ClassやObject内部でthisを呼び出すと自身のインスタンスを示します。

alert( this ); //object DOMWindow

(function(){
    alert( this ); //object DOMWindow
})();

var Class = function(){};
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this ); 
    },
};
Object.create( Class.prototype ).echo(); //object object

var SampleObject = { 
    property : 'Object!',
    echo : function() {
        alert( this );
    },
};
Object.create( SampleObject ).echo(); //object object

html5のcanvasを使ってブラウザ上でのお絵描きやニコニコ動画風テロップを実装する

概要

html5canvasで遊んでみます。canvasの2dは学生の頃から使ってGoogleMap上にお絵描きできるシステムを作りました。またcanvasを巧く使えばニコニコ動画風のテロップも作れると思って今回実装してみました。次回は3dに挑戦したいです。

ブラウザ上でのお絵描き

仕組み

作り方は非常に簡単でhtmlにcanvasタグを埋め込み、それをJavascriptでdocument.getElementById( 'canvas' ).getContext( '2d' );とするだけでcanvasの2dオブジェクトが操作できます。mousedown,mousemove,mouseupのイベントを追加して、downしたら描画開始/moveしたら軌跡を残す/upしたら描画終了という処理の流れになります。当然の事ながらマウスのポイントを取得する必要がありevent.clientX/event.clientYで取得します。canvas上に軌跡を残すにはマウスポイントの一つ前の座標と新しい移動座標をからmoveTo、lineTo、strokeといったcanvasの2dオブジェクトが持っているメソッドを利用するだけです。jsも全部で50行ぐらいです。必要なことを箇条書きでも記します。

項目 簡易コード
canvas2dの取得 document.getElementById( 'canvas' ).getContext( '2d' );
マウスイベント追加 addEventListener( 'mousedown', Canvas.mousedown, false);
addEventListener( 'mousemove', Canvas.mousemove, false);
addEventListener( 'mouseup' , Canvas.mouseup , false);
マウスポイント取得 { x:e.clientX, y:e.clientY };
canvasに軌跡を書く var can = document.getElementById( 'canvas' ).getContext( '2d' );
can.beginPath();
can.moveTo(last.x, last.y);
can.lineTo(new.x, new.y);
can.closePath();
can.stroke();
ソースコード

html,javascriptともにgithubに上げました。

(function(){
    Canvas.init = function(){
        addEventListener( 'mousedown', Canvas.mousedown, false);
        addEventListener( 'mousemove', Canvas.mousemove, false);
        addEventListener( 'mouseup'  , Canvas.mouseup  , false);
    };

    Canvas.getObject = function() {
        return document.getElementById( 'canvas' ).getContext( '2d' );
    }

    Canvas.getPosition = function(e) {
        return { x:e.clientX, y:e.clientY };
    };

    Canvas.mousedown = function(e) {
        Canvas.attr.drawing = true;
        Canvas.attr.lastposition = Canvas.getPosition(e);
    };

    Canvas.mouseup = function(e) {
        Canvas.attr.drawing = false;
    };

    Canvas.mousemove = function(e) {
        if( Canvas.attr.drawing === false ) {
            return false;
        }
        Canvas.attr.position = Canvas.getPosition(e);
        var can = Canvas.attr.Object;
        can.beginPath();
        can.moveTo(Canvas.attr.lastposition.x, Canvas.attr.lastposition.y);
        can.lineTo(Canvas.attr.position.x, Canvas.attr.position.y);
        can.closePath();
        can.stroke();
        Canvas.attr.lastposition = Canvas.attr.position;
    };

    Canvas.attr = {};
    Canvas.attr.drawing = false;
    Canvas.attr.lastposition = { x:0, y:0 };
    Canvas.attr.position = { x:0, y:0 };
    Canvas.attr.Object = Canvas.getObject();
    Canvas.attr.Object.strokeStyle = 'black'; 
    Canvas.attr.Object.lineWidth = 10;
})();

ニコニコ動画風テロップ

仕組み

手書きの件よりも少し複雑で文字列の描画とその消去を繰り返す実装を行いました。描画と消去の呼び出しをsetIntervalで定期的に関数を呼び出すのですが、setInterval関数の第二引数の間隔が重要になります。短くした方が描画がスムーズで今回は0.025秒間隔で文字列再描画のx座標を減らしていきます。文字列をcanvas上に描画するためにfillStyleプロパティとfillTextメソッドを利用します。また消去するためにはclearRectメソッドにてcanvas上のデータ全部を削除します。箇条書きで項目を記載します。

項目 簡易コード
canvas2dの取得 document.getElementById( 'canvas' ).getContext( '2d' );
文字列描画 var can = document.getElementById( 'canvas' ).getContext( '2d' );
can.fillStyle = 'black';
can.fillText( text, text.start.x - text.offset , text.start.y );
文字列消去 var can = document.getElementById( 'canvas' ).getContext( '2d' );
can.fillStyle = 'white';
can.clearRect( 0, 0, 800, 800 );
ソースコード

html,javascriptともにgithubに上げました。

(function(){
    Nico.init = function() {
        if( Nico.attr.timer ) {
            clearInterval( Nico.attr.timer );
        }
        Nico.attr.timer = setInterval( 'Nico.play()', 25 );
    };

    Nico.getObject = function() {
        return document.getElementById( 'canvas' ).getContext( '2d' );
    };

    Nico.play = function() {
        Nico.remove();
        for( var i=0; i<=Nico.attr.text.length-1; i++ ) {
            var data = Nico.attr.text[i];
            Nico.attr.Object.fillStyle = 'black';
            Nico.attr.Object.fillText( data.node, data.start.x - data.offset , data.start.y );
            Nico.attr.text[i].offset += 7;
        }
    };

    Nico.remove = function() {
        Nico.attr.Object.fillStyle = 'white';
        Nico.attr.Object.clearRect( 0, 0, 800, 800 );
    };

    Nico.attr = {};
    Nico.attr.Object = Nico.getObject();
    Nico.attr.Object.font = '40pt Arial';
    Nico.attr.text = Array();
    for( var i = 0; i<=10; i++ ) {
        Nico.attr.text[i] = {};
        var prefix = '';
        for( var j = 0; j<=i; j++ ) {
            prefix += '    ';
        }
        Nico.attr.text[i].node = prefix + "(´;ω;`)";
        Nico.attr.text[i].start = { x:800, y:80*i };
        Nico.attr.text[i].offset = 0;
    }
})();

JavaScript Ajax : XmlHttpRequestのLevel2でSameOriginPolicyを回避する

クロスドメイン制限の回避について

  • 今まではXHR(XmlHttpRequest)の仕様によりJSを読み込んでいるHTMLファイルがあるサーバから異なるドメインサーバへのAjaxリクエストが制限されていました。Same Origin Policyと呼ばれているものです。Same Origin Policyの役割としては悪意のあるscriptが個人情報等を他のサイトに転送する事を防ぐためです。このセキリティ制限を回避するために多くの人が代表的なJSONP(JSON with Padding)を利用してサーバサイドでクライアントのコールバック関数をechoしてクライアント側で実行されることにより、クロスドメイン間のAjax通信をそれっぽく動くように対応していたと思います。
  • JSONPについては以前記事を書いたので宜しければ参照してください。20秒で理解するJSONP - Yuta.Kikuchiの日記 はてなブックマーク - 20秒で理解するJSONP - Yuta.Kikuchiの日記
  • JSONPを利用するのは少し面倒でコールバック関数をリクエストされたサーバサイドから返却しないといけなく、またセキュリティ面でも不安で十分に気をつける必要があります。JSONP以外にもリバースProxyを用意する、Flashを経由するとクロスドメインリクエスト可能となりますがこの面倒な事に頭を悩ませるJavascripterも多かったと思うのですが、色々と調べてみるとXHRのLevel2の仕様によりJSONPを使わずとも異なるドメイン間でのAjaxリクエストが可能となりそうです。

制限を回避するために

Access-Control-Allow-Origin
  • HTTP access control - MDN はてなブックマーク - HTTP access control - MDNにあるようにAccess-Control-Allow-OriginというHttpResponseHeaderを仕込むとクロスドメインを回避する事ができるようです。このHttpResponseHeaderをどこに仕込むのか?ということが疑問になりそうですが、Ajaxリクエスト先のAPIサーバに設置します。(リクエスト元に設定する方式だとXSRFなどが好き勝手出来てしまうのでそれは無いですね)
  • Access-Control-Allow-Originにリクエストを受け付けるURLを指定、もしくは*(ワイルドカード)を指定すると全てのURLを受け付ける設定になります。
関連Header一覧
  • 色々なブラウザ仕様があるのでAccess-Control-Allow-Originだけでなく、念のため以下のResponseHeaderも設定しておくと動作が確認できるようです。
    • Access-Control-Allow-Origin : 上で説明した通りでアクセス元のURLを指定します。*(ワイルドカード)指定可能です。
    • Access-Control-Allow-Methods : GET,POST,PUT,DELETE,OPTIONSなどの受け付けるRequestMethodを指定します。
    • Access-Control-Allow-Headers : RequestHeaderに仕込んである値を見て許可する内容を指定します。*(ワイルドカード)指定可能です。
    • Access-Control-Max-Age : 各種OptionHeaderの有効時間を設定します。

Demo

今回のAPI/ClientともにサンプルコードをGitHubに挙げました。https://github.com/yutakikuchi/JS/tree/master/crossdomain

API Server
  • アクセスするAPIサーバをGoogle App Engineに置きます。上で説明したHeaderを仕込み、とりえあずは簡単な文字列(CrossDoaminRequest)だけをprintします。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.ext.webapp import template, util

class CrossDomain(webapp.RequestHandler):
  def get(self):
    self.response.headers[ 'Content-Type' ] = 'text/plain'
    self.response.headers[ 'Access-Control-Allow-Origin' ] = '*' 
    self.response.headers[ 'Access-Control-Allow-Methods' ] = 'GET'
    self.response.headers[ 'Access-Control-Allow-Headers' ] = '*' 
    self.response.headers[ 'Access-Control-Allow-Age' ] = '86400'
    self.response.out.write( 'CrossDoaminRequest' )

def main():
  application = webapp.WSGIApplication([('/CrossDomain', CrossDomain)])
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()
API出力Header
http://mobiles-proxy.appspot.com/CrossDomain

GET /CrossDomain HTTP/1.1
Host: mobiles-proxy.appspot.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:8.0.1) Gecko/20100101 Firefox/8.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Connection: keep-alive

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/plain
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET 
Access-Control-Allow-Headers: *
Access-Control-Allow-Age: 86400
Expires: Fri, 01 Jan 1990 00:00:00 GMT 
Content-Encoding: gzip
Vary: Accept-Encoding
Date: Sat, 25 Feb 2012 08:29:03 GMT 
Server: Google Frontend
Content-Length: 38
Request Client
  • IEでも動くようにXDomainRequestとXMLHttpRequestをそれぞれ使い分けるようなコードを書きます。色々と試してみたところクライアント側でAPI ServerのResponseを取得した時にAccess-Control-Allow-Originの値にて許可されたサイトであることを判定しているようです。
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<title>CrossDomainAjax</title>
<script>
var Request = function() {
    var url = 'http://mobiles-proxy.appspot.com/CrossDomain';
    var XHR;
    // IE対策
    if( window.XDomainRequest ) {
        XHR = new XDomainRequest();
        XHR.onload = function(){ 
           echo( XHR.responseText )
        };
        XHR.open( 'GET', url );
        XHR.send();
    // IE以外
    } else {
        XHR = new XMLHttpRequest();
        XHR.onreadystatechange = function(){
            if( XHR.readyState === 4 && XHR.status == 200 ) 
                echo( XHR.responseText );
            };
        XHR.open( 'GET', url, true );  
        XHR.send();
    }
}
var echo = function( text ) {
    alert( text );
}
</script>
</head>
<body>
<input type='button' name='ajax' value='CrossDomainRequest' onClick='javascript:Request();' />
</body>
</html>
実行検証
  • いくつかのBrowserで動作確認をします。環境がMacなのでIEは試していません。
  • 結果SafariAccess-Control-Allow-Originが無くても実行が出来てしまいました。またOperaは動作しませんでした。
Browser Access-Control-Allow-Origin Access-Control-Allow-Methods Access-Control-Allow-Headers Access-Control-Allow-Age
Safari ver 5.1.2 不要 不要 不要 不要
Chrome ver 17.0.963.56 不要 不要 不要
FireFox ver 10.0.2 不要 不要 不要
Opera ver 11.61 - - - -

まとめ

  • JSONPを用いなくてもXHR Level2を利用するとCrossDomainRequestが可能です。
  • XHR Level2を利用するにはAccess-Control-Allow-OriginというResponseHeaderをアクセス先のサーバが返す必要があります。
  • IEで動作させるにはXDomainRequestオブジェクトを生成します。
  • 各種ブラウザによって挙動が異なり、SafariAccess-Control-Allow-Originさえ無くても動き、Operaは未対応です。
  • 今回の検証サンプルコードを以下に設置しました。https://github.com/yutakikuchi/JS/tree/master/crossdomain

HerokuでNodeJSを動かしてみる


前提

準備

nodejs/npmのバージョン確認
$ node -v
v0.4.8

$ npm -v
1.0.6

Herokuパッケージ設定手順

Herokuのinstall
$ sudo gem update --system
$ sudo install heroku
アプリ一覧を表示

まだ何も登録していないのでYou have no apps.と表示される。

$ heroku list
Enter your Heroku credentials
Email:  hoge@gmail.com
Password: 
Found existing public key: /Users/YutaKikuchi/.ssh/id_rsa.pub
Uploading ssh public key /Users/YutaKikuchi/.ssh/id_rsa.pub
You have no apps.

必要なファイル

ファイル構成
.git
.gitignore
Procfile
socketchat.js
node_modules
package.json
Procfile
  • 新しいHerokuの環境となったCedarから取り入れれたファイル。以下のように記述
web: node socketchat.js
package.json(Nodeを使う場合に必要)
  • アプリの説明やNPMの依存を記述する。
{
  "name": "socketchat",
  "version": "0.0.1",
  "dependencies": {
    "express": "2.2.0"
  }
}
実行するNodejsファイル
  • socketchat.jsというファイルにて作成。
  • とりあえず動く事を確認したいのでHerokuにあるサンプルプログラムをそのまま記述する。
var express = require('express');

var app = express.createServer(express.logger());

app.get('/', function(request, response) {
  response.send('Hello World!');
});

var port = process.env.PORT || 3000;
app.listen(port, function() {
  console.log("Listening on " + port);
});
.gitignore
  • GitHubへの登録を無視する設定ファイル。
node_modules

GitHubへの登録

localhostで動作確認
  • expressを使うのでnpmでinstallしておく
$ sudo npm install express
mime@1.2.4 ./node_modules/express/node_modules/mime
qs@0.4.0 ./node_modules/express/node_modules/qs
formidable@1.0.8 ./node_modules/express/node_modules/connect/node_modules/formidable
connect@1.8.2 ./node_modules/express/node_modules/connect
express@2.2.0 ./node_modules/express
$ node socketchat.js 
Listening on 3000
GitHubにコミット
$ git init
$ git add .
$ git commit -m "init"
$ git remote add origin git@github.com:yutakikuchi/Node.git
$ git push -u origin master

HerokuへDeploy

Herokuにアプリケーションを作成
  • socketchatという名前で登録する。
  • --stack cedarとすると.heroku.comというドメインではなく、.herokuapp.comという名前で作成される。
$ heroku create socketchat --stack cedar
Creating socketchat... done, stack is cedar
http://socketchat.herokuapp.com/ | git@heroku.com:socketchat.git
Git remote heroku added
HerokuにアプリケーションをDeploy
$ git push heroku master
Counting objects: 10, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (10/10), 1022 bytes, done.
Total 10 (delta 1), reused 0 (delta 0)

-----> Heroku receiving push
-----> Node.js app detected
-----> Fetching Node.js binaries
-----> Vendoring node 0.4.7
-----> Installing dependencies with npm 1.0.94
       express@2.2.0 ./node_modules/express 
       ├── qs@0.4.0
       ├── mime@1.2.4
       └── connect@1.8.2
       Dependencies installed
-----> Discovering process types
       Procfile declares types -> web
-----> Compiled slug size is 6.9MB
-----> Launching... done, v4
       http://socketchat.herokuapp.com deployed to Heroku

To git@heroku.com:socketchat.git
 * [new branch]      master -> master
コマンドラインからブラウザ起動
$ heroku open

http://socketchat.herokuapp.com/にてHellow World!が表示されることを確認。

次回

Nodeを書いてもう少しちゃんと動くものを提供する予定。