余白

https://blog.lacolaco.net/ に移転しました

Patreonで投げ銭受け取りはじめました

Patreonという、個人を金銭的に支援できるサービスに登録して、投げ銭を受けられるようにしました。

www.patreon.com

簡単に$1〜の少額を毎月支援できます。いつでもキャンセルできるしクレカで払えるし気軽。

きっかけはこの記事

jp.techcrunch.com

Patreonは前から知っていたけど、あらためてこういうの当たり前にしていきたいなーと思い、まずは自分でやってみます。

別にOSSで生活したいわけではないので、集まったお金は僕のモチベーションアップになります。

まだ肌感がわかってないので、とりあえず用意したのはネタ2種類です。

  • たまご: $1から支援できて、特に特典はなしです。気持ちだけ贈るオプション
  • まぐろ: $5から支援できて、1対1のSlackチャンネルに招待します。プログラミングの相談とか、自由に話せるホットラインです。

f:id:lacolaco:20180628195601p:plain

らこらこホットライン、むしろプログラミング勉強してる学生とか応援したいし相談乗れるようにしたいんだけど、Patreonだと最低$1必要だし、そもそもクレカないパターンあるよね。 どうやってリーチするといいのかなーとぼんやり考え中。

まだ日本のOSS活動やってる系プログラマーでPatreonやってる人全然いないから、どんどん登録して相互支援で相互リンクみたいな楽しい感じの盛り上がりやりたいですね。

それでは。

AngularにおけるstrictPropertyInitializationのベストプラクティス

AngularコアチームのStephen Fluin氏が、こんなブログ記事をあげている。

https://fluin.io/blog/property-has-no-initializer-and-is-not-definitely-assigned

TypeScript 2.7から導入された、クラスプロパティの初期化をチェックするstrictPropertyInitializationオプションの話だ。

tsconfigのstrictPropertyInitialization オプションを有効にすると、undefinedを許容していないプロパティがプロパティ宣言時あるいはコンストラクタで初期化されていないときにコンパイルエラーになる。 これをstrictNullChecksオプションと併用することで、明示的に T?あるいは T | undefinedという宣言をしない限りかならず初期化を要求される。

たとえば次のようなコードがエラーになる。nameプロパティはstring型なのでundefinedを許容せず、初期化漏れのコンパイルエラーになる。

class Person {
    name: string; // Property 'name' has no initializer and is not definitely assigned in the constructor.

    constructor() {
    }
}

この設定は安全なTypeScriptを書くうえでかなり便利だが、Angularにおいては少し注意が必要である。

AngularでstrictPropertyInitializationを使う上で問題になるのは、クラスプロパティのうちAngularのデコレーターによって遅延して初期化されるものだ。

たとえば、@ViewChild@ContentChildrenなどは、クラスの初期化時ではなくコンポーネントのビューツリーの解決時に初期化されるので、strictPropertyInitializationがうまく噛み合わなくなる。

そのため、ビュー解決後は値を持っていることはほぼ確実だが、それまではundefinedになるので、プロパティを?としてオプショナルにするか、| undefinedとしてundefinedを許容することになる。

Stephenのベストプラクティス

AngularコアチームのStephenは、TypeScriptにしたがい、ビュー解決を待つプロパティは?でオプショナルにするのを推奨している。

理由は書かれていないが、推測するとコンポーネントのクラス実装とテンプレートは文字列あるいは別ファイルに存在した疎結合の関係であり、開発者の頭の中では確実に存在するとわかっていても、システム上は実行するまでViewChildで取得しようとしている子のビューが存在することは不定である。

次のように、childは基本的にオプショナルであり、存在が確認できるときだけ処理をするのがベストである。なぜならngIfによるスイッチングなど、コンポーネントの生存中に子ビューの参照が消えることは多々あるからだ。

class SomeComponent {
    @ViewChild() child?: SomeChildComponent;

    ngAfterViewInit() {
        if (this.child != null) {
           // ...
        }
    }
}

Inputプロパティについてのプラクティス

Angularで初期化が問題となるプロパティデコレーターのもうひとつは、@Inputデコレーターだ。

現実問題として、Inputにはオプショナルなものと必須なものがある。常に特定のInputが与えられることを前提として記述されるコンポーネントだ。たとえば次のような例が考えられる。

@Component({
  selector: 'user-card'
})
class UserCardComponent {
    @Input() user: User;
}

