TypeScript
AngularJS
angular
Angular2

#ng_kyoto の公式サイトを100% Angular 2で作ってみましたえ

More than 3 years have passed since last update.

こんにちは、@armorik83です。5月からng-kyotoってゆうのを立ち上げまして、昨日は初のイベントとして『Angular Meetup Kyoto #1』を開催したんですけど、そん時に紹介したng-kyoto公式サイト、実は全部Angular 2で作ってんのでここで詳しぃ解説します。


京都弁のイントネーションを知らない方には伝わらない文章で始めてみた。改めましてこんにちは、@armorik83です。

本稿の概要


ng-kyoto


ng-kyoto とは

まずng-kyotoについて紹介します。ありがたいことに、私もAngularJS, Angular 2に関する記事を数々掲載していることでお馴染みとなりましたが、GDG神戸さんのAngular勉強会に参加していたユーザにたまたま京都の方が多かったので、そんな縁からAngularユーザグループ『ng-kyoto』を設立する運びとなりました。私も京都市出身、京都市在住です。

最初は冗談半分だったのですが、1.4.0もリリースされ脂の乗ってきたAngularJSをもっと広める一環にしたいという思いを固め、5月に正式に私を代表として発足してからメンバー内でアイデアを出し合いました。

ng-japanの公認も得られたので、今後がんばります。


Angular Meetup Kyoto

Angular Meetup Kyotoはng-kyotoの主活動のひとつで、セミナー形式+もくもく形式を合わせた勉強会のことです。これは私の今までの経験が元となっており、GDGやNodeSchoolといったコミュニティの活動風景を意識して採り入れています。

今後AngularJS 1.xがさらに普及し、Angular 2もBetaから安定版リリースと時を重ねていくことを見越し、すでに定期的な活動を目標に取り組んでいます。こちらは随時告知を行いますので、ご興味がありましたらサイトを確認してください。


公式サイト

ng-kyoto発足のタイミングでさっそく公式サイトも開設しました。このデザインはAngular 2サイトのパロディとなっています。

このサイトを制作する際、業務でもないしせっかくだからAngular 2で構築しようと考え、Angular2.0.0-alpha.25(制作開始時)を用いて組み立てました。本稿では、このサイトにどういったAngular 2の機能が使われたか、どういう点がAngular 2らしいのか、alpha特有の問題は何か、といったレポートをまとめていきます。


Angular 2最新情報


alpha.25よりTypeScript化完了

alpha.24までAngular 2のソースはAtScriptにて書かれ、Angular 2チームによって改造されたtraceur-compilerを用いていたので、npm iした際のソースにはtraceur-runtimeが使用されていました。このためBabelが使用できず、Babel + Browserifyな筆者にとっては開発環境構築のハードルが高く、悩みの種でした。

alpha.25からようやくTypeScript化が完了し、npm iで落ちてくる内容はtraceur-runtimeの出てこないES5ソースとなったため、他の多くのnpmパッケージと同じようにBabel + Browserifyでの開発が可能となりました。


a24

// 略

var $__change_95_detection__ = ($__change_95_detection__ = require("./change_detection"), $__change_95_detection__ && $__change_95_detection__.__esModule && $__change_95_detection__ || {default: $__change_95_detection__});
var $__core__ = ($__core__ = require("./core"), $__core__ && $__core__.__esModule && $__core__ || {default: $__core__});
var $__annotations__ = ($__annotations__ = require("./annotations"), $__annotations__ && $__annotations__.__esModule && $__annotations__ || {default: $__annotations__});


a25

