JavaScript
angular
WebComponents
AngularDay 10

AngularにWebComponentsがやって来るヤァ!ヤァ!ヤァ!〜@angular/elementsのファーストインプレッション〜

タイトルは「ビートルズがやって来るヤァ!ヤァ!ヤァ!」のパロディです。

@angular/elementsとは何か?

@angular/elementsは、AngularコンポーネントをWeb Componentsにするためのパッケージです。9/29にPullRequestが起こされ10/28にLGTMが出てlabsブランチに移されました。まだexperimental=実験ステータスのできたてほやほや新機能です。

これは言い訳なんですが、@angular/elementsの方向性もまだ流動的ですし、筆者もまだ全然知識が追いついてません。現状わかる範囲でまとめましたが、ツッコミや訂正ありましたら、コメントや編集リクエスト大歓迎ですのでよろしくお願いします:bow:

Web Componentsのおさらい

Web Componentsとは、HTML/CSS/JavaScriptで再利用できる部品作りを促進する仕組み 1 です。

Web Componentsにすると、どんなアプリケーションからでも(Angularじゃなくてもブラウザのwindowにアクセスできるなら何からでも)、Angularで作成したカスタムタグを利用できるようになります。もちろん、逆に例えばVueで作成したコンポーネントをカスタムタグとして登録してAngularで利用することも可能です。(ただし、条件はあります。2
また、後述しますが、Angularならではの事情に対するソリューションとして、エントリポイントを複数持ったAngularアプリを作る手段にもなります。

そのために、@angular/elementsが、AngularコンポーネントをCustom Elements(WebComponents四天王その1)のインターフェースに合わせたビルドをして、window.customElementsで登録できるようにします。なぜ、Angularじゃなくても使えるようになるかと言うと、Custom Elementsで外部インターフェース仕様が定められているからです。他のアプリからでも、propertyを与えたりeventを受け取れば、Angularと他のjsアプリが協調できるというわけです。この場合のjsアプリとは、scriptタグに書かれたjavascirpt関数のことでも適用されます。
なお、@angular/elementsは、ほぼこのCustom Elementsのことを指しています。

インターフェースを決めただけでは不十分です。ホストからカスタムエレメントを隠蔽する仕組みも重要です。ホストのcssやjavascriptオブジェクトがカスタムエレメントの内部と衝突したり上書きしてきたり、逆にカスタムエレメントのオブジェクトが外部に影響与える可能性はいくらでも存在します。そのために、Shadow DOM(WebComponents四天王その2)が、ホストから一部を隠蔽したDOM空間を提供します。

さらには、ES Module(WebComponents四天王その3)によってブラウザのみでモジュール化したものをインポートできるようになります。アベヒロシのサイトに現在の環境を変えることなくAngularカスタムタグをフュージョンすることもできるでしょう。

Web Componentsは四天王なので、もう一人います。Template(WebComponents四天王その4)が、ブラウザに読み込まれても非活性のままでいられる(つまり、読み込んでもレンダーされない)html部品を提供します。

@angular/elementsが生まれた2つの背景

ブラウザの実装状況が進んだ

1つ目の背景は、Web Componentsの仕様策定が進み、ブラウザの実装が揃い始めていることです。Web Componentsを構成する主要4要素の現在のブラウザ対応状況は以下となります。

まだ、核となるCustom Elementsが黄色と赤の行進なんですが、chrome、Safariの二大モバイルブラウザで対応進んでいることが確認できます。

Custom Elements
https://caniuse.com/#feat=custom-elementsv1
Shadow DOM
http://caniuse.com/#feat=shadowdomv1
ES Module
http://caniuse.com/#feat=es6-module
Template
http://caniuse.com/#feat=template

Angularのユースケースカバーに限界が出た

2つ目の背景としては、Angularの路線問題です。Angular2になってからTypescriptとAoTコンパイルを主軸とした強靭なSPA(single page application)路線を推進してきたけど、その代償として、クリエイターが手軽にweb部品を作り、オープンでコラボレーティブなインターネットを楽しめる、ウィジェット路線が弱点になりました。

WordPressの次世代エディタのフレームワークの選定でもAngularは早々に候補から外れています。また、Angular自身のドキュメントサイト構築でも問題になったそうです。ドキュメントサイトの経緯については、AdventCalender1日目の記事の「第二の軸: 動的アプリケーションのサポート」段落をご覧ください。英語がわかる人には、記事で取り上げられているyou tubeも面白いかと思います。私はヒアリングができないので、スライドで読みました。

Angularのドキュメントサイト構築で直面した問題
you tube
https://youtu.be/__H65AsA_bE
スライド資料
https://onedrive.live.com/view.aspx?resid=635B97DC223BC5B2!136637&ithint=file%2Cpptx&app=PowerPoint&authkey=!AFgQoakql9CnkD0

@angular/elementsを用いる理由とは?

2つの方向性

Web ComponentsそのものはHTML/CSS/JavaScriptだけで作れます。@angular/elementsを用いる理由は、Angularパッケージを用いて開発できるようになることです。

初めに述べたように@angular/elementsはまだexperimentalステータスです。ですので、まだ機能も確定していませんし、ユースケースも手探りで進めている段階です。これは現時点で、本件に関わっている人たちの中でも重視している方向性が2つ見られることからも証明されます。

現在、観測できる2つの方向性は以下となります。

  • UI部品のパブリッシュ-ngComponents Everywhere
    • 特定のAngularコンポーネントをWeb Componentsに乗せ、フレームワーク無関係で利用できるようにする
  • Angular特有の課題解決-Dynamic Bootstraping
    • Angularアプリ全体をWeb Components化して、component単位でbootstrapできる手段を提供し、実行タイミングをコントロールできるようにする

本件の提唱者であるRob Warmald氏は、もともとはWeb Componentsの薄いラップAPIを考案していました。Rob氏が6月に公開したサンプルリポジトリangular-elements 3 は、特定のUI部品のパブリッシュを主眼に置き、window.customElements.define()を薄くラップして、extends HTMLElementsしたAngularコンポーネントをCustom Elementsとして公開できるようにしたものです。筆者でもスイスイ読めるわかりやすい感じです。

一方で、@angular/elementsを実装したgkalpak氏は、Angularアプリのbootstrapを解体し、component単位にself-bootstrapingできることに価値を置いているようです。現在のAngularアプリをそのままWeb Components化するものです。彼は、@angular/elementsは最終的にcoreパッケージの中に含まれるのではないかという発言もしています。4 こちらは正直な話、筆者はあまりついていけてない話です。実現しようとしている内容は、別の実験PJであるngiv 5 とも重なる気もしていて、流動的な雰囲気あります。

なお、10月11月にRob氏が@angular/elementsをプレゼンした時は、双方含められているので、どちらも@angular/elementsのターゲットであるようです。@angular/elementsのregisterAsCustomElements()の定義もこの2つのポリモーフィズムになっています。(export functionは2つ+両者をmixした実実装の合計3つです。)

スライド資料
https://docs.google.com/presentation/d/1jiXHYwfe1iSUiVLdKLFhSPRHLI_FmIvrI60QTpP6KLk/mobilepresent?slide=id.g26d86d3325_0_0
you tube
https://www.youtube.com/watch?v=ljsOPm4MMEo

UI部品のパブリッシュ-ngComponents Everywhere

あなたのお気に入りのfuncy-buttonを作成してインターネットに公開できるようになります。
今までやってたことと決定的に違うことは、Angular以外のユーザーにもリーチできるようになることです。

わかりやすいのはWeb Componentsのアセットストア 6 です。例えば、MaterialDesignは、Material Design Lite(JQuery)、AngularMaterial、Material-UI(react)、VueMaterial、paper-elements(polymer)のように、現在jsフレームワーク毎に実装されています。しかし、標準インタフェースで良ければ、ストアで配布するMaterialDesignの提供はjsフレームワーク関係なく一本で済みます。

(@angular/elementsのregisterAsCustomElements()のこの使い方は別途記事にする予定です。)

また、PreactとAngularの協調デモが披露されています。7 以下のようにPreactのcomponentの中で、JSXにAngularカスタムタグを記述しています。

/** @jsx h */
const { h, render, Component } = Preact;

// Preacのコンポーネントを普通に作る
class Demo extend Component {
  constructor() {
    super();
    this.state.progress = 0;
  }

  ...

  render(props, state) {
    // Angularのカスタムタグを記述
    return <ng-progress-bar progress={state.progress}></ng-progress-bar>;
  }
}

render(<Demo />, document.body);

地球は一つだったんだね。

Angular特有の課題解決-Dynamic Bootstraping

エントリーポイントを複数にする

これは完全にAngularユーザー向けの説明になりますが、現在のAngularのエントリーポイント=bootstrapは基本的に一つのcomponentを登録します。bootstrap自体はarrayになっていて、先述の「Angularのドキュメントサイト構築で直面した問題」では、既存のbootstrapに沿った形で、embeddableComponentsとしてarrayをentryComponentsに放り込んでいました。@angular/elementsは、bootstrapをオーバーライドするような形でグローバルにcomponentを複数登録できるようになります。

main.tsのplatformBrowserDynamic().bootstrapModule(AppModule)registerAsCustomElements(webComponents, bootstrapFn)に差し替える形になります。

まじで筆者が付いていけてないポイントになるのですが、このやり方だと現在のAngularアプリの構成とほとんど変わりません。相変わらずSPAですし、routerでゴリゴリ遷移します。main.tsとapp.module.tsの記述だけ差し変わる感じで、既存のAngularアプリをWeb Components化しましたという話になるようです。ほんまか?

簡単にサンプルで違いを示します。

現状です。比較のために出してるだけで、ほぼangular-cliでng newしたばかりの状態です。

main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Web Components化した姿。サンプルコード内にコメントで筆者の混乱、いや、解説を付けていきます。

main.ts
import { enableProdMode, destroyPlatform } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { registerAsCustomElements } from '@angular/elements';

import { AppModule, webComponents } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

// なんだこれは!?
destroyPlatform();

const bootstrapFn = () => platformBrowserDynamic().bootstrapModule(AppModule);
// 第一引数にWebComponentsとして登録したいcomponentをセットする。
// 第2引数にこれまでのbootstrapをセットしている。
registerAsCustomElements(webComponents, bootstrapFn)
  .then(moduleRef => /* Custom Elements are ready. Do your thing... */);
  .catch(err => console.log(err));
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LikeButtonComponent } from './like-button/like-button.component';

