lacolaco

唐揚げとアニメとプログラミングが大好きです

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)

FlutterのBLoCパターンをAngularで理解する

この記事ではAngularDart/Flutterの文脈で新しいコンポーネント設計パターンとして広まりつつあるBLoCパターンを、Angularの語彙で理解し、実装する方法を紹介する。

BLoCパターンとは

BLoCとは、Business Logic Componentの略である。 BLoCを使ったアプリケーションの実装パターンをBLoCパターンと呼ぶ。

まず誤解を招きそうなポイントとして、この"Component"はReactやAngularなどでいうところのビューを構築する"コンポーネント"ではない。 一般的な単語としての、アプリケーションを構成するひとかたまりの要素という意味の"Component"なので誤解しないこと。 対比するレベルとしては、"UI Component" vs "Business Logic Component"のようになる。

BLoCは複数の環境向けにアプリケーションを開発するときのコードシェアカバレッジを高めるための、リファクタリング指針のようなものだ。 具体的には、以下の指針を与える。

  1. BLoCの入力・出力インターフェースはすべてStream/Sinkである
  2. BLoCの依存は必ず注入可能で、環境に依存しない
  3. BLoC内に環境ごとの条件分岐は持たない
  4. 以上のルールに従う限り実装は自由である

詳しくはBLoCパターンの初出であるこのセッションを見るとよい。

Flutter / AngularDart – Code sharing, better together (DartConf 2018) - YouTube

AngularにおけるBLoCパターン

AngularにおいてBLoCパターンの恩恵がどれほどあるのかは議論の余地があるが、 持続可能なAngularアプリケーション開発のために大事なこと - lacolaco でも述べたようにフレームワークに依存しない部分を明確に分ける、というのは設計指針として重要である。 サーバーサイドでの実行やNativeScript、Ionic、あるいはReact/Vueなどへの換装など考えても、BLoCパターンはアプリケーションのAngular依存度を適切に保つために良いルールに思える。

さて、さっそくAngularでBLoCを実装してみよう。 Dartには言語標準のStreamとSinkがあるが、JavaScriptにはまだ存在しないため、非標準の実装が必要である。 幸運にもAngularはRxJSと相互運用可能なので、RxJSのObservableをStreamに見立ててBLoCを実装することができる。

まずはUIコンポーネントビジネスロジックを持ってしまった状態の例を以下に挙げる。

https://stackblitz.com/edit/angular-bloc-example-1?file=src%2Fapp%2Fapp.component.ts

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="onInputChange($event)">
    </mat-form-field>
  </div>

  <span> {{preamble}} </span>

  <ul>
    <li *ngFor="let result of results">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  private query = '';
  results: string[] = [];

  get preamble() {
    return this.query == null || this.query.length == 0 ? '' : `Results for ${this.query}`;
  }

  constructor(private repository: SearchRepository) {}

  onInputChange(query: string) {
    this.query = query;
    this.executeSearch(query);
  }

  private async executeSearch(query: string) {
    const results = await this.repository.search(query);
    this.results = results;
  }
}

UIコンポーネントAPIの呼び出しや状態の保持などさまざまなビジネスロジックを持っているので、もしこのアプリケーションを別プラットフォームにも展開したくなってもコードが共有できない。

BLoCの作成

BLoCはポータビリティを考えると、ほとんどの場合は単なるクラスとして宣言される。 ここではSearchBlocクラスを作成する。 もともとAppComponentが持っていたビジネスロジックをすべてSearchBlocに移動すると次のようになる。

class SearchBloc {
  private query = '';
  results: string[] = [];


  get preamble() {
    return this.query == null || this.query.length == 0 ? '' : `Results for ${this.query}`;
  }

  constructor(private repository: SearchRepository) {
  }

  async executeSearch(query: string) {
    this.query = query;
    const results = await this.repository.search(query);
    this.results = results;
  }
}

そしてAppComponentSearchBlocに依存して次のようになる。

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="bloc.executeSearch($event)">
    </mat-form-field>
  </div>

  <span> {{ bloc.preamble }} </span>

  <ul>
    <li *ngFor="let result of bloc.results">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  bloc: SearchBloc;

  constructor(private repository: SearchRepository) {
    this.bloc = new SearchBloc(this.repository);
  }
}

https://stackblitz.com/edit/angular-bloc-example-2?file=src/app/app.component.ts

Observableへのリファクタリング

先述のとおり、BLoCパターンではBLoCのすべてのインターフェースはStreamでなければならない。 これはFlutterのStatefulWidgetやAngularDartのChange Detectionの間で、データの変更に対するUIのリアクションのアプローチが違うからだ。 同期的な状態の管理ではプラットフォームごとに特別な処理が必要になる。

一方StreamであればFlutterはStreamBuilderでStreamからデータが流れてくるたびに再描画する仕組みをもっており、AngularDartもasyncパイプにより同様の反応機構をもっている。 プラットフォームに依存せず非同期的な値を描画するために、DartBLoCパターンではStreamを活用する。

Angularの場合はRxJSがBLoCの実装を助けてくれる。

DartのStreamをObservable、SinkをObserverに置き換えると、SearchBlocは次のようになる。