function __export(m) {

for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
__export(require('./change_detection'));
__export(require('./core'));
__export(require('./annotations'));

a24a25で見た目が全然違います。


Angular 2はBabelだと書きにくい

最近はTypeScriptに振り回されることが面倒くさくなってきて、ES61で書く機会が増えました。このng-kyotoサイト制作も最初はES6(厳密にはES7 Decorators構文含む)で書き始めたのですが、ちょっとAngular 2を書くには課題があったため、おとなしくTypeScript 1.5.0-betaに切り替えました。


emitDecoratorMetadataとは何ぞや

TypeScript 1.5.0-betaの新オプションにemitDecoratorMetadataというものがあります。これはどうみても「Angular 2のため」に導入されたとしか思えませんが、このオプションが適用されていないとAngular 2は正常にDIを行えないという事情があります。

これがなんなのか、正直筆者もよく分かっていません。とりあえずこの情報を元にDIすべきものを決定しているようです。ひとまずソースを見てもらいましょう。TS 1.5.0-betaを用いて、emitDecoratorMetadataの有無で比較してみます。

ちなみに1.5.0-betaには色々面倒くさいバグがてんこ盛りで、このせいで急激にTypeScriptに対して冷めてしまったので、1.5.0安定版が出るまではおとなしく1.4.1 npm i -g typescript@1.4.1 を使い、Angular 2のことも忘れるが吉です。

元のソース


index.ts

// 実験のためAngular 2はimportしていない

// コンパイラを通すために宣言のみ行う
class ViewContainerRef {}
declare function Component(...args: any[]);
declare function View(...args: any[]);

@Component({
selector: 'example',
})
@View({
templateUrl: './example.html'
})
class ExampleComponent {
viewContainer: ViewContainerRef;

constructor(viewContainer: ViewContainerRef) {
// noop
}
}


emitDecoratorMetadataなし

$ tsc -t es5 index.ts


index.js

/* Decorators構文のためのランタイム */

if (typeof __decorate !== "function") __decorate = function (decorators, target, key, desc) {
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") return Reflect.decorate(decorators, target, key, desc);
switch (arguments.length) {
case 2: return decorators.reduceRight(function(o, d) { return (d && d(o)) || o; }, target);
case 3: return decorators.reduceRight(function(o, d) { return (d && d(target, key)), void 0; }, void 0);
case 4: return decorators.reduceRight(function(o, d) { return (d && d(target, key, o)) || o; }, desc);
}
};

/* コンパイラを通すために宣言したclass ViewContainerRefの出力 */
var ViewContainerRef = (function () {
function ViewContainerRef() {
}
return ViewContainerRef;
})();

/* class ExampleComponent */
var ExampleComponent = (function () {
function ExampleComponent(viewContainer) {
//
}
ExampleComponent = __decorate([
Component({
selector: 'example',
}),
View({
templateUrl: './example.html'
})
], ExampleComponent);
return ExampleComponent;
})();


emitDecoratorMetadataあり

$ tsc -t es5 --emitDecoratorMetadata index.ts


index.js

/* Decorators構文のためのランタイム */

// 略

/* emitDecoratorMetadata使用時に追加される部分 */
if (typeof __metadata !== "function") __metadata = function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

/* コンパイラを通すために宣言したclass ViewContainerRefの出力 */
// 略

/* class ExampleComponent */
var ExampleComponent = (function () {
function ExampleComponent(viewContainer) {
//
}
ExampleComponent = __decorate([
Component({
selector: 'example',
}),
View({
templateUrl: './example.html'
}),
__metadata('design:paramtypes', [ViewContainerRef])
], ExampleComponent);
return ExampleComponent;
})();


__metadata('design:paramtypes', [ViewContainerRef])が追加されているのが大きな特徴で、併せて冒頭にランタイムも追加されています。これがAngular 2のDIに欠かせないTypeScript 1.5.0-betaからの新オプションです。(こいつらどんな蜜月なんだ)

BabelでAngular 2を記述する際、__metadata('design:paramtypes', [ViewContainerRef])と同様の処理を手書きすれば出来なくもないのでしょうが、同時にDecorators構文の使用を諦めるか、emitDecoratorMetadata対応Babel Pluginを自作して対処するか…。なんにせよ、とてもつらい。

大人しくTypeScript使って書け!という姿勢のよう。とりあえず早く1.5.0 stableをだな。


alpha.26で大きいBreaking Changeが入った

何が問題になったかは後述するハマりどころで解説します。とにかく今はalpha版なので、けっこう平気で大規模な変更を行います。仕方ないことですが、頻繁に追うか全て諦めるかしないとalphaと付き合うには厳しいものがあります。

QiitaにてAngular 2 alpha版のAPI変更についてメモという記事も書いていますので、気付いたAPI変更はここにまとめています。


ng-kyotoサイトをAngular 2で作ってハマったところ

まずおことわりですが、alpha.25から27にかけてハマったところなので、安定版では解決している可能性が十分にあります。以下は言及しない限りalpha.25についての内容であり、一部alpha.26, 27を含みます。


reflect-metadataが必要

どうやらBrowserifyビルド時にreflect-metadataというライブラリをimportしていないと、ビルド後に動かないようです。なおこの作者のrbuckton氏はMicroSoft社TypeScriptの中の人です。


Promise polyfillを入れてはダメ

今回Babel Polyfillを使ってみたら、見事にAsync関連でハマってしまいました。埒が明かなかったので開発チームにすがったところ解決法を教わったのでメモしておきます。

要約するとAngular 2にはDartとJavaScriptの環境を揃えるためのZone.jsというモンキーパッチがあるから、それに当てはまらないPolyfillは使っても動かないよ、とのこと。


Browserifyビルドがなにかと困難にぶつかるのでHosted使おうぜ

上のissueで教わったのですが、ビルド済みAngular 2のHostedがありますので、これを使うのが一番手っ取り早いです。こうするとimportやPolyfillで悶えることなく動作するAngular 2が手に入るので、ビルド時には自分のソースのみ気を配ればよくなります。

自ソースで使う際は一旦グローバル変数をラップしてます。今後この手法を止める際に変更箇所を最小限にするためのもので、特にこだわりなければソース中でwindow.angular使っても動くはず。


https://github.com/ng-kyoto/ng-kyoto.github.io/blob/master/app/angular2.ts



angular2.ts

export const angular = (<any>window).angular;


今後のプロダクション用途などでは、この辺りが改善されることを信じて待っています。

(150721追記:a30からwindow.angularwindow.ng改められました。おそらくAngularJS 1.x系とコンフリクトするためでしょう。この変数名に依存している場合は変更しないと動きません)


【a26以降】properties仕様のBreaking Changeでドハマりした

上のHostedは、バージョン番号を変えれば簡単に最新にできるため、調子にのってalpha.25alpha.27にしたところ、作ってきたもの全てがブッ壊れてえらいことになりました。そもそも画面表示が盛大に狂ってしまい、一体何が起きたのかと呆然となった。

この崩壊の原因はこのBreaking Changeでした。

Before

@Directive(properties: {
'sameName': 'sameName',
'directiveProp': 'elProp | pipe'
})

After
@Directive(properties: [
'sameName',
'directiveProp: elProp | pipe'
])

Objectとして'sameName': 'sameName',と書くのは冗長だから['sameName']にしようという変更で(理由はこれだけじゃないはずですが)今までの記述をこれに改めないと全て壊れるという事態になったわけです。issueはこちら

早くもAngular 2を紹介している記事などが他にもあれば、a26以前の言及だとこの箇所が大幅に異なりますので、読む際には注意してください。


ng-kyotoサイトのどこにAngular 2らしさがあるか解説

正直、Angular 2じゃなくても十分作れる規模のペライチですが、無理やりAngular 2を導入している感があります。せっかくなのでいくつか機能を紹介します。


ヘッダのボタンもComponentにしている

Screen Shot 2015-06-20 at 17.19.59.png

この「参加する」というボタンのことです。(もう#1は終わったので本来は除去すべきですね…)


header.ts

constructor(viewContainer: ViewContainerRef) {

this.label = (<any>viewContainer).element.domElement.innerText;
}

ここではViewContainerRefという機能を試してみました。AngularJSでいうng-transcludeに近く、あれより遥かに直感的に記述できます。こういった進化は評価したい。サンプルソースで<any>を多用しているのは、.d.tsの整備が遅く自力で書くには多すぎるからです(察して)


HTML中にもJSが書ける

Screen Shot 2015-06-20 at 17.25.31.png

Aboutのメッセージについてです。<p><br></p>ではなく、Array<string>をAngular 2のAPI*ng-for="#line of message"で回しています。このとき[class.short]="line.length < 40とすると文字数に応じてCSS Classが設定され、あとはCSS側letter-spacingを調整する仕組み。

こうすることで、文字数の少ない行と多い行の視覚的なデコボコ感を減らしています。スマートフォン上では左揃えのためこの演出は不要なので、Media Queriesで除外しています。

今回はちょっと横着してますが、極めるとそれなりのタイポグラフィ自動化も可能…? いろいろ面白いものが作れそうですよ。


Ajaxの練習もしておいた

Screen Shot 2015-06-20 at 17.32.39.png

Qiita APIを叩いてng-kyotoオーガナイザの書いたAngularJS, Angular 2に関する記事を抽出し、それを整えて表示する部分を作ってみました。organizers.tsでリクエストを投げ加工し、organizer.tsで各人ごとのComponentを生成しています。Fluxとか、そういうやつはやってません。普通にjQueryでできる範囲です。

ただし見本のcommitでは、Meetup本番中にAPIアクセス集中による規制で表示がされない事故を防ぐため、モックのJSONを使っています。また後日APIを叩くように改修する予定。


Bootstrap row/colをComponent化してみた


https://github.com/ng-kyoto/ng-kyoto.github.io/blob/8d6b9df54773ee27d0e90fb5206342ed43b8c2b7/app/utils/directives/bootstrap-grid.ts


地味につらかった。Bootstrap CSSのcol-xs-offsetといったclassを書くのが面倒でComponent化してみた。ところで、もしかしてBootstrap自体オワコンだったりしますか?


getter/setterどうやって書くの問題


https://github.com/ng-kyoto/ng-kyoto.github.io/blob/8d6b9df54773ee27d0e90fb5206342ed43b8c2b7/app/components/organizer.ts#L9

https://github.com/ng-kyoto/ng-kyoto.github.io/blob/8d6b9df54773ee27d0e90fb5206342ed43b8c2b7/app/components/organizer.ts#L26-L31


propertiesで定義したプロパティはget, setでフック可能です。ただここではthis._organizerという冗長なプロパティも発生しているため、これをどう書くかというのが悩ましい話になりそう。


https://github.com/ng-kyoto/ng-kyoto.github.io/blob/852cd3c0df17717c0a70ca7bcd30f0939428a3b3/app/components/organizer.ts#L23-L33


ng-kyotoメンバーの@_likr氏とも話し合って、WeakMapを使って隠蔽してみようかと考えてみたり。モバイル対応のこともあって、ひとまず見送りました。


まとめ


  • Angular 2、一応ちゃんと動くぞ…?

  • でもまだ早いな、TypeScript 1.5.0-betaも不安定だから、手出ししなくてよし

  • 遊びたかったらHostedだ

  • 公式のDocsは更新がワンテンポ遅い、ウソ載ってても泣くんじゃない

  • Angular 2のソース読むぐらいの気合がないとヤケドする

以上です。次回活動は、8/7金 - 8土にオープンソースカンファレンス2015 Kansai@Kyotoにて出展を予定しています。ng-kyotoをどうぞよろしくお願いします。





  1. ECMAScript 6thについてはES2015とも書かれますが、本稿では表記をES6に統一します。