// エントリーポイントにしたいcomponentを並べている。
export const webComponents = [
  AppComponent,
  LikeButtonComponent
];

@NgModule({
  declarations: [
    ...webComponents,
  ],
  // bootstrapのキーは記述せずにentryComponentsにする
  entryComponents: [
    ...webComponents
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
  ],
})
export class AppModule {
  // お前はどこから来た?
  // 手動でbootstrapするためのものらしいが...
  // https://blog.angularindepth.com/how-to-manually-bootstrap-an-angular-application-9a36ccf86429
  ngDoBootstrap() { }
}

SPA&SSR方式へのオルタナティブ

これはあまり見かけない気もする話ですが、筆者としては、routerを使わないAngularというものを頭の片隅にイメージしています。NotSPAとして、サーバルーティングでサーバ自身がテンプレートを保持してる状況で、ページ単位だったり同一ページの中で複数のAngularコンポーネントを組み込むやり方です。

要は、SSR(server side rendering)が求められるようになる状況、すなわちSEOやOGPが神となる類のアプリケーションサービスの構築における代替手段ですね。SSRは歪つなソリューションだと思っています。もともとサーバで作成されていたテンプレートを、SPAとしてブラウザに持ってきて、またサーバに戻したら、別のnodeサーバが必要になりましたって、なんだよそれ、めんどくさすぎだろ!

