Web Components の仕組みを少し理解した

現在のフロントエンド開発に於いて、[Angular.js](https://angular.jp/)、 [React.js](https://ja.reactjs.org/)、[Vue.js](https://jp.vuejs.org/index.html) のいずれかの View ライブラリを使用することが、多くなりつつあります。

実際にこれらを使って実装を進めていく中で、コンポーネントを実装するための基盤となる仕組みや未来である、「Web Components」を深く理解したいという気持ちになりました。

## Web Components とは

> Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。コードの他の部分から独立した、カプセル化された機能を使って実現します。 
> ( 引用元: [Web Components | MDN](https://developer.mozilla.org/ja/docs/Web/Web_Components) )

HTMLのパーツ ( コンポーネント ) をカプセル化して再利用可能なものにするための技術のことです。

カプセル化することで、コンポーネント同士の依存関係を防ぐことや再利用性が上がることがメリットだと考えらます。

## Web Components を構成する技術

1. Costom Elements
2. Shadow DOM
3. ES Modules
4. HTML Templates

## 1. Costom Elements ( カスタム要素 )

> Webコンポーネント標準の重要な特徴の一つはカスタム要素を作れることです。それはページの機能を提供する長く> ネストした要素のバッチではなく、HTMLページ上で機能をカプセル化します。
> ( 引用元: [Using custom elements - Web Components | MDN](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_custom_elements)

任意の HTML タグを作成することができる JavaScript API です。

```
<hoge-component></hoge-component>
```

こんなタグも自由に作成できます。

### カスタム要素の第一歩

``` main.js
class MyButton extends HTMLElement {
    constructor() {
        super();
    }
}
customElements.define('my-button', MyButton);
```

1. カスタム要素を定義するために `HTMLElement` を継承したクラスを作成
2. 作成したクラスを `customElements.define()` 関数で要素として登録

``` index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Web Components</title>
</head>
<body>
<my-button></my-button>
<script src="./main.js"></script>
</body>
</html>
```

これだけだと、なにが良いのか分からないので機能を追加していきます。

### ボタン機能の作成

1. 作成したカスタム要素の中に button 要素がバインドされるようにします。
2. ボタンをクリックしたときは p 要素に変わるようにします。

``` main.js
class MyButton extends HTMLElement {
    constructor() {
        super();
        + this.innerHTML = `<button type="button">${this.innerHTML}</button>`;
        + this.addEventListener('click', e => {
        +     this.innerHTML = '<p>今日は良い天気ですね。</p>'
        + })
    }
    + // オマケ ( 要素が DOM に追加された時のイベント )
    + connectedCallback () {
    +     console.log('ボタン要素が DOM にマウントされたで');
    + }
}
customElements.define('my-button', MyButton);
```

文字 ( innerHTML ) を追加

``` index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Web Components</title>
</head>
<body>
+ <my-button>ボタン</my-button>
<script src="./main.js"></script>
</body>
</html>
```

こんな感じで、好きに要素を追加したり、イベントを追加したり、マウント後に何かをしたりできます。

...こんなん別にカスタム要素じゃなくても、普通に書けるやんって感じですが、Shadow DOM と組み合わせることで本当の力を発揮します。

## 2. Shadow DOM

> Web コンポーネントにおいてカプセル化 (構造やスタイル、挙動を隠し、同じページの他のコードと分離すること) は重要です。これにより他の場所でのクラッシュを防ぎ、またコードが綺麗になります。Shadow DOM API はこの隠され分離された DOM を付加するための方法を提供しています。この記事では Shadow DOM を使う基本を記述しています。
> ( 引用元: [shadow DOM の使い方 - Web Components | MDN](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_shadow_DOM) )

従来のフロントエンドでは、HTML, CSS, JavaScript それぞれのファイルで無数のコンポーネントを作成し、管理していました。

しかし、すぐ破綻します。

CSS を上書きし、さらに上書きし、もうダメだと思って新しいコンポーネントを作成し、不要なコンポーネントが増え続け、さらにクラス名を変えたことにより JavaScript が動かなくなったり...、おいお前の処理はどこに書かれとんや...。

もうあかん、、、リプレイスや...。

こういった事態を、[BEM](https://en.bem.info/methodology/) という命名規則や、[SMACCS](http://smacss.com/ja) といった設計手法を用いて、メンテナブルに開発していくことで、解決する手法がとられていました。

そこで、すごい人は考えました。

もう、HTML も CSS も JavaScript もひとつのファイルで作ってカプセル化したらええやん！

### 簡単な Shadow DOM を作ってみる

1. `attachShadow` を呼び出し Shadow DOM を作成。
2. 引数に `{ mode: 'open' }` を指定。( `shadowRoot` を使ってアクセスできるようなります )
3. shadowRoot を呼び出し、ボタン要素やスタイルを作成 ( 通常の DOM ツリーから分離してレンダリングされるようになります )

``` main.js
class MyButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                button {
                    border: 0;
                    font-weight: bold;
                    color: #fff;
                    background-color: green;
                    border-radius: 3px;
                    padding: 5px;
                    display: block;
                    margin-bottom: 10px;

                }
            </style>
            <button type="button">${this.innerHTML}</button>
        `;
    }
}
customElements.define('my-button', MyButton);
```

1. カスタム要素と Shadow DOM で作成したコンポーネントを追加
2. 通常のボタン要素を追加

``` index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Web Components</title>
</head>
<body>
<my-button>Shadow DOM で作ったボタン</my-button>
<button type="button">普通のボタン</button>
<script src="./main.js"></script>
</body>
</html>
```

<img width="1111" alt="スクリーンショット 2019-11-16 11.59.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221598/17ef04a8-ea08-d3da-844f-00e34d7783fb.png">

カプセル化された！！！！

これで、どこにも影響を与えないコンポーネントをいっぱい作れるやで！！！！

...え

...コンポーネント毎に `<script src="./component.js"></script>` 書くのダルい...

## 3. ES Modules

> import 文は、他のモジュールからエクスポートされたバインディング（関数、オブジェクト、プリミティブ）をインポートするために用います。インポートされたモジュールは宣言のあるなしにかかわらずStrict モードで動作します。import 文は、type="module" を指定しない限り、埋め込まれたスクリプトでは使えません。
> ( 引用元: [import - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import) )

外部 JavaScript を動的に読み込む機能のことです。

### 従来の外部 JavaScript の読み込み

``` index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Web Components</title>
</head>
<body>
<script src="./MyComponent1.js"></script>
<script src="./MyComponent2.js"></script>
<script src="./MyComponent3.js"></script>
</body>
</html>
```

この方法だと、読み込み順を気にしたりなど、扱い辛い部分があります。

### ES Modules やってみる

export 文で、Shadow DOM の MyButton クラスをエクスポート

``` components/MyButton.js
export default class MyButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `<button type="button">${this.innerHTML}</button>`;
        // オマケ ( 要素がクリックされた時のイベント )
        this.addEventListener('click', () => {
            console.log(this.innerHTML);
        })
    }
}
```

1. scriptタグに `type="module"` を指定 
2. import 文で作成した MyButton クラスをインポート
3. ボタンを三つ作ってみる

``` index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Web Components</title>
</head>
<body>
<my-button>ボタン1</my-button>
<my-button>ボタン2</my-button>
<my-button>ボタン3</my-button>
<script type="module">
    import MyButton from './components/MyButton.js';
    customElements.define('my-button', MyButton);
</script>
</body>
</html>
```

良い感じ！！！！それぞれのボタンがちゃんと動いてる！！！！

ただ、もっと簡単に HTML 書きたい！！！！

## 4. HTML Templates

> あるWebページ上で同じ構造を繰り返し使用する必要がある場合、同じ実装を繰り返し書くよりも、テンプレートのようなものを作って利用する方が合理的でしょう。これは以前から可能でしたが、<template> 要素より簡単に使えるようになりました。 この要素と中身は DOM 上ではレンダリングされませんが、JavaScript から参照することができます。
> ( 引用元: [template と slot の使い方 - Web Components | MDN](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_templates_and_slots) )

JavaScript の中に、 HTML を書くことができます。

### HTML Templates で MyForm クラスを作る

1. `shadowRoot.innerHTML` を template を指定
2. template に HTML を組み立てる

``` components/MyForm.js
export default class MyForm extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = this.template();
    }

    template () {
        return `
            <p style="margin:0;">検索フォーム</p>
            <form action="">
                <input type="text">
                <button type="button">送信</button>
            </form>
        `;
    }
}
```

よっしゃ、HTML っぽく書けた！！！！

どこにも依存していない、別世界のコンポーネントを作れたやで！！！！

## ブラウザ対応

> ![スクリーンショット 2019-11-14 20.46.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221598/0baefa2e-bb6d-e78c-77a0-4bd0240cdf81.png)
> ( 引用元: [webcomponents.org - Discuss &amp; share web components](https://www.webcomponents.org)

IE...

## クロージング

View ライブラリを使わず Web Components だけでカプセル化されたコンポーネントを作ることができました。

この技術が新たな Web 標準となるかもと考えると少し WAKUWAKU しました。