日本語は1文字何バイト?

こんにちは、wakです。秋ですね。寒いですね。

さて、今日もどこかから「英語は1文字1バイト、日本語は2バイト」といった雑な話が耳に入ってきて、「UTF-8で日本語はだいたい1文字3バイト!」と抗議していたのですが、エンジニアとして「だいたい」という言葉を使うのもまた雑な話です。どんな例外があるのかをまとめておくことにしました。

f:id:nurenezumi:20171109173329j:plain

1匹あたり数兆個の細胞からなる猫

基礎知識

コードポイント

Unicodeでは世界中全ての文字に個別のコードを振っています(これをコードポイントと呼びます)。アルファベットでもひらがな・漢字でも、絵文字でもヒエログリフでも全部です。このコードポイントは通常16進数で表し、 U+FFFF の形式で書きます。たとえば「A」なら 0x41 なので U+0041*1、「あ」なら U+3042 です。JavaScriptでは "\u0041", "\u3042" などと書けば直接文字リテラルを書いたのと同じことになります。

let hoge = "\u3042"; // let hoge = "あ"; と書くのと同じ

UTF-8の文字エンコーディング

Unicode以前の文字コードSJISEUCなどでは文字コードと文字エンコーディング(=バイト列として表現する方法)とが同一でした。たとえばSJISで「ソ」の文字コード0x835C ですので、ファイルに 0x83 0x5C と書けばSJISで「ソ」と書いたことになります*2

UTF-8では、この文字エンコーディング方法にちょっと面倒な方法を採用しています。つまり、コードポイントの範囲によってバイト数が変わるのです。

  • 1バイト: U+0000U+007F (ASCII文字。例: 「A」)
  • 2バイト: U+0080U+07FF (主にギリシャ文字アラビア文字など。例: 「¶」「Ψ」)
  • 3バイト: U+0800U+FFFF (日常的に使うほとんどの文字はここ)
  • 4バイト: U+10000U+1FFFFF (その他)
  • 5バイト: U+200000U+3FFFFFF (未使用)
  • 6バイト: U+4000000U+7FFFFFFF (未使用)

U+0000-U+FFFF までの文字を基本多言語面(BMP)と呼びますが、このBMPに入っていない文字は全てUTF-8で4バイトになります*3。また、ちょっと盲点になることもあるのですが、上に示したようにギリシャ文字などは2バイトになります。

これを踏まえた上で、さらに2つのUnicodeの仕様を見ていきましょう。

異体字セレクタ

「ワタナベ」さんの「邊」(U+908A)には細かいバリエーションがあることはご存知でしょう。たとえば二点しんにょうが一点しんにょうになった「邊󠄄」(U+908A U+E0104)、右上の「自」が「白」になった「邊󠄆」(U+908A U+E0106)などです(環境によっては全く同じように表示されるため、手持ちのAndroidで表示したスクリーンショットを貼っておきます)。

f:id:nurenezumi:20171109163959p:plain

これら異体字の数は数十種類にもおよび、別々のコードポイントを振ることは現実的ではありません。そこで、まず基底文字(基本となる文字)「邊󠄆」(U+908A)を書き、続けて見えない制御文字をもう1文字書くと字体が変わるという仕組みが作られました。これが異体字セレクタです。

つまり、「邊󠄄」(U+908A U+E0104)は1文字に見えますが実際は2文字分で、しかも2文字目はBMPからはみ出していて4バイトになるので、計7バイトになります。原理的には1文字8バイトまで行きます。

結合文字

Unicodeには、単独では使われず、他の文字とくっつけるための結合文字があります。たとえばひらがなの「か」(U+304B)に結合文字の半濁点(U+309A)を繋げると「か゚」(U+304B U+309A)という見慣れない文字になります。これは1文字ですが、実体としては2文字なので、こちらもやはりUTF-8で6バイトになります。

しかも結合文字には「何文字まで」という限度がありません。「か゚」の後ろに結合文字の「○」(U+20DD)を続けて書くと、1文字9バイトのこんな文字になります(今度はWindowsのWordpadで表示した画面キャプチャを示します)。

f:id:nurenezumi:20171109164024p:plain

こうなってくると「1文字は何バイトか」という議論自体がナンセンスになってきますね。

絵文字

泥沼に踏み込むことになるのでここでは触れませんが、絵文字ではこの異体字セレクタと結合文字がふんだんに使われています。絵文字をパーツごとに組み立てていくような仕組みが採られているため、「1文字」で何バイトになるか見当も付きません。この記事が綺麗にまとまっていました。

qiita.com

結論

というわけで、UTF-8ではどのような文字が3バイト以外になるかをまとめます。

ASCII文字

いわゆる半角英数字と記号は1バイトです。これはいいでしょう。

ギリシャ文字アラビア文字など

Wikipedia一覧がありました。この U+0080U+07FF の間の文字は2バイトになります。

第3・第4水準漢字の一部

JIS X 0213に含まれる第3・第4水準漢字の大半はBMP外に収録されています。具体的には「𠀋」(U+2000B) はBMPに入らない文字の一つで、UTF-8では4バイトになります。

異体字

「邊󠄄」(U+908A U+E0104)、「邊󠄆」(U+908A U+E0106)などの異体字です。1文字に見えますが実体は2文字なので最大8バイトになります。

結合文字

いくらでも文字がくっつくため、1文字何バイトになるか分かりません。「1文字」扱いすべきかどうかは要件によります。

まとめ

これですっきりしました。「日本語のほとんどはUTF-8で3バイトになる。ただし第3・第4水準漢字の大半は4バイト。記号・結合文字は最低3バイト。あとギリシャ文字とかは2バイトだよ」と言えばいいのですね。分かっているつもりのことでもきちんと調べると気持ちが良いものです。それでは。

*1:ASCIIコードと同じ

*2:この2バイト目は偶然にもSJISの「\」(0x5C)と一致しているため、様々な問題を引き起こしていました。興味のある人は「ダメ文字」で検索しましょう

*3:5バイト・6バイトになる領域にはまだ文字が定義されていません

Fusion Tablesを使ってGoogleの環境だけでデータ分析する

サーバーを用意せずに、お金もかけずにさくっとデータ分析をする。データベース、分析実行、結果出力まで、全てGoogleのサービスだけを使って実現する方法を紹介します。

はじめに

前提条件

G Suiteユーザー

こんな人向け

