2019年はWebComponents元年(2回目)！WebComponentsをReact/Angular/Vueと一緒に使う

WebComponentsは未来の技術みたいな感じでしたが、[lit-element](https://www.polymer-project.org/blog/2019-02-05-lit-element-and-lit-html-release)が登場してかなり作りやすくなりました。ただ、実際のウェブアプリケーションで使うにはどんな感じになるのかな、というのを脳内シミュレーション＆実験してみましたので、それの記録です。

この記事自体は5月ごろに書いて放置したのですが、JSConfで発表したのでそれに合わせて公開します。

# 本記事の想定ストーリー

* かっこいい部品を作りたいデザイナーさん、もしくは社内的なデザインガイドラインがある。ホストのフレームワークがなんであるかは気にしないで、とりあえずデザインを作り込みたい
* 管理画面はフォームのバリデーションとかが手厚いAngularを使いたい。でも、ユーザー向け画面は高速にVue.jsかReactで作りたい。SSRもしたいかもしれない。そういうチャンポンなアプリケーションが作りたい
* とりあえずTypeScriptは使う（環境問わず）

いろいろ書いているけど、要するに「一度作ったかっこいいデザインの部品をどこでも使いたい」です。

# 試してみる

lit-elementで作り込んでもいいのですが、すでにlit-elementを使って作られた[@material/mwc-button](https://github.com/material-components/material-components-web-components/tree/master/packages/button)を各フレームワークに組み込んで見ます。

どの手順でも下記のコマンドはプロジェクト作成後に一度実行するものとします。

```sh
$ npm install --save @material/mwc-button
```

## React

create-react-appで--typescriptオプションを使って作ったアプリケーションを想定します。ReactでTypeScriptを使う場合、JSX.IntrinsicElementsに定義されている情報を元にJSXの中のタグの情報のチェックが行われます。タグ名や属性などです。WebComponentsでタグを作ると、ここに定義されてないタグになってしまうのでビルド時にエラーになってしまいます。属性も含めてきちんと定義してあげると、エラーチェックやら補完やらがきちんと行われるようになります。

```ts:src/react-app-env.d.ts
/// <reference types="react-scripts" />

//↓これを追加する
declare namespace JSX {
  interface IntrinsicElements {
    "mwc-button": {
      raised?: boolean,
      children?: React.ReactChild,
      onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void };
  }
}
```

あとは、表示したいページのコンポーネントとかにimport文を追加してあげて、タグをJSXに追加してあげればボタンが表示されます。

```tsx
import "@material/mwc-button";

const App = () => {
  const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
    alert("clicked");
  }
  return (
    <div className="App">
      <mwc-button raised onClick={onClick}>Click Here!</mwc-button>
    </div>
  );
}
```

## Angular

ng newで作成したアプリケーションを想定して説明します。

Angularでカスタムのタグを使うには、src/app/app.module.tsで、カスタムタグを使うという宣言をしてあげる必要があります。
ボタンのパッケージのimportもここに書いておきましょう。

```ts:app.module.ts
import { BrowserModule } from '@angular/platform-browser';
// ↓この行のCUSTOM_ELEMENTS_SCHEMAを追加
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TopPageComponent } from './pages/top-page/top-page.component';

import '@material/mwc-button'; // ←この行を追加

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // ←この行を追加
  declarations: [
    AppComponent,
    TopPageComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

あとは、アプリケーションのコンポーネントのHTMLの中でタグを追加してあげるだけです。簡単ですね。

```html:page.component.html
<mwc-button raised>Button</mwc-button>
```

### Angular 6 LTSに要注意

Angular 6はTypeScript 2.9.2を使います。lit-elementはTypeScript 3系で導入されたunknownキーワードを使っているので、Angular 6では利用できません。LTSを使いたい場合はあと半年待って、Angular 8が出てLTSになるのを待ちましょう。上記のサンプルはAngular 7での動作を確認しています。なお、importせずに、scriptタグで読み込む方法を使えば問題ないと思われます。

## Vue.js

vue cliでtypescriptを有効にして生成したアプリケーションを想定して説明します。main.tsでは、カスタムなタグの名前をVue.jsに教えてあげる必要があります。ついでに、コンポーネントのパッケージのimportもしておきましょう。

```ts:src/main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

// ↓この行を追加
import '@material/mwc-button';

Vue.config.productionTip = false;
// ↓この3行を追加
Vue.config.ignoredElements = [
  'mwc-button',
];

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');
```

あとは.vueファイルのテンプレートに書くだけで使えます。楽勝ですね。

```vue:home.vue
<template>
  <div class="home">
    <mwc-button raised>Button</mwc-button>
  </div>
</template>
```

# サーバーサイドレンダリングの考察

さて、WebComponentsといえば、二言目に出て来るのがサーバーサイドレンダリングとの相性です。いくつかのケースに分けて考えて見ます。なお、Googleの検索botが賢くなってJSをきちんと解釈できるようになったという前提で話をします。つまり、SEOはここでは考えません。よくわからないし。そこは詳しい人に誰かフォローして欲しい（し、きっとGoogleが作った規格だからGoogleはきちんと扱ってくれるのを期待したい）。

## トップページで使われない部品

これについては、まったくもって問題ありませんよね。表示されないので。たとえば、ポップアップで表示されるダイアログの中で使われる部品とか、何かしらのインタラクションで表示されるやつです。

## トップページで使われているけどシンプルな部品

SSRの手段がファーストビューの高速化ということであれば、条件分岐で大幅に表示内容が変わらないような部品であれば表示のごまかしはできそうな気がします。生成されるHTMLは、``<mwc-button>``というタグとして出力されます。これが、JSが読み込まれていない時の状態（上記のサンプルのimport文をコメントアウトした）です。

<img width="497" alt="Screen Shot 2019-05-17 at 18.08.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6105/447fbdee-6385-db31-51cb-dc1e53d67a34.png">

ちなみに、JSを読み込むと、#shadow-rootというのが作られます。

<img width="809" alt="Screen Shot 2019-05-17 at 18.07.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6105/186ba2c4-dab2-109f-4c1a-601744559676.png">

どうせ、JSが読み込まれるまでは何も操作もできないので、次のようなCSSを雑に定義してみます。開発者ツールでうまく動作したときのスタイルをコピペしたりしたものです。

```css
mwc-button {
  font-family: Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.0892857em;
  text-transform: uppercase;
  display: inline-flex;
  position: relative;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 64px;
  height: 36px;
  line-height: inherit;
  user-select: none;
  -webkit-appearance: none;
  vertical-align: middle;
  text-decoration: none;
  padding: 0px 8px;
  border-width: initial;
  border-style: none;
  border-color: initial;
  border-image: initial;
  outline: none;
  overflow: hidden;
  border-radius: 4px;

  background-color: #6200ee;
  color: #fff;
}
```

ちょっと雑に作ったので、少し見た目が違いますが、もうちょっと工夫して影をつけたり、パディングを調整すれば、何もインタラクションがない状態のスタイルは再現できそうです。ファーストビューのスタイルなんてこんな感じでごまかせるんじゃないですかね。

<img width="161" alt="Screen Shot 2019-05-17 at 18.07.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6105/55bc89fd-3a60-ae0c-4400-4d35883e86fc.png">
<img width="153" alt="Screen Shot 2019-05-17 at 18.08.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6105/1c78d75c-1c90-3324-072e-de8937584cdb.png">

## あるいは愚直にJSを読み込む

たとえば、中のテキストや属性値によって見た目が大きく変わるような部品があったとすると、それをCSSで再現するのは難しいです。そのようなものは、実際のロジックを使う方が良いでしょう。

どちらにしても、CSSとか画像とかを読み込まないとブラウザの表示はされません。利用するWebComponentsのJSだけをうまく取り出して、優先度をあげて読み込むようにしてあげれば万事解決です。アプリケーション本体のヘビーなロジックはプライオリティを下げて後から読み込ませるようにすれば、まずは見た目が大きく崩れるということは減るでしょう。

優先度については下記の表がよくまとまっています。

* [JavaScript Loading Priorities in Chrome](https://addyosmani.com/blog/script-priorities/)

SSRするときのアプリケーションロジックは``<script defer>``にしてbodyタグの末尾に置いて、WebComponentsのコードだけheadタグにscriptタグを置くなどすれば良さそうです。あるいは、サーバーPUSHを使って送り付けちゃうとかでも良さそうです。

# WebComponentsをSSRで使うメリット

WebComponentsをSSRで使う場合、デメリットばかり言われますが、メリットもあると思います。SSRはたいていNode.jsを使うでしょう。VueでもAngularでもReactでも、コンポーネントはJavaScriptのコードです。最終的にブラウザが解釈できるプリミティブなタグを生成するためにCPUを使ってコンポーネントを展開していきます。

一方、WebComponentsは、これらのフレームワークからすると、未知ではあるものの、単なるタグです。``<mwc-button>``という文字列を生成したところで、これらのフレームワークのお仕事はおしまいです。

Node.jsはシングルスレッドなので、CPUを使う処理が重くなって来るとパフォーマンスが落ちます。計測したわけではないですが、WebComponentsを使う方がSSRをする場合のタスクが減るので当然、スループットが上がるものと思われます。ISUCONみたいなやつなら効果絶大じゃないですかね。知らんけど。

# 結局どこで使うのが良いのか？

画面を作成する場合の、末端の小さなコンポーネントをWebComponentsでガンガン作っていくといいんじゃないですかね？ループでたくさんの情報を表示する系とか、サーバーアクセスでReduxでstoreでユーザーオペレーションで・・・みたいなそういうのはVue/React/Angularのレイヤーで作って、小さいテキストボックスとか、チェックボックスとか、ボタンとか、属性だけで結果が100%決まるような、そういう要素をどんどんWebComponentsにしていくのがいいんじゃないかと思います。

実際のアプリケーション全体をWebComponentsで作るのがいいというのはまだいえないと思います。開発のためのインフラとかそういうところがまだ弱いと思いますし。そのうち、routerとかrouter-outletとか、状態管理とかいろいろそろってきて、上から下まで全部WebComponentsでという時代が来るかもしれませんが、まだ2019年にはこないかなぁと思います。

# アプリケーションの構造をどうするのか

アプリケーションを作るときは、コンポーネント単位で小さいプロジェクトを作ることになると思います。各部品はes6 modulesにしてあげて各ページの中でscriptタグで読み込むのがいいのか、各ページごとに使われる部品をまとめたJSファイルを1つずつ作って、ページごとにimportするのか、アプリケーション内部で使われるWebComponentsをまとめたJSファイルを1つ作り、すべてのランディングページで同一のscriptを読み込むのか、チューニングをどこまでやるのかもいろいろ考えられますね。一番楽しいところですよね！

# 2018年から2019年の環境の変化

なぜ今年がWebComponents元年なんでしょうか？いろいろ環境が変わりました。

* もともと、4つの要素（Custom Elements、HTML Imports、HTML templates、Shadow DOM）で構成されていたが、Mozilla、Appleが抵抗していたHTML Importsはなくなった。
* Shadow DOMのPolyfillをPolymerプロジェクトが頑張って作っていったが、これと仮想DOMの相性が悪かった（vdom内で想定しているタグ構造と、実際のタグ構造が変わってしまい、更新がうまくいかない）ので、Shadow DOMのネイティブサポートが大規模アプリケーションでは必要だった（React/Vueを捨ててPolymerで、というのは難しい）
    * Chrome/Opera/Safariは2016年からサポートしていた
    * 2018/10/23リリースのFirefox 63でShadow DOMがサポートされた！
    * Edgeがいなくなってしまったため、EdgeHTMLの対応を待つ必要がなくなってしまった(2020/1/15リリース予定)
* Polymerプロジェクトが当初の計画通り、フェードアウトしてきて、[lit-element v2.0.0](https://github.com/Polymer/lit-element/releases/tag/v2.0.0)が2019/2/6にリリースされた

それまでは、Reactなどを捨てないとうまく動かせない（ことがある）ということで、プロジェクト構成とかへのインパクトがかなり大きく、導入のサンクコストが極めて高いという状況でした。

昨年末からほとんどのブラウザで問題なく使える状況がそろってきました。そのため、既存のプロジェクトにポン付けできるように環境がそろってきました。また、一方で、lit-elementという、部品を作りやすくするライブラリが出てきました。つまり、既存のプロジェクトへの導入コストがかなり低減されたのと、作るための部品が揃ってきた、という両輪で、かなり使いやすくなったと言えると思います。

なお、Edgeの状況ですが、``<template>``タグはサポートしていましたし、[ShadowDOM](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/shadowdom/)も[CustomElement](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/customelements/)も、ステータスとしては開発中になっていたわけで、Microsoftが手を引いたからWebComponentsの可用性が高まったということはありません。少し早まった、というのはあるかもしれませんが・・・R.I.P...

# 既成のWebComponentsの部品

これまではあまり積極的に使われてこなかったので、今の所はあまりないように見えます。

## Google純正Material DesignのWebComponents

https://github.com/material-components/material-components-web-components

純正の部品は、npmでは ``@material/mwc-コンポーネント名``で提供されています。mwc-buttonやmwc-switchなどがすでに提供されています。しかし、mwc-listやmwc-selectなどはまだ[非公開](https://github.com/material-components/material-components-web-components)で、すべての機能が使えるまでは時間がかかりそうです。 

なお、WebComponents以前のCSSでそれっぽい見た目を実現するコンポーネントは ``@material/コンポーネント名`` でプリフィックスなしのパッケージとなっていますので、注意してください。

## Ionic

https://ionicframework.com/

IonicはAngular用のモバイル用アドオンみたいな扱い(Angular版React Nativeみたいな)でしたが、UI部品系をWebComponentsに寄せて来ました。そして、Vueとかでも使えるよ、というPRをし始めています。 ``@ionic/vue`` とか ``@ionic/angular`` みたいなフレームワーク特化のアダプターもあるのですが、それを使わなくても使えます。

具体的には、[Ionic Packages & CDN](https://beta.ionicframework.com/docs/installation/cdn)で紹介されている方法を使って、index.htmlに.jsと.cssファイルの参照を追加します。あとは、本記事で紹介した方法を使って各フレームワークの中で使えます。Vue.jsの場合は次のような感じになります。

```ts:main.ts
Vue.config.ignoredElements = [
  'ion-app',
  'ion-page',
  'ion-header',
  'ion-toolbar',
  'ion-title',
  'ion-content',
];
```

```App.vue
<template>
  <div id="app">
    <ion-app>
      <ion-page>
        <ion-header mode="md">
          <ion-toolbar mode="md" color="primary">
            <ion-title>Ionic Vue!</ion-title>
          </ion-toolbar>
        </ion-header>
        <ion-content>
          Hello World
        </ion-content>
      </ion-page>
    </ion-app>
  </div>
</template>
```

なお、ionicのコンポーネントはイベント名が``@ionChange``だったり、``@ionInput``だったりして標準のHTMLとは違います。そのためか``v-model``属性で双方向バインディングができません。Vue.js初心者なので気づくのに時間がかかってしまいました。

https://qiita.com/ozone/items/b75efe5c449cbc469b1e

> v-modelは実はただの糖衣構文。:value(prop)と@input(event)に展開して扱われます。

## Fast

MicrosoftがかっこいいUI部品を公開しています。[デモサイト](https://explore.fast.design/components/fast-radio)もありますが、RTLの向きの変更ができたり、ダークモードにも対応しています。今までは出自がGoogle製ということもあり、マテリアルデザインのものが多かったので、貴重ですね。

https://github.com/microsoft/fast

## Google Pay Button

https://developers-jp.googleblog.com/2020/07/react-web-components-google-pay.html

フレームワーク横断で導入できる機能として、GoogleがGoogle Pay Buttonを提供しています。このようなAPI組み込みには最適ですね。ソースコード自体もscriptタグで読み込み（アプリのJSに組み込まない)で、タグだけ置くのであれば、API変更は全部提供側が吸収できて、責任分界点をかなりAPI提供側に寄せることができます。この方法は今後さまざまなSaaSベンダーとかで活用されそうな気がしますね。

## ui5

https://sap.github.io/ui5-webcomponents/

SAP製の「エンタープライズグレード」をうたうUI部品群です。ボタンとかはそこはかとなくBootstrap風。

## その他

[WebComponents.org](https://www.webcomponents.org/)に、Polymer時代のWebComponentsの実装がいろいろあります・・・・が、これはたぶんほとんど使えないでしょう。残念ながら。これはHTML importsを想定したコードが多くて、htmlをlinkタグで取り込むようなコードが多いし、親クラスとしてPolymerを同様に取り込んでいるものが多く見受けられます。しかし、[HTML importsの惨状](https://caniuse.com/#search=html%20import)をみていただければ、これらはそのまま使えないことがわかるでしょう。

## WebComponentsを自作する場合

自作で作るときの方法としては、x-tagとかAngular Elementとかあるけど、今後はlit-elementを使うので良いかと思います。あとは、skate.jsとかもありますね。Google純正のmwc-シリーズはlit-elementを、Ionicは[stencil.js](https://stenciljs.com/)を使っています。

# まとめ