このコンポーネントで、 userをオプショナルにするのは意味論的に避けたいし、契約としてそういったコンポーネントの利用は禁止したい。そのためuserの型はNon-NullableなUser型である。 しかし、これをこのまま放置すると、strictPropertyInitializationオプションで初期化していないとエラーになる。

この問題について尋ねると、別のAngularコアチームメンバーであるRado Kirov氏からアドバイスをもらえた。

少し乱暴ではあるが、契約として必ず値が渡されることを求めるプロパティについては、Non-nullアサーションオペレータ !を使ってプロパティがundefinedじゃないことを明示的に示せばいいというものだ。

次のようなコードになる。プロパティの宣言時には必ず初期化されていることを明示し、コンポーネントの初期化後にはそれを確認する。 ?を使ったものと違い、プロパティの型をオプショナルにしていないので、プロパティを使用するたびにif文で型ガードを作らなくてもよい。

親からの値が必須であるInputプロパティについては、実行時アサーションとセットにしたNon-nullアサーションオペレータで解決するのが、現状のベストプラクティスになりそうだ。

@Component({
  selector: 'user-card'
})
class UserCardComponent {
    @Input() user!: User;

    ngOnInit() {
        if (this.user == null) {
            throw new Error('[user] is required');
        }

        this.someFunc(this.user); // no need `if` type guard
    }

    someFunc(user: User) {}
}

将来的にはcodelyzerやlanguage-serviceでこのへんをチェックして、undefinedを許容していないInputへの値渡しがテンプレート中で行われていないことを検知してもらいたい。

Observableの初期化

Storeとの接続や、リアルタイムDBとの接続など、コンポーネントがObservableを購読する必要があるときは、コンストラクタでObservableの初期化をおこなうのがよい。

よくあるRedux的な状態管理をしているアプリケーションだと、このようにコンポーネントとストアを接続する。 そしてコンポーネント内ではsubscribeせず、テンプレート内でasyncパイプを使って非同期ビューを構築する。

class UserListComponent {
    userList$: Observable<User[]>;

    constructor(store: Store) { 
        this.userList$ = this.store.select(state => state.userList);
    }
}

コンポーネント内でsubscribeする必要がある場合は、Observableの初期化だけをコンストラクタで行い、subscribeの開始はngOnInit以降に開始すべきである。 コンストラクタでsubscribeしてしまうと、コンポーネントの初期化より先に値の解決が始まってしまい、変更検知のタイミング制御が困難なり、デバッグしにくくなる。

まとめ

  • @ViewChild@ContentChildはオプショナルプロパティとして扱うべし
  • 必ず親から値を渡されないと困る@Inputは、実行時アサーションとセットでNon-nullアサーションオペレータを使うべし
  • Observableのプロパティ初期化はコンストラクタで行い、subscribeasyncパイプあるいはngOnInit以降にAngularのライフサイクルにあわせて開始するべし

ECMAScriptのimport/exportについてのメモ

js-primerというJavaScriptの本を書く上でES2015のimport/export構文の仕様について気になったところがあって調べたメモ

github.com

デフォルトエクスポートの扱い

まず、デフォルトエクスポートする方法がふたつある。ひとつは専用のexport default文によってエクスポートする方法。

export default function () {} 

また、defaultという名前で名前付きエクスポートすれば、それもデフォルトエクスポートしたことになる。

function foo() {}

export { foo as default };

デフォルトエクスポートがdefaultという固有名がつけられることは、Specの https://www.ecma-international.org/ecma-262/6.0/#sec-exports-static-semantics-exportentries にかかれている。

f:id:lacolaco:20180626135151p:plain

f:id:lacolaco:20180626135151p:plain
default export

デフォルトエクスポートをインポートする方法

デフォルトエクスポートされたものをインポートする方法もいくつかある。ひとつは一番シンプルなデフォルトインポート用の専用構文を使う。

import otherDefault from "other.js";

ところでこれは名前付きインポートで次のように書き換えられる。先程デフォルトエクスポートで書いたように、デフォルトエクスポートはdefaultという固有名でエクスポートされていることを利用できる。

import { default as otherDefault } from "other.js";

この構文、MDNのimport文のところには書かれていない。

developer.mozilla.org

ちなみに、default asというのは専用の構文ではなく、asによるエイリアス付きインポートの構文が適用されており、仕様上ではdefaultは特別なキーワードではなくただのIdentifierNameとして扱われているはず。 次のコードでdefaultとfooは仕様上同じもので区別できない。