データ分析をするために、例えばBIツールを入れるのはコストがかかりますよね。そこまでしたくない場合、自前で管理画面に作ったり。それもエンジニアが開発してリリースして…というので柔軟性とスピードに欠ける。とはいえ、Excelでやるにはデータ量が多すぎてできない。今回のFusion Tablesを利用した方法は手軽にやりたい時にピッタリです。

環境構築がいらない、コストがかからない、外部からアクセスできてブラウザだけで全て構築できる、権限設定もGoogleアカウントでできる、かなりメリットあります。特に出張や外出の多い私には最適だったりします。ただし、もちろんダメなところもたくさんあるので、それも最後に紹介しておきます。

G Suiteで使うアプリケーション

ものづくり

分析対象のデータをFusion Tablesに格納する

まずはこの辺を読んでFusion Tablesの基礎を。
qiita.com

分析対象のデータを突っ込みましょう。

本当はここでSQL書いたりView作ったりできれば良いんですけど、SQL書けないし、View作るにしてもものすごく単純な条件でしか作れない。ということで、Tableにデータを格納する、くらいしか使えません。

ということで、Google Apps Scriptを使います。

Google Apps Scriptで分析処理を書く

スプレッドシートでコードエディタを開いても良いんですけど、今回はドライブにスクリプトを配置します。分析元のデータや出力先のスプレッドシートが複数でも管理しやすいためです。また、出力用のスプレッドシートも作成しておきます。
f:id:jabe20:20171108104901p:plain

Google Apps ScriptでFusion Tablesを操作できるように、リソースを追加します。
qiita.com
またまたこの人のブログに頼っちゃいます。笑 分かりやすい。

これで、スクリプトからデータを取り出すところまでできました。ただ、使えるSQLはかなり限定されていて、例えば、、、

  • CONCATに当たる構文が無いため、文字列操作がほとんどできない。
  • CASEが使えないため、条件に応じた抽出ができない。
  • INSERT INTO ~ SELECTができないため、一時テーブルのようなものは作成できない。

などなど。詳細は下記リファレンス参照。
Row and Query SQL Reference  |  Fusion Tables REST API  |  Google Developers

なので、SQLで色々やって効率よく処理ができないのがけっこう辛いです。データを全部取得して、スクリプトの中でぐるぐる回して処理することになります。

スプレッドシートに出力する

下記ブログの後半部分参照。
sitest.jp
(というか、これFusion Tables作成のところから一通り書かれてるからこのブログ見たら一発かもしれないけど…笑)

デバッグについて

Google Apps Scriptでは、ブレークポイントを置いてデバッグもできます。
f:id:jabe20:20171108111021p:plain
こんな風に変数の中身チェックしたり、
f:id:jabe20:20171108111107p:plain
ステップイン、ステップオーバー、ステップアウトも。

実行時エラーはこんな感じで。
f:id:jabe20:20171108111532p:plain

SQLエラーも一応出ます。分かりづらいけど…
f:id:jabe20:20171108111711p:plain

ダメなところ

データベースから大量データを取得できない
一度に大量データを取得しようとすると実行時エラーになります。そうなるとデータを複数回に分けて取得しなければなりません。ちゃんとサイズは測っていませんが、1レコード150byteくらいのデータ約15万件は2分割でもダメで、3分割しました。

Google Apps Scriptの処理時間は6分まで
なんか無理矢理やってる人もいますが、強引すぎる…笑
kido0617.github.io
処理時間が短くなるよう、普通に処理を分けましょう。

一度に大量のデータを出力しようとすると動作が不安定になるらしい
分析結果や、分析途中のデータをFusion Tablesのテーブルに出力することもできますが、これはお勧めできません。一度に大量のデータを出力しようとすると動作が不安定になるらしいので。
qiita.com
この人、よく試してる~

制限
リファレンスにあるとおり、Fusion Tablesには以下の制限があります。
f:id:jabe20:20171108113557p:plain
※一応Google Apps Script関連の制限はここで。
Quotas for Google Services  |  Apps Script  |  Google Developers

こんな感じでデメリットも多く感じますが、メリットの方が大きいので、けっこう活躍できるんじゃないかなーと思います。あとは、Fusion Tablesの今後には期待したいところ。

Node.jsからmultipart/form-dataでデータをPOSTする

お久しぶりです。最近Node.jsばかり書いているwakです。

さて、最近こういうことがありました。

  1. Slackにスニペットをアップロードするするスクリプトを書きたい
  2. Slack botトークンをスクリプトには埋め込みたくない
  3. そうだ! Lambdaの中にトークンを書くことにしよう!

というわけで、こういう流れになりました。

  1. スクリプトはLambdaにパラメーターを渡して実行する
  2. LambdaはSlackのエンドポイントをHTTP POSTで叩いてスニペットをアップロードする

つまり、Node.jsからPOSTでファイルアップロードを行いたいのですが、ちょっと探した範囲では 素の https.request でこれを行っているサンプルが見つかりませんでした。各種ライブラリが何をしているか分からないのも気持ちが悪いので、自分で書いたコードを残しておきます。

f:id:nurenezumi:20171102092624j:plain

猫パンチを送信しようとしている猫

やりたいこと

export token="xoxb-000000000000-xxxxxxxxxxxxxxxxxxxxxxxx"
export channel="channel_name"
export filename="filename"
curl -F file=@test.zip -F channels=$channel -F token=$token https://slack.com/api/files.upload -F title=$filename

と同じデータを送信したい。内容はバイナリかもしれない。

結論

コードは次の通りです。

"use strict"

const https = require("https");

let binaryArray = [0x00, 0x40, 0x01, 0x41, 0x02, 0x42, 0x03, 0x43]; // ファイルの内容のバイト配列
let buffer = Buffer.from(binaryArray); // バイト配列をそのままBufferに
let content = buffer.toString("ascii"); // 1バイトずつstringへ変換

let options = {
    host: "slack.com",
    port: 443,
    path: "/api/files.upload",
    method: "POST",
    headers: {
        "Content-Type": "multipart/form-data; boundary=------------------------deadbeefdeadbeef"
    }
};

let req = https.request(options, res => {
  res.setEncoding("utf8");
  res.on("data", (chunk) => {
    // レスポンスを受け取って何か処理をしたいならここでやる
  });
});
req.on("error", (e) => {
  console.error(e.message);
});
req.write(`--------------------------deadbeefdeadbeef\r
Content-Disposition: form-data; name="file"; filename="filename.dat"\r
Content-Type: application/octet-stream\r
\r
${content}\r
--------------------------deadbeefdeadbeef\r
Content-Disposition: form-data; name="channels"\r
\r
channel_name\r
--------------------------deadbeefdeadbeef\r
Content-Disposition: form-data; name="token"\r
\r
xoxb-000000000000-xxxxxxxxxxxxxxxxxxxxxxxx\r
--------------------------deadbeefdeadbeef\r
Content-Disposition: form-data; name="title"\r
\r
filename\r
--------------------------deadbeefdeadbeef--\r
`);
req.end();