railsやspringのようなwebフレームワークは大抵テンプレートエンジンを持っています。そこに@angular/elements+ESModuleでの開発環境をドッキングすれば、部品単位でマウントできるようになるじゃんねということです。

正直、node&Angular環境が必要になるのは変わらないので、webフレームワークに統合するのはなかなか困難な気がしますが、railsのAssetPipelineのような感じで統合できたら便利そうという期待あります。キーとなるのはCustom ElementsとESModuleですね。

ただ、これももう一つの実験PJ「ngiv 5」が担う気もします。@lacolacoさんが指摘しているようにivはIsomorphic Viewengineの略称ではないかと考えられるからです。筆者が一時公開していたngivのリポジトリを見つけたのですが、ソースにnodeがちょこちょこ出てきていたんだけど、DOM Nodeの意味とNode.js環境の意味の用法がそれぞれ見受けられていたことを記憶しています。browser APIを前提にしたものであることだけは認識できました。

現段階でできること

現在、@angular/elementsは、labというR&DのPJで展開されています。experimentalの位置付けながら、npmでもパブリッシュされています。

$ npm install angular/elements-builds#labs/elements

リポジトリはbranchが用意されています。
https://github.com/angular/angular/tree/labs/elements

筆者はこちらのサンプルリポジトリをcloneして遊んでいました。SPAのままでWebComponents化する方向のやつです。
https://github.com/playerx/angular-elements-sample

