🅰️

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

2021/07/04に公開

こんにちは、@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 に振り回されることが面倒くさくなってきて、ES6[1]で書く機会が増えました。この 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 に統一します。 ↩︎

Discussion