ここではあえてパラメーターの名前や値を直接ベタ書きしていますが、動的にやるのも別に難しくないでしょう。簡単ですね。

なお、

  • テンプレートリテラルの改行コードはプラットフォームによらずLF(\n)に統一される
  • multipartでデータを送信する際に使用する改行コードはCRLF(\r\n)とされている

という違いから、面倒だとは思いながらも各行の末尾に \r を書いています*1replace(/\n/g, "\r\n") などとやって一括置換をかけると content の中身まで置換される可能性があるのでやめましょう。

説明

上で示したように

curl -F file=@test.zip -F channels=$channel -F token=$token https://slack.com/api/files.upload -F title=$filename

こんなPOSTを送信すると、サーバーには以下に示すようなデータが送信されます。

ヘッダ部

重要な値として Content-Type があります(というか重要なのはこれだけです)。

Content-Type: multipart/form-data; boundary=------------------------deadbeefdeadbeef

これは、ボディ部にmultipartで複数のデータ(ファイルの内容、その他のパラメーター)が送信されること、そしてそれぞれの区切り目(境界区切子)が何かを示しています(RFC 2046)。 boundary の書式にはややこしい制約があるようなのですが、ハイフンをたくさん並べた後ろにランダムな英数字*2を書けば間違いはありません。

ボディ部

前半はファイル名とその内容、後半はその他のパラメーターです。それぞれの値は「ハイフン2個 + boundary の値 + CRLF」で区切ることとされているので、よく見ると boundary の値とはハイフンの数が違います。また、一番最後にはハイフンを2個付けます。

--------------------------deadbeefdeadbeef
Content-Disposition: form-data; name="file"; filename="test.zip"
Content-Type: application/octet-stream

<バイナリデータ>
--------------------------deadbeefdeadbeef
Content-Disposition: form-data; name="channels"

channel_name
--------------------------deadbeefdeadbeef
Content-Disposition: form-data; name="token"

xoxb-000000000000-xxxxxxxxxxxxxxxxxxxxxxxx
--------------------------deadbeefdeadbeef
Content-Disposition: form-data; name="title"

filename
--------------------------deadbeefdeadbeef--

テキストファイルの場合は text/plain にできます。文字エンコーディングはコード中で指定している通り(この場合はUTF-8)です。

--------------------------deadbeefdeadbeef
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

テキストデータ

オチ

Lambdaの環境には curl コマンドも入っているので、ファイルを /tmp あたりに作ってそのまま実行すれば済んだりします。ただしコンテナは使い回される可能性が高いため、作成したファイルは必ず削除しておきましょう。

*1:実際にはSlackはLFのみでも受け付けてくれましたが

*2:ファイルやパラメーターの内容とかぶらないように

Analytics の内容を Slack でグラフ表示

こんにちは、久しぶりに和朗です。

サービススタート時など、Google アナリティクスの情報を見ていると面白い!!とは思いつつも、他の仕事をしているとなかなか毎日の変化に気づけません。 アナリティクスの情報を定期的にかつ視覚的に見る方法はないものかと考えました。 そこで、毎日使っているSlackとAnalyticsの間にspreadsheetsとapps scriptをいれてグラフ表示を実現しました。

f:id:teradak:20170519165901p:plain

Google Analytics の情報を取り込む

アドオンの入手

f:id:teradak:20170517193648p:plain スプレッドからGoogle Analyticsのアドオンを利用できるように設定します。

GAの情報をスプレッドに取込み

f:id:teradak:20170517193644p:plain

Create new reports を選択

f:id:teradak:20170517193650p:plain

ログインアカウントでGAのデータが参照できる状態になっている必要があります。 参照したいGAの情報がなければ、共有してもらいましょう!

f:id:teradak:20170517194620p:plain

レポート取得のための設定情報シートが作成されます。 ※複数サイトの情報を横並びに設定できます。 上記の設定は、過去2週間分のメトリクスを日ごとに取得してくるようになっています。

日毎に最新情報を取り込むようにスケジュール

f:id:teradak:20170517194935p:plain

Schedule reports を選択

f:id:teradak:20170517195107p:plain

毎日4-5時に実行 これで常に最新の値が見れるようになります。

グラフ作成

f:id:teradak:20170517195538p:plain

これは簡単ですね!データ探索を使うと面白いグラフが見れるかもしれませんw

グラフをSlackへポストする

Slackにアクセスするためのトークンを用意

f:id:teradak:20170519171727p:plain

スプレッドのグラフをファイルデータにする

スプレッドの「ツール>スクリプトエディタ」を利用してグラフをファイルデータにします。

var sheet = book.getSheetByName('[シート名]');
var chartImage = sheet.getCharts()[0]
    .getBlob().getAs('image/png')
    .setName("chart.png");

Slackにポスト

var filesUpload = 'https://slack.com/api/files.upload';
var token = '[トークン]';
var payload = {
'token' : token,
'channels': channel,
'file':chart,
'filename': name,
'initial_comment':url
};

var params = {
'method' : 'post',
'payload' : payload
};

var response = UrlFetchApp.fetch(filesUpload, params);

日毎に最新情報をポストするようにスケジュール

f:id:teradak:20170517202815p:plain

f:id:teradak:20170517202944p:plain

完成

f:id:teradak:20170518150021p:plain

まとめ

今回はじめてSlackへの画像送信を使ってみました。画像を都度作るのが大変な場合もあるかもしれませんが、視覚的に判断できる色や形で情報を共有できるということはメリットだと感じました。

参考サイト

web-tan.forum.impressrd.jp

engineer.crowdworks.jp

Class Blob  |  Apps Script  |  Google Developers

files.upload method | Slack

API Key認証付きのAPI GatewayからS3へリダイレクトしたい(できない)

こんにちは、AWS担当のwakです。

概要

表題の通りです。できない理由を書きます。また、ついでにSame-Origin PolicyとCORS (Cross-Origin Resource Sharing)についても簡単に解説します。

f:id:nurenezumi:20170310124638j:plain

猫もおてあげ

やりたかったこと

とあるデータ(刻々と追加されていきます。更新はなし)をDynamoDBに溜め込んでいます。そのデータを検索して返すWeb APIAPI Gateway + Lambdaで実装しました。