今後の展開

今後のマイルストンとしては、labsでフィードバックが集められつつ、来年4月のAngular6でexperimentalが取られ、正式リリースする目標になっているそうです。また、closeされたPullRequestに、12/7に今後のロードマップの質問コメントが投下され、そこで実装していたgkalpak氏が以下の返答しています。4

  • coreに入るかもしれない?
  • coreの内部改造を研究中であり、それを受けてngElementsも改良することになるだろう(バンドルサイズの削減など)
  • angular.ioでngElementsを投入する
  • ドキュメントを準備する

また、3日前にWeb ComponentsのISSUEが立ち上がっています。gkalpak氏は@angular/elementsに関する話題を当スレに誘導しています。
https://github.com/angular/angular/issues/20859

最後に

未来の話なのであくまで私見ですが、Web Componentsが本格稼働始めたことにより、jsフレームワークは分解されるというか溶けていく方向になるのではと考えています。

本記事で取り上げたbootstrapの解体は、Angularの長所でもあり弱点でもあった箇所へ、Web Componentsの媒介によりメスが入り始めた動きとも言えます。これは、reactではvirtualDOMからFiberへ刷新する動きが相当するという見方もできます。reactの長所であったvirtualDOMですが、canvasやmediaでは弱点となっていました。Fiberの目的はチューニングが全面に出ていますが、DOMとの協調を増す方向へ舵転換していることに筆者は注目します。(長所と短所の裏返しの話と言うかなかなか根が深くて一筋縄ではいかなさそうな様子ですが)

従来から、コンポーネント指向のjsフレームワークは、ブラウザがWeb Componentsを搭載するまでのpolyfillだという見方がありました。Web Componentsの搭載が完了したからといって、現在の局所最適化された各jsフレームワークの役目が無くなるとは思えません。しかし、徐々に外部インターフェースを開いていくことにより、相互運用-interopの敷居がどんどん下がります。AngularとPreactの協調デモは象徴的です。これを溶けていくと表現しました。

現状では各jsフレームワークを複数同居させるということは、インタフェースが解決すればいいというものでもなく、バンドルファイルの膨張や開発環境の煩雑さなど、「できるけどやるかどうかは別だ」という話です。

しかし、問題さえ定義されれば、解決するのがエンジニアです。Angularは動き始めました。ゆえに、Web Componentsが進むことにより、エコシステムや技術者のスキルスタックにも大きな変化が起こり、相互運用-interopが進んでいくのではないでしょうか。

地球は一つだったんだね(2回目)

以上です。明日は@pondayさんです。


  1. https://html5experts.jp/shumpei-shiraishi/24239/ 

  2. フレームワークによっては、カスタムタグのインターフェースとなるproperty受け渡しやevent同期で問題が発生します。各jsフレームワークのCustom Elementsインターフェースの対応状況はこちらを参照ください。AngularやVueは満点です。reactは僕たちの魂を震えさせたアイツが足を引っ張ってます。とはいえ、Preactはかなり頑張っています。lowercaseやkebab-caseならイベント同期できるようです。  

  3. Rob氏が6月に公開したサンプルリポジトリ。@angular/elementsのローンチにより現在はarchivedされ読み取り専用になっています。demoで取り上げられている3つのサンプルはWeb Componentsの知識さえあればスイスイわかるものになっています。https://github.com/robwormald/angular-elements/tree/master/src/demos 

  4. https://github.com/angular/angular/pull/19469#issuecomment-350211610 

  5. ngivについても、「Angular 2018年進路予想」の「第二の軸: 動的アプリケーションのサポート」段落をご参照ください。 https://medium.com/lacolaco-blog/angular-ac-2017-1c6d30e9982a 

  6. https://www.webcomponents.org/ 

  7. AngularとPreactの協調デモ http://jsfiddle.net/u9m5x0L7/243/