import { default as otherDefault, foo as otherFoo } from "other.js";

https://www.ecma-international.org/ecma-262/6.0/#sec-imports-static-semantics-boundnames

f:id:lacolaco:20180626135841p:plain

しかし、次のコードはシンタックスエラーになる。asによるエイリアスを使わない場合には、defaultfooは等価ではなくなる。 なぜならdefaultECMAScript予約語であるからだ。

import { default, foo } from "other.js";

https://www.ecma-international.org/ecma-262/6.0/#sec-keywords

ここの肝は、Import構文のImportsListというもので、これはImportSpecifierのリストだが、ImportSpecifierはImportedBindingあるいはidentifierName as ImportedBinding と定義されている。

https://www.ecma-international.org/ecma-262/6.0/#sec-imports

f:id:lacolaco:20180626140548p:plain

つまり、import { default } と書いたときのdefaultImportedBindingとして扱われるが、これはシンボルとして参照可能なので予約語のチェックに違反する。

一方で、import { default as alias }と書いたときのdefaultidentifierNameであって、ImportedBindingではなくなり、予約語のチェックから外れる。

結果的に、default asはまるで普通のエイリアスされた名前付きインポートのように振る舞うことができる。 しかし仕様上はimport { default as ...}構文というのは存在していないが、デフォルトエクスポートや予約語の仕様が絡んだ結果、事実上の構文っぽいものになっているのがややこしい。

MDNの構文例はexportとimportどちらも、デフォルトエクスポートがdefaultという名前付きエクスポートとして振る舞っている例を扱っていないが、 これは仕様が複雑で説明が難しいからされてないんだろうか。

とりあえず自分の中で整理がついたので良し。

Angular v6.1で導入されるRouter Scrollerの紹介

こんにちは、lacoです。

Angularの次のマイナーアップデートで、久しぶりに新機能らしい新機能が増えます。 その名もRouter Scrollerです。 長くAngularを使っている人には涙が出るほど嬉しい待望の機能です。 この記事ではRouter Scrollerの紹介と、来週のbeta.1まで待てない!今すぐ試してみたい!という奇特な方のために、最新のビルドで試す方法も紹介します。

Router Scroller

Router Scrollerは、Angular Routerにスクロールに関連する機能を与えるものです。 Router Scrollerを使うと、次のようなことができます。

  • ブラウザバックしたときに遷移前のスクロール位置に復元する
  • #foo のようなフラグメント付きのURLで、対応するIDを持つ要素まで自動でスクロールする

どちらも、静的なHTMLページであればブラウザが自動的に行なってくれるものですが、Angular RouterによるSPAでも同じふるまいを簡単に導入できます。 それぞれについて紹介します。

Scroll Position Restoration

Router Scroller第一の機能は、スクロール位置の記憶と復元です。 Routerによるナビゲーションを行うたびに、その時点でのスクロール位置を記憶します。 そして、ブラウザの戻る操作で前の画面に遷移したときには、記憶したスクロール位置に自動的に復元します。 この復元処理はRouterによってタイミングが制御されているため、前の画面のルーティング処理が終わったあとにスクロールが移動します。 よって、先にスクロールが走ってしまい、あとからコンポーネントが描画されてしまい位置がずれる、ということはありません。

https://media.giphy.com/media/g0EEPgQPgyKDoAO6fh/giphy.gif

Anchor Scrolling

もうひとつの機能は、URLに#fooのようなフラグメントが付いている場合、対応するIDを持つ要素があればそこまでスクロールする機能です。 これも静的なHTMLページではよく使われている機能ですが、Angularではブラウザが要素を探すタイミングがRouterによるコンポーネント生成より早いためうまくいきませんでした。 今回RouterがAnchor Scrolling機能を持ったことで、Angularアプリケーションであっても#fooによるスクロールが可能になりました。 Routerによるナビゲーションだけでなく、リロードしても同様にスクロールしてくれます。

https://media.giphy.com/media/2WH6rWL48nbrLScyMM/giphy.gif

使い方

Scroll Position RestorationもAnchor Scrollingも、v6.1.0時点ではデフォルトで無効になっています。 そのため、有効にするにはRouterModuleに設定を行う必要があります。

RouterModule.forRootメソッドの第2引数のオプションに、scrollPositionRestorationanchorScrolling が追加されています。 それぞれ、'enabled'に設定すると機能が有効になります。これだけで完了です。

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled',
      anchorScrolling: 'enabled',
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Scroll Offset