ところで、これらのデータは1日単位でS3にJSON形式で書き出してアーカイブとして保存してあります。もし検索条件が日付だけなら、わざわざDynamoDBを見るまでのこともありません。「S3のこのURLを見てね」と言えばいいだけです。つまり、こういうことができると都合がいいわけです。

  1. API Gateway経由でLambdaを起動する
  2. 条件が複雑な場合は普通にDynamoDBを検索して返す
  3. 条件が日付だけの場合はHTTP 303 See OtherでS3にリダイレクトする

3番目は無理でした、というのが今回の記事の趣旨になります。以下、その説明をします。

Same-Origin Policy

ブラウザには、RFC 6454で定められたSame-Origin Policy(同一オリジンポリシー、同一生成源ポリシー)というルールがあります。ここでは「オリジン」が何かはちょっと後回しにして、次のようなシナリオを考えてみます。

  • mycompany.local という社内Webサーバーがあるとします。このWebサーバーは外部には公開されていません
  • 私はここなら安心だと思って、秘密のデータを http://mycompany.local/himitsu.json として置いておきます。
  • 悪い人がいて、どこからかこのURLを入手したとします。
  • 悪い人は http://evil.com/dorobou.html というWebページをインターネット上に作り、何らかの方法*1で私にアクセスさせます。
  • このWebページにはJavaScriptが書いてあります。スクリプトはWebページを開くと同時に勝手に上記のURLにXHRでアクセスし、取得したデータを全て外部に送信してしまいます。
  • 私は死にます。
f:id:nurenezumi:20170310123858p:plain

このようなことではいけません(私は死にたくありません)。もちろんこの秘密のデータを他のWebサイトから参照したいような状況もありますが、少なくとも全ての外部Webサイトのスクリプトからアクセスできる必要はまったくありません。そこで、スクリプトは原則として「同一のオリジン(場所)」にあるリソースにしかアクセスできないというルールができました。これが同一オリジンポリシーです。

オリジンとは

オリジンという言葉が突然出てきましたが、これは大ざっぱに言えばドメインのことだと理解すればいいです。厳密にはRFC 6454で定義されていて、

  • HTTP/HTTPSの区分
  • FQDNexample.comとか、www.example.comとか)
  • ポート番号

をセットにしたものです*2。つまりhttp://evil.com上にあるスクリプトは、次のような場所にあるリソースにはアクセスできないわけです。

  • http://mycompany.localドメインが全然違う)
  • https://evil.com(同じに見えるけどHTTPとHTTPSが違う)
  • http://evil.com:8080(ポート番号が80番と8080番で違う)
  • http://www.evil.com(これも別扱い)

これだけ厳密なチェックが入るなら安心ですね。めでたしめでたし。

CORS (Cross-Origin Resource Sharing)

ところが、異なるオリジンをまたいで外部リソースを参照することが一切許されないのでは不都合なことも多々出てきます。そこで、リソースが置いてある側、つまりmycompany.local側で事前に設定を行い、どのようなタイプのリクエストなら許可するかをホワイトリスト形式で指定することになりました。これがCORS (Cross-Origin Resource Sharing)です。

具体的には、ブラウザからのリクエストに応じ、リソースを置いてあるサーバーがHTTPヘッダで「どこからのアクセスなら許可するか」「リクエスト時に含まれていても良いHTTPヘッダ」のような項目を回答します。ブラウザはこの値をチェックし、リクエストの内容がそれとマッチするときにだけアクセスを許可します(そうでないならアクセスをブロックしてネットワークエラーを発生させます)。この制御により、リソースの所有者の意図、ブラウザの利用者の意図に反してアクセスが行われてしまうことを防ぐわけです。

余談:CORSは何でないか

CROSはあくまでブラウザが(いわば)自主規制をするだけのもので、ユーザーが意図的に発生させたリクエストをブロックするようなものではありません。悪意を持って作成されたスクリプトを実行してしまっても、リソースの所有者やユーザーが意図しないうちに勝手にリクエストが発生してしまう(発生させられてしまう)ことを防ぐための仕組みです。セキュリティについてはサーバーサイドできちんと考える必要があります。

2種類のクロスオリジンリクエスト