class SearchBloc {
  private _results$: Observable<string[]>
  get results$(): Observable<string[]> {
    return this._results$;
  }

  private _preamble$: Observable<string>
  get preamble$(): Observable<string> {
    return this._preamble$;
  }

  private _query$ = new BehaviorSubject<string>('');
  get query(): Observer<string> {
    return this._query$;
  }

  constructor(private repository: SearchRepository) {
    this._results$ = this._query$
      .pipe(
      switchMap(query => observableFrom(this.repository.search(query)))
      );
    this._preamble$ = this.results$.pipe(
      withLatestFrom(this._query$, (_, q) => q ? `Results for ${q}` : '')
    );
  }

  dispose() {
    this._query$.complete();
  }
}

results: string[]results$: Observable<string[]>になり、preamble: stringpreamble$: Observable<string>となった。 これらはqueryの変更に反応して変化する非同期的な値として表現される。

queryObserver<string>インターフェースを外部に公開し、新しい値の追加をUIに許可する。 SearchBlocの内部では_query$: BehaviorSubject<string>を実体として持ち、コンストラクタでは_query$に反応する_results$_preamble$が宣言されている。

これをAppComponentから使うと次のようになる。テンプレート中でasyncパイプを使い、Observableの変更に反応してビューの再描画が実行されるようになる。

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="bloc.query.next($event)">
    </mat-form-field>
  </div>

  <span> {{ bloc.preamble$ | async }} </span>

  <ul>
    <li *ngFor="let result of bloc.results$ | async">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  bloc: SearchBloc;

  constructor(private repository: SearchRepository) {
    this.bloc = new SearchBloc(this.repository);
  }

  ngOnDestroy() {
    this.bloc.dispose();
  }
}

https://stackblitz.com/edit/angular-bloc-example-3?file=src/app/app.component.ts

これでBLoCの実装が完了した。

考察

まとめ

皮肉屋とシステム

ここ数年で一番好きな映画は「マネーショート」(原題 "The Big Short") だ。

映画館でも2回観たし、Netflixにあったから頻繁に見てたんだけどなくなってしまった。今はGoogle Playで買って見ている。

役者もストーリーも演出も全部好きなんだけど、中でもひとつ心に残っていつも頭の片隅から離れないセリフがある。

元々バカなシステムなんだ (中略) 自分を見ろ 皮肉屋を気取ってもどこかでシステムを信じてる

ローンのデフォルトが増えているのに債権の価格が高騰するという、合理的に考えればありえないことが起きていた。 格付け機関も債権のランクを下げることなく、詐欺的なシステムが稼働していた。

作中のマーク・バウムはなぜ合理的に考えればありえない詐欺がまかり通っているのかと、ドイツ銀行のジャレド・ベネットを問い詰めた。その時にジャレドが返したセリフ。

ウォール街のシステムの崩壊を予期し、破綻に賭けて空売りを仕掛けた悲観主義の皮肉屋の彼らは、それでも格付け機関や銀行が正しく機能して正しく債権が暴落し、空売りが成功すると信じていたわけだ。 システムを疑いながらもどこかでシステムを信じている、この二律背反は気づかないうちに自分にも当てはまっていることが多い。


www3.nhk.or.jp

2週間、宿泊費自己負担で8万人のボランティアを集めるためにやりがいをPR、どうかしている。 システムが狂っている。まともに考えたらこんなのでうまくいくわけがない。

最初はこう思うわけだが、この まともに考えたら ってのは、社会の合理性をどこかで信じていることの現れじゃないかと考えてる。 社会は まともに 運営されて稼働している、という観念が根強く響いてきている。 マイナンバー、新元号、まともじゃないものはいままで何度も見てきているのに、やはりどこかで信じてしまっている。

非合理的な社会はそこに確かに存在していて、だからこういった非合理的なシステムが生まれようとしていて、元になった社会自体が非合理なのだから、多分これは"まかり通る"んだろう。 なんだかんだ、詐欺的なシステムの上で、東京オリンピックは終了し、さまざまな形で負債を残していくんだと思う。祭りが終わるまでは誰も現実を見ないんだろう。

特に何か結論があるわけでもない話。おしまい。

危機感の話

常に危機感がある。これは自分が博士号も修士号も持っておらず、第三者から観測可能な価値を持っていないどころか、コンピューターサイエンスの教育を受けたことがないくせにソフトウェアエンジニアとして専門職に就いて生計を立てているのが根底にあるかもしれない。 プログラマーとしてインターネット上で活動しはじめたころからずっとアカデミーに対して劣等感がある。 自分がやっていることなんて高度な教育を受けた人間がちょっと参入してくればあっという間に淘汰されるだろうし、常に風前の灯火っていう感じがしてる。

正直なところ1年後に自分に仕事があるかどうかまったく自信がない。半年後すら曖昧だ。3年後なんてまったく想像もできない。 何をしても足りてない気がするから、毎日生き急いでいるような気がする。

本当は価値がないかもしれないけども、少なくとも価値があると錯覚してもらうために、常に可能な限り優れた成果を出してそれをアピールして生きてる。 そういうセルフブランディングだけで生きているので、それをやめた瞬間すべてが止まってしまうという危惧がある。

これは克服できるのか、すべきなのか、なんもわからん。