Router Scrollerには、オフセットを指定することもできます。 ヘッダーがfixedやstickyな場合など、スクロール位置をずらしたい場合には、RouterModule.forRootメソッドの第2引数のオプションでscrollOffsetを設定します。 先程のAnchor Scrollingの例では、上部のヘッダーがstickyなので、y座標を64pxだけ下にずらしています。

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled',
      anchorScrolling: 'enabled',
      scrollOffset: [0, 64] // [x, y]
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

今すぐ試すには

Router Scrollerはv6.1.0-beta.1からリリースに含められる予定です。 それより先に試したい方は、@angular/router@angular/common@angular/platform-browser@angular/platform-browser-dynamicの4つをパッケージを開発版ビルドに置き換える必要があります。次のようなバージョン指定でよいでしょう。

    "@angular/common": "angular/common-builds",
    "@angular/compiler": "^6.0.0",
    "@angular/core": "^6.0.0",
    "@angular/platform-browser": "angular/platform-browser-builds",
    "@angular/platform-browser-dynamic": "angular/platform-browser-dynamic-builds",
    "@angular/router": "angular/router-builds",

もっと知りたい

さらに詳しく知りたい方は、当該機能のコミット内容を読むと良いでしょう。

github.com

この記事のサンプルコードはGitHubで公開しています。

github.com

なぜ落合陽一が僕の心を震わせるのか

最近落合陽一氏の本を数冊読み、あっさりとファンになってしまった。 おそらく彼の思想や野望に触発されたんだろうと思うが、その理由を自己分析してみるメモ書き。

落合氏は一貫して「日本」という国をベースにしている。 彼のスキルがあれば日本にこだわらなくてもアメリカやヨーロッパで十分活躍できるし、個人として大きく成功できるだろうと思う。 なのになぜ日本に貢献しようという軸がぶれないんだろうと疑問に思っている。

もし万が一、落合氏の目にこのブログが止まったら、そのモチベーションの源泉が知りたい。

わからないなりに想像してみたのが、彼のモチベーションは貢献そのものなんじゃないだろうか。 つまり、日本というコミュニティが好きで、そのコミュニティへの貢献そのものが目的なんじゃないか。 見返りや報酬のための手段ではなく、目的化した貢献がそこにあるんじゃないかと思った。

そうしてみると、規模は小さいながらオープンソースコミュニティに貢献している自分と重なる部分があることに気づいた。 イベントのオーガナイザやドキュメントの翻訳、ソースコードへの修正パッチなどコントリビューターとしての活動を毎日している。 それをなぜ?と言われると、貢献したいから貢献している、という状態であるのは間違いない。 自分が好きなコミュニティがあり、その中で自身の役割を自分は貢献者だと決めているんだろうと思う。

つまり、日本というコミュニティに対する落合氏と、オープンソースコミュニティに対する僕の相対的なポジションは、それほど違いがないのではないかと思った。

落合氏に勇気づけられるのは、彼もまた僕と同じ20代、同世代であるということだ。 同じ時代で同じ社会現象、出来事を見てきた仲間が世界に影響を与える人物になっていて、その上でまだ日本を見捨てていない。 そこにすごく希望を感じるのだ。 巷では「日本は終わり、プログラマは価値があるうちに海外に行くべき」のような煽りをよく見る。 でも20代なら、まだ再興した先の日本を自分の目で見られるギリギリの世代かもしれない。 日本が好きなら、再興に賭けてみてもいいんじゃないかと、そういう勇気をもらえる。

オープンソースの世界でも、コントリビューターによるプロポーザルやパッチによってとても便利になったり、いままで見えなかった可能性が見えたりする。 僕にとって落合氏は日本というコミュニティに対して再興戦略というプロポーザルを送っているスターエンジニアのようなものだ。 そして同じコミュニティに属している仲間として、コントリビューションを生きがいとする者の端くれとして、彼の野望に巻き込まれたいと思ってしまうのだろう。 そうして僕も「これからの世界をつくる仲間たち」の一員になりたいと思っている。

完全にただのファンレターになってしまったが、一言でいうなら「貢献者」としての親近感が、ファンになってしまった原因じゃないかと思う。

もし少しでも興味を持った同世代の仲間がいれば、ぜひ日本再興戦略を読んでほしいと思う。アフィリエイトはやってないので自分で勝手に買ってほしい。

日本再興戦略 (NewsPicks Book)

日本再興戦略 (NewsPicks Book)