このように、アクセス元のオリジン(上記の例ですとhttp://evil.com)、リソースのあるオリジン(同じくhttp://mycompany.local)とが異なる場合のアクセスはクロスオリジンリクエストと呼ばれます。ブラウザはこのリクエストを2種類に分けて扱います。

シンプルなクロスオリジンリクエスト

シンプルなクロスオリジンリクエスト(Simple cross-origin request)とは、特定のHTTPヘッダのみ・特定のContent-Typeの値のみを持つGET/POST/HEADリクエストです(詳しくはリンク先を読んでください)。jQueryでこのようなコードを書いた場合がこれに該当します*3

$.ajax({
    url: "https://...",
    type: "GET",
    headers: {}
}).done(function(data) {
    console.log(data);
});

スクリプトが「シンプルなクロスオリジンリクエスト」を送信した場合、ブラウザはまずレスポンスヘッダの中からAccess-Control-Allow-Originヘッダの値を確認します。その値が元のオリジンと一致すれば(*https://*.example.comなどのワイルドカードも使用可)そのアクセスは認められることになります。逆に一致しないか、そもそもAccess-Control-Allow-Originヘッダがなかった場合、そのアクセスはブロックされてしまいます。

シンプルではないリクエスト

前項のシンプルなクロスオリジンリクエストではないリクエストは全てこれです。認証のためにAuthorizationヘッダやX-Api-Keyヘッダを付加する場合、PATCHリクエストやDELETEリクエストを送信する場合などが該当します。たとえばBASIC認証が必要なURLにアクセスするため、jQueryで次のようなコードを書いたとします。

$.ajax({
    url: "https://...",
    type: "GET",
    headers: {"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}
}).done(function(data) {
    console.log(data);
});

この場合、ブラウザはGETリクエストに先立ち、同じURLに対して自動的にOPTIONSリクエストを送信します。これはプリフライト(preflight)と呼ばれ、スクリプト側からは制御できず強制的に行われます。クロスオリジンリクエストを受け付けたいWebサーバーは、このOPTIONSリクエストに対して「どのようなリクエストなら受け付けても構わないか」を返答しなければなりません。返答は全てレスポンスヘッダで行います*4

API Gateway

以下、API GatwayからLambda (Node.js)のコードを呼び出し、そのまま結果を返すか、またはS3へリダイレクトすることを考えます。また、API Gateway側は「Enable CORS」の設定を済ませていて、さらにLambda Proxy integrationを使うことを前提にします。リクエストはGETを利用します。

f:id:nurenezumi:20170224071730p:plain

A. シンプルなクロスオリジンリクエスト&リダイレクトを行わない場合

一番単純なパターンです。ブラウザが前述の「シンプルなクロスオリジンリクエスト」を送信してきて、かつリダイレクトを行わない場合です。Lambda側の処理はこうなります。

callback(null, {
    statusCode: 200,
    body: JSON.stringify({message: "nya-!"}),
    headers: {
        "Access-Control-Allow-Origin": "http://example.com",
        "Content-Type": "application/json; charset=utf-8"
    }
});

ブラウザはここでセットしたAccess-Control-Allow-Originヘッダの値を確認し、アクセス元のオリジンと一致するかどうかを調べて、問題がなければアクセスを許可します。API Gateway側のCORS設定は使用されません。

B. シンプルではないクロスオリジンリクエスト&リダイレクトを行わない場合

最初にブラウザがプリフライトリクエストを送ってきますが、これにはAPI Gatewayが勝手に応答します*5。レスポンスヘッダにはAccess-Control-Allow-Origin: *という内容が含まれていて*6、ブラウザは最初のチェックを行います。

このチェックをパスしたら、ブラウザは改めてGETリクエストを送りますが、後の処理は前項(A)と同じです。再度Access-Control-Allow-Originのチェックがかかるところも変わりません。

C. シンプルなクロスオリジンリクエスト&リダイレクトを行う場合

可能ですが制約が厳しくなります。

callback(null, {
    statusCode: 303, // See Other
    body: "",
    headers: {
        "Access-Control-Allow-Origin": "http://example.com",
        Location: `https://s3-ap-northeast-1.amazonaws.com/BUCKET-NAME/path/name.json`
    }
});

最初のアクセスに対してAPI Gatwayは303を返します。ブラウザはAccess-Control-Allow-Originをチェックした上でリダイレクト、つまり指定されたS3へ再度GETを送ります。

ところがこの2回目のGETでは、ブラウザはオリジンとしてnullを使用します*7。この挙動はRFCで規定されています*8。したがって、リダイレクト先のS3側ではAccess-Control-Allow-Origin: *を返す必要があります。CORSでのアクセス制御がかけられないことを意味していて、重要なデータを扱うには非現実的になります。

cors - Are there any browsers that set the origin header to "null" for privacy-sensitive contexts? - Stack Overflow

D. シンプルではないクロスオリジンリクエスト&リダイレクトを行う場合

ダメです。

Chrome cancels CORS XHR upon HTTP 302 redirect - Stack Overflow

こちらで紹介されているW3C Recommendationにはこのような記述があります。

If the response has an HTTP status code of 301, 302, 303, 307, or 308 Apply the cache and network error steps.

つまり、このような処理が行われます。

  1. 最初にブラウザはプリフライトリクエストとしてOPTIONSリクエストを送信します。
  2. API GatewayはHTTP 200 OKを返します。レスポンスにはAccess-Control-Allow-Originも含まれます。
  3. 次にブラウザはGETリクエストを送信します。
  4. API GatewayはLambdaを実行し、上記のコードに従ってHTTP 303 See Otherを返します。
  5. ブラウザは無条件で通信をキャンセルします。

API GatewayAPI Key認証を行うためにはリクエスト時にX-Api-Keyヘッダを含める必要があります。このヘッダを含めた場合、リクエストは「シンプルなクロスオリジンリクエスト」ではなくなってしまいます。するとCではなく)のパターンになり、ブラウザは仕様上通信を行うことができません。

結論

お疲れ様でした。

*1:同僚のSlackのアカウントを乗っ取って「これを見てください」と言ってURLを送るとか

*2:データURIなどについても記述がありますが省略します

*3:本当は jquery.get() で書けますが比較のために jquery.ajax() で書いています

*4:OPTIONSリクエストですからボディ部はありません

*5:「Enable CORS」の設定を行ったときに追加されたOPTIONSメソッドの設定に従ってレスポンスが送信されます

*6:“*"はデフォルト値です。もちろん変更も可能です

*7:Chrome 56.0.2924.87, Firefox 51.0.1で確認

*8:ただし「リダイレクトのときはこうなる」と明示されているわけではなく、ブラウザの実装に依存します

Slackにスレッド表示機能が追加されました

こんにちは、Slack大好きなwakです。Slackにスレッド表示機能が追加されたと聞いてさっそく使ってみた雑感を書きます。

概要

これまでと画面が大きく変わるわけではありません。

  1. スレッドの始まりとなる発言(誰のものでもOK)を選択する
  2. コメント1を追記する(この時点でスレッド化される)と、画面右にスレッド画面が表示される
  3. コメント2, コメント3, ……を書き込んでいく
  4. 元の発言の下に「1 reply」「2 replies」「3 replies」とカウンタが増えていく。書き込んだ人のアイコンも表示される
f:id:nurenezumi:20170120200154p:plain

スレッドに参加する方法

そのスレッドに参加していれば、新しく増えた「All Threads」に通知が来ます。参加するためにはいくつかの方法があります。

  • スレッドを作った人は自動的にそのスレッドに参加する
  • スレッドの開始点となった発言の発言主も自動的にそのスレッドに参加する
  • それ以外の人は、自分で明示的に「follow this thread」をクリックすればスレッドに参加できる
  • スレッドの中でメンションを飛ばされた人も自動的にそのスレッドに参加する
    • ただし @channel@here は使えない

たとえば雑談チャンネルで【今日のお昼はどこで食べたいか】を話したいときは、

  1. 「お昼どこにする? このスレッドで決めましょう」と書き、
  2. その発言を開始点としてスレッドを作って、
  3. その中で参加者にメンションを飛ばす

とすればいいことになります。

ピン留めされたスニペットとの違い

弊社ではゴルフ場様向けに様々なサービスを開発・提供しており、お客様からのお問い合わせも数多くいただきます。そのインシデント管理はSalesforceを活用しているのですが、担当者だけでは解決しなかった場合、別の詳しいメンバーや開発担当等にエスカレーションし、ヘルプを求めます。それを多数のメンバーでスピーディに行うためにSlackを活用しており、サポートチャンネルにその旨をスニペット*1書き込んでピン留めし、簡易的な課題管理のような使い方をしています。

ピン留めされたスニペットや画像は、

  • そのチャンネルに入っている全員が見える
    • 現在未解決のまま残っている問い合わせが一目で分かる*2
  • コメントをスレッド形式で書き込んでいける
    • 進捗や担当・開発者間のやり取りも当初の問い合わせ内容の下にぶら下げて書ける

というメリットがあります。こんな感じになります。

f:id:nurenezumi:20170120195507p:plain

スレッド表示もこんな感じで使えると良いかと思ったのですが、スレッドに参加する・参加させるというステップが必要なこともあり、現在のところはもっと軽い使い方になりそうです。また、チャンネルに参加している人数の多寡によってもありがたみは変わってきそうだと感じました。数百人といった大きなチャンネルでの使い勝手も聞いてみたいところです。

それでは良いチャットライフを。

*1:一言二言で終わるような問い合わせはないため、これは自然な行動です

*2:インシデント管理は別途Salesforce+Remedyforceで行っているため、これはあくまで現状を簡単に確認するためのものです。また言うまでもありませんが、解決したらピン留めは外します。

石器時代みたいなWebアプリ開発をしていたエンジニアがReact.jsに導かれて2016年にやってきた話

はじめに

社内向けのシンプルなWebアプリケーションを作成することになりました。 求められているものはサービスそのものであり、実装方式については全て自分で決めていいわけですが、この際なので2016年のモダンな開発方法で書こうと思い立ちました。そこでReact.jsを採用します。今回の記事はその日記のようなものです。

この記事のまとめ

React.jsを使い始めました。そのためにnpmとBabelを入れました。ついでにgulpとBrowerifyを入れました。快適かつモダンな開発環境を手に入れました。

f:id:nurenezumi:20161220160056j:plain

2016年を代表する猫

まずReact.jsについて

React.jsってなに

Facebookの偉大なエンジニアが作ったフロントエンド用のフレームワークです。特徴として、

  1. DOMを全てJavaScriptで構築・操作する
  2. (基本的には)データの流れが一方通行
  3. コードをReact.js独自の文法「JSX」で記述する

が挙げられます。

DOM

HTMLを書くのはやめます。DOMは全てJavaScript側で構築してHTMLに流し込みます。動的に操作するのももちろんJavaScriptです。したがって、HTMLは

<body>
<div id="app">読み込み中...</div>
<script src="./lib/app.js"></script>
</body>

こんな風に超シンプル(というより空っぽ)になります。React.jsが動き出すと、この<div id="app">読み込み中...</div>が丸ごとJavaScript側で生成したDOMに置き換わるわけです。

一方通行

jQueryを使ったDOM操作でWebアプリケーションを作るとき、たとえば画面の右側(例:アイテムの絞り込み条件)と左側(例:アイテムの一覧表示)で不整合が起きるようなバグに苦しむことがあります。これは画面の要素のあちこちが独自に状態を抱え込み、しかもその要素が複雑に絡み合って情報を受け渡しするため、その管理を正しく行うのが難しくなるからです。

これに対してReact.jsでは、

  1. 画面に表示すべき全てのデータを1個のオブジェクトにまとめてしまう
  2. そのオブジェクトを受け取って画面表示する(=DOMに変換する)処理を書く

という基本原則があります。画面を書き換えたい場合は、

  1. オブジェクトの値を更新する
  2. 全ての画面要素を一括で描画し直す

で済ませてしまいます。このルールにより、画面表示と内部データは常に一貫性を持っていることが保証されます。jQueryでこんなことをやっていたら重くてまともに操作できなくなりそうですが、React.jsはまず画面更新後のDOMを内部的に算出し、発生した差分のみに対して実際のDOM操作を行うというテクニックを採用しており(これはVirtual DOMと呼ばれています)、処理は高速に行われます。差分がなければDOM操作は発生しません。

JSX

ここからが本題です。

まず、React.jsのコードはJSXという文法で記述します。これはJavaScriptを拡張した独自の文法で、JavaScriptに似ていますがJavaScriptではありません(明らかに文法エラーになります)。簡単な例を示します。

function App() { return <span>hello world!</span>; }
ReactDOM.render(<App />, document.getElementById('app'));

JavaScriptのコード中にHTMLのタグが出現しています。これは、

という処理です。コンポーネントはいくらでも作ることができます。React.jsでは、Webアプリケーションを構成する要素をこのコンポーネントに分解し、それらを組み立てることでDOMを構築していきます。

JSX→JavaScriptの変換をしたい

JSXはFacebookの偉大なエンジニアが勝手に定義した言語ですので、ブラウザは解釈できません。したがって、JSX→素のJavaScriptへの変換を誰かが行う必要があります。

1つめの方法は手動で変換をかけるものです。オンラインの変換ツールがありますので、ここにJSXをコピペすれば通常のJavaScriptに変換してもらえます。実際にやることはないでしょうが、一度このリンクを開いておいてください。

2つめの方法は毎回動的に変換をかけるものです。HTML側で

<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
<script type="text/jsx" src="app.js"></script>

と書いておくと(type="text/jsx"に注目しましょう)、JSXで書かれたapp.jsをその場でJSXTransformerがJavaScriptに変換してくれます。これはお手軽なのですが、当然ながらパフォーマンス上の問題が生じますし、JSXTransformerは既にdeprecatedになっています

最後の方法は、何らかのツールでトランスパイル*1をかける方法です。

ここでちょっとnpmについて

.NETにNuGetがあるように、PHPPEARやComposerがあるように、またRubyにgemがあるように、JavaScript*2にもnpm (Node.js Package Manager)というパッケージマネージャーがあります。多数の人々により様々なパッケージが提供されているわけですが、そのパッケージをダウンロード・インストールしてくれたり、その際に依存関係を解決してくれたり、また新しいコマンドをインストールしてくれたりする優れものです。

インストール

npmはsudo apt install npmで簡単に入ります。すみませんがAmazon LinuxなどFedora系では入れるのが地味に面倒くさいので自分で調べてください。

プロジェクト作成

npmでは、まず最初にプロジェクトを作成する必要があります。空のディレクトリに対して

npm init

を実行すると、プロジェクト名などの基本情報を入力するように要求され*3、これが終わるとこのディレクトリはプロジェクトとして管理対象になります。管理対象と言っても、単に管理用ファイルpackage.jsonが新規作成されるだけです。

パッケージ取得・インストール

これができたら次のコマンドでパッケージを取得・インストールすることができます。

npm intall --save <パッケージ名>
npm intall --save-dev <パッケージ名>

ここで--saveと書いているのは、package.jsonの中に「このプロジェクトが依存するパッケージ」としてインストールしたパッケージを記録しておくことを示します。開発時にのみ必要なパッケージをインストールする際は--save-devオプションを指定します。他の人は後からpackage.jsonを見ればそのプロジェクトのために必要なパッケージが分かりますし、package.jsonだけ手元にあれば、npm installでその中に記載されたパッケージを一括でダウンロード・インストールすることができます。

さてインストールを行うと、プロジェクトのディレクトリ直下にnode_modulesディレクトリが作成され、ダウンロードしたファイル(多くの場合はJavaScriptのコード)が入ります。これらのコードは、

var hoge = require("MODULE_NAME");

などの文法で参照することができます。これについては後述します。

また、インストールと同時にnode_modules/.bin/以下に実行可能ファイルが置かれるものもあります。これはプロジェクトごとにインストールされるものですが、プロジェクトによらず、OS全体で(グローバルに)利用したいこともあるでしょう。後述のgulpなどがそれにあたります。この場合は、

sudo npm install --global <パッケージ名>

でインストールができます。OS全体に影響が及ぶのでsudoが必要なことがあります。

Babel導入

さて、上の方で試しに利用したオンライン変換ツールの名前はBabelとありました。そもそもBabelってなんでしょう?

Babelの名前は、(おそらく)聖書のバベルの塔の記述、「全ての地は、同じ言葉と同じ言語を用いていた」から来ています。JavaScript*4にはES5, ES2015(ES6), ES2017(ES7)といくつかのバージョンがあり、また正式採用前ながらも開発者コミュニティによく使われている文法もあり、さらにはJSXのような方言も存在します。そのサポート状況はブラウザによって異なります。先進的なバージョンの文法で書かれたJavaScriptはどれ一つとしてブラウザが対応していないことだって考えられますし、現時点ではJSXを理解できるブラウザは存在しません(これから先もないでしょう)。このようなJavaScriptを古いバージョンのJavaScriptに変換し、どのようなブラウザでも解釈できるようにしてくれるのがBabelです。「同じ言葉と同じ言語」が実現するわけですね。

Babelはオンラインで使うこともできますが、普通はコマンドとして使いたいでしょう。そこでnpmを使います。このような流れで実行します。

  1. 空のプロジェクトを作る
  2. このプロジェクトにnpmでBabelをインストールする(babelコマンドも入る)
  3. このプロジェクトにnpmでBabelのreactプリセット(プラグインのようなもの)をインストールする
  4. 準備完了

順に実行します。まず空のディレクトリを作り、そこに空のプロジェクトを作成します。

mkdir helloworld
cd helloworld
npm init

次にBabelとBabelのプリセットをインストールします。

npm install --save-dev babel-cli babel-preset-react

変換が終わってしまえば稼働時には必要ないので--save-devを指定しています。これで準備は完了です。次のコマンドでJSXのファイルを読み込み、JavaScriptに変換して標準出力に吐き出します。

function App() { return <span>hello world!</span>; }
ReactDOM.render(<App />, document.getElementById('app'));
neko@ubuntu:/workdir$ node_modules/.bin/babel --presets react app.js
function App() {
  return React.createElement(
    'span',
    null,
    'hello world!'
  );
}
ReactDOM.render(React.createElement(App, null), document.getElementById('app'));

先ほどオンラインで試したものと同じコードが出力されました。

ES2015? ES2016???

上の方で出てきた「ES2015」「ES2016」について説明します。

まず、JavaScriptECMAScriptという規格に沿った原語です。ECMAScriptには、

  • ECMAScript 5th Edition (ES5)
    • IE6とかのいにしえの時代はこれ準拠のJavaScriptが使われていた
  • ECMAScript 2015 (ES2015)
    • ES6とも呼ばれる
  • ECMAScript 2016 (ES2016)
    • ES7とも呼ばれる

というバージョンがあり、更新されるたびに便利な(あるいは複雑な)機能が追加されていっています。テンプレートリテラル(文字列の組み立て)などはもうES5の時代には戻れません。

var name = "neko";

// ES5
console.log("my name is " + name + ".");

// ES2015
console.log(`my name is ${name}.`);

主要なブラウザが搭載しているJavaScriptエンジンはES2015に対応していますが、ES2016は新しすぎるためまったくと言っていいほど未対応です。

gulp導入

さて話を戻して、ここではgulpというツールを投入します。

gulpとは「ビルドシステム」で、あらかじめ定義したタスクをまとめてコマンド一発で実行してくれるツールです。たとえばJSXをJavaScriptに変換するだけではなく、ついでに圧縮とかもしたいですよね? それもコマンド一発でやって欲しいですよね? さらにはファイルの更新を監視し、それらの処理を自動で済ませたりしてほしいですよね? gulpがやってくれます。

試しに使ってみる

まず、グローバルでgulp-cliをインストールします。どこのディレクトリで実行しても構いません。

sudo npm install --global gulp-cli

これでどこからでもgulpコマンドが実行できるようになりました。次に、プロジェクトのディレクトリに移動してgulpとgulpが利用するパッケージをインストールします。ここではJavaScriptのコードを圧縮するgulp-minifyを入れてみます。

npm install --save-dev gulp gulp-minify

gulpは「あらかじめ定義したタスク」を自動で行うものだと言いました。この定義はgulpfile.jsというファイルで行います。プロジェクトディレクトリの直下にgulpfile.jsを作成し、以下のように書きます。

var gulp = require('gulp');
var minify = require('gulp-minify');

gulp.task("neko", function() {
  gulp.src("src/*.js")
    .pipe(minify())
    .pipe(gulp.dest("lib"));
});

gulp.task("default", ["neko"]);

ここではnekoという名前のタスクを定義していて、その内容はsrcディレクトリ以下の*.jsを対象にminify()して、結果をlibディレクトリに出力するというものになっています。また、タスク名が指定されなかった場合のdefaultタスクの内容としてはnekoを使ってね、ということも書いてあります。

試しにsrc/app.jsとしてこんなJavaScriptを書いて、

// tama-san is kawaii
var name = "tama";
console.log("hello, my name is " + name + " desu.");

コマンドラインgulpとだけ実行すると(明示的にgulp nekoと実行しても構いません)、libディレクトリ(存在しなければ自動的に作成されます)にオリジナルのapp.js、圧縮済みのapp-min.jsが出力されます。中身を見てみます。

var name="tama";console.log("hello, my name is "+name+".");

確かにコメントや空白が削除されて圧縮されています。

gulp + Babel

これでgulpが何者かは分かりました。gulpからBabelを使えるようにしましょう。まずgulpが利用するパッケージをインストールします。

npm install --save-dev gulp babel-cli babelify browserify vinyl-source-stream babel-preset-react babel-preset-es2015

見知らぬ名前がたくさん出てきました。

  • gulp : gulp本体
  • babel-cli : Babel CLI
  • babelify : Babel
  • browserify : (後述)
  • vinyl-source-stream : gulpが利用する
  • babel-preset-react : BabelのReact.js用プリセット
  • babel-preset-es2015 : BabelのES2015用プリセット

分かってしまえばどうと言うことはありませんね!*5

gulpfile.jsの内容はこうなります。

var gulp = require("gulp");
var babelify = require("babelify");
var browserify = require("browserify");
var source = require("vinyl-source-stream");

gulp.task("neko", function() {
  browserify({entries: "src/app.js"})
    .transform(babelify)
    .bundle()
    .on("error", function (err) { console.log("Error : " + err.message); })
    .pipe(source("app.js")) 
    .pipe(gulp.dest("./lib/"))
});

gulp.task("default", ["neko"]);

gulp.task("watch", function() {
  gulp.watch("src/*.js", ["neko"]);
});

さらにプロジェクトディレクトリの直下に.babelrcというファイルを新規作成します。これはBabelの動作内容を決める設定ファイルです。ReactのJSXとES2015を使いたいのでこのように書きます。

{"presets": ["react", "es2015"]}

これで下準備が全て整いました。srcディレクトリにapp.jsを配置します。

const App = props => <span>hello world!!!!</span>; // ES2015の新文法
ReactDOM.render(<App />, document.getElementById('app'));

これでgulpを実行すると、libディレクトリに変換されたJavaScriptが出力されます。これを次のHTMLで読み込めば動作します。

<!doctype html>
<html>
<head>
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
</head>
<body>
  <div id="app">読み込み中...</div>
  <script src="./lib/app.js"></script>
</body>
</html>

ファイル監視

gulpとだけ実行すると、gulpはタスクを1回だけ行って終了します。gulp watchとすると、gulpはファイルの更新を監視し、更新されたらその場その場でタスクを繰り返し実行してくれます。つまり、もう手動でコマンドを実行する必要はなくなるのです。

Browserify

次に、上の項で唐突に出てきたBrowserifyの解説をします。

古き良き時代

ここでjQuery UIのことを思い出してみましょう。ドキュメントを読むと「jQuery UIはjQueryに依存している」と書いてあります。したがって、HTMLに

<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>

のように書かないといけないのでした。このような依存関係は人間がドキュメントを読んだり書いたりして管理しないといけません。また、jQueryグローバル変数$を利用するため、同じ変数を利用するライブラリとは同時に利用できません*6。これも利用者が気を配ってやる必要があります。

Node.jsではどうやっているか

このような牧歌的な時代は終わりました。現代のJavaScriptではこんなのどかなことを言っていてはいけません。

サーバーサイドJavaScript実行環境であるNode.jsには他モジュールのインポート機能があります。たとえばAWS APIを使いたいのであれば、まずnpmを使ってaws-sdkパッケージをインストールします。

npm install --save aws-sdk

プログラムコード中でaws-sdkをインポートするにはrequire()文を使います。

var AWS = require("aws-sdk"); // aws-sdkという名前のモジュールを読み込む
var ec2 = new AWS.EC2();
ec2.startInstances(...); // EC2インスタンスを起動

ブラウザでも同じことがしたいと誰もが思います。しかし(現在の)ブラウザに許されるのは<script>タグでHTTPリクエストを発生させてソースコードを読み込むことだけ。このような仕事はブラウザの領分を超えています。

そこでBrowserify

Browserifyは、npmと組み合わせてこのインポート構文をブラウザ用JavaScriptにも持ち込むためのツールです。このように使います。

  1. npm install --save <パッケージ名>で必要なパッケージをインストールする
  2. JavaScriptのコードの中にrequire()文を書く
  3. Browserifyを通すと、require()が消滅し、代わりに参照したパッケージが埋め込まれる

この結果、ユーザーは参照しているライブラリ(パッケージ)が全て結合された1つの大きな.jsファイルを得ます。名前空間の汚染がないような工夫も行われています。

例1

JavaScriptで日次を任意の書式(たとえばYYYY/mm/dd HH:mmとか)で出力したいというとき、整数のフォーマットが面倒でいつも頭を悩ませていました。従来はそのためのjQueryプラグインなどに頼っていたわけです。

我々はもうjQueryと手を切ると決めたので、ここでnpmパッケージを探すとdateformatというものがあります。便利そうなので使わせてもらいましょう。インストールは簡単です。

npm install --save dateformat

あとはrequire()で読み込むだけです。

var dateFormat = require("dateformat"); // これ!

var timestamp = dateFormat(new Date(), "yyyy/mm/dd HH:MM");
console.log(timestamp); // "2016/12/21 12:34"

これをgulpでビルドすると、require()部分が消えてdateformatが取り込まれ、単体で動作するJavaScriptソースコードが出てきます。

例2

最初の方の例では、react.js, react-dom.jsを直接CDNから読み込んでいました。これを統合してみましょう。

まず、npmでreact, react-domを追加します。

npm install --save react react-dom

次にapp.jsを修正します(上2行を追加します)。

var React = require("react");
var ReactDOM = require("react-dom");

const App = props => <span>hello world!!!!</span>; // ES6の文法
ReactDOM.render(<App />, document.getElementById('app'));

そしてgulpでこのapp.jsをビルドすると、生成されたlib/app.jsはライブラリを取り込んで700KBになりました*7。これならもうスクリプトは1個で済みます。

<!doctype html>
<html>
<head>
</head>
<body>
  <div id="app">読み込み中...</div>
  <script src="./lib/app.js"></script>
</body>
</html>

終わりに

ここまで準備してしまえば、その他のライブラリを導入するのも、別のトランスパイラを利用するのもぐっと障壁が低くなります。特にJavaScriptの開発環境は異様なほど流行廃りが速いのですが、なるべく便利なものをどんどん取り入れて快適に開発を進めていきたいものです。

*1:コンパイルのようなものですが、コンパイルのようにバイナリを吐くわけではないのでこう呼ばれることがあります

*2:正確にはNode.js

*3:全てEnter空打ちでOKですが、プロジェクト名に大文字は含められないため、ディレクトリ名に大文字が入っている場合だけは手で入力する必要があります

*4:正しくはECMAScript

*5:こういうところでは気になってもとりあえず先に進み、しかし後から戻ってきてちゃんと理解するのが惑わずにいるコツです

*6:近年ではあまり名前を見なくなりましたが、prototype.jsが該当します

*7:圧縮すればもっと縮みます