“Web Componentsだけ” で新サービスを実装して見えたこと

[Double O](https://ooapp.co/) というサービスを作りました。

フロントエンドはピュアな Web Components を採用していて、バックエンドは Lambda と DynamoDB のみで構成しました。 (厳密には CloudFront とか API Gateway とかもあるけどそこは省いていいよね？)

REST API 以外の Util 系の Lambda 関数はすべて AWS Cloud9 で管理することで環境構築も不要な Lambda ができて楽でした。

# TL;DR

サーバーレスについてはごく普通のことしかしていないので、詳しくは触れないでおきます。

1. ピュアな Web Components だけでサービスを成立させることができた。
2. `HTMLElement` クラスを継承するだけなのでメジャーライブラリは不要になった。
3. Web Components の Custom Elements は標準仕様に則った要素であるから、ドメイン依存を排除した設計によって所謂 Embed code を代替しえる。
4. REST API ではなく Custom Elements をサードパーティー向けに公開する可能性が出てきた。

Web Components による開発の具体的な手法については、また別の記事にしたいと思います。

# はじめに

今回リリースした [Double O](https://ooapp.co/) はウェブ上のどこからでも Ask me anything を受け付けらるサービスです。

![Ask Me](https://qiita-image-store.s3.amazonaws.com/0/71785/1ac762f7-bbc1-3dbd-e657-3db1b6b81198.png): https://ooapp.co/JEKAgXAqHmqC0jBoH2zc8l7E

上の画像のようなフォームは、`<oo-ask>` という Custom Elements で実現していて、HTML に次のように書くだけで利用できます。

```html
<oo-ask data-iam="JEKAgXAqHmqC0jBoH2zc8l7E"></oo-ask>
```

もちろん先に Custom Elements を定義したスクリプト、つまり `HTMLElement` を継承した `oo-ask` 要素クラスを `customElements.define` に渡しておく必要があります。これは CDN でも配信しているので、

```html
<script async src="//elements.ooapp.co/stable/oo-ask.js"></script>
```

と定義済みのものを読み込んでおくか、次のように ES Modules を使って自前で定義しておきます。

```html
<script type=module>
    import OOAsk from '//elements.ooapp.co/stable/oo-ask.mjs'
    customElements.define('oo-ask', OOAsk)
</script>
```

先のフォームをモーダルダイアログに格納しておいてボタンで開く `<oo-button>` という Custom Elements もあります。

![fiddle.PNG](https://qiita-image-store.s3.amazonaws.com/0/71785/e817a04c-eae3-e9a6-711d-a92c3596ee72.png): https://jsfiddle.net/aggre/racxtdug/

受け付けた質問は、`<oo-project>` という Custom Elements で確認できます。

![Project](https://qiita-image-store.s3.amazonaws.com/0/71785/de5bb782-cb0e-9d84-6d63-86623cfbc43b.png): https://ooapp.co/project/2wgNxq2Da7sLhrf4sODq4ZsD

```html
<oo-project data-uid="2wgNxq2Da7sLhrf4sODq4ZsD"></oo-project>
```

自分宛ての質問の一覧は、`<oo-projects>` という Custom Elements で確認できます。

![Projects](https://qiita-image-store.s3.amazonaws.com/0/71785/878560e1-4536-9573-9697-68ec8c334956.png)

HTML はこう。

```html
<oo-projects data-iam="JEKAgXAqHmqC0jBoH2zc8l7E"></oo-projects>
```

## ようするに

フォーム、ボタン、回答一覧などすべての要素を Custom Elements として配信しているので、どんなドメインでも自由な組み合わせで Ask me anything を受け付けることができるサービスになりました。

これらすべてが React や Vue など( Web Components とはいえ Polymer も)使わずに、実現することができました。

一例として、ちょっとしたウェブページにも次のコードを足せば、受付フォームと過去の質問一覧を出すことができます。

```html
<script async src="//elements.ooapp.co/stable/oo-elements.js"></script>

<oo-ask data-iam="{Your UID}"></oo-ask>
<oo-projects data-iam="{Your UID}"></oo-projects>
```

# ライブラリが不要

ブラウザが標準で備えている `HTMLElement` クラスを継承して要素を作っていくだけなので、コンポーネントのためのライブラリが不要になります。

[Double O](https://ooapp.co/) のなかで一番シンプルな `<oo-version>` という要素を例にしてみます。

```ts
import {html, render} from 'lit-html'

const {PACKAGE_VERSION: version} = process.env

export default class extends HTMLElement {
	connectedCallback() {
		this.render()
	}

	html(v: string) {
		return html`
		<span>${v}</span>
		`
	}

	render() {
		render(this.html(version), this)
	}
}
```

TypeScript です。ブラウザなのに `process.env` があって変な感じですが、ここはビルドの際にただの文字列に変わります。

ここで重要なのは、継承元の `HTMLElement` はどこからも `import` していないということです。

外部のライブラリで依存しているのは `<template>` を扱うための [lit-html](https://github.com/Polymer/lit-html) という小さなライブラリのみです。

コンポーネント開発をするために React などのライブラリを習得する必要がなくなり、またライブラリ分のファイルサイズも 0 になります。lit-html は開発効率のために採用しましたが、利用する API によるものの 4kb ~ 8kb 程度のサイズに収まります。

lit-html については、以前に書いた [lit-htmlとバニラWeb Componentsでコンポーネントを実装する](https://qiita.com/aggre/items/3ed2558a9fb8ba887385) を参照いただけると分かりやすいはずです。

# ドメイン依存の排除

これまでのコンポーネントの開発は、それがひとつのドメイン( ≒アプリケーション )で動作することを目指したものが大半のはずです。

コンポーネント自体はドメインに依存していなくても、`ReactDOM.render` を実行できる環境を整えるため事実上はすべてをコントロールできる環境下でなければ使えないことが多いのではないかと思います。

Web Components は **DOM 操作ではなく**、Custom Elements という形でブラウザが解釈可能な要素としてコンポーネントを提供します。これによってドメイン依存を排除するモチベーションが変わってきます。

ドメイン依存を排除することで、自分の開発したコンポーネントがひとつのサービスに縛られず、どのようなウェブページでも利用可能になります。

これまで Embed code を発行することで他サイトに機能を提供していたサービスであれば、このベネフィットは分かりやすいはずです。

他サイトでの利用を想定していなかったサービスだとしても、価値の新しい提供方法として検討することができます。例えばニュース記事を Custom Elements で提供すれば、記事の引用を制御可能なものにすることができます。

# Web Components による開発プロジェクト

[Double O](https://ooapp.co/) で利用しているリポジトリは全部で 3 つあります。

- [Custom Elements 用リポジトリ (GitHub)](https://github.com/frame00/oo-elements)
- [テンプレート用リポジトリ (GitHub)](https://github.com/frame00/oo-app)
- REST API 用リポジトリ (非公開)

詳しくはまた後で書くとして、参考までに俯瞰していきます。

## Custom Elements

すべての Custom Elements を管理するリポジトリです。

![oo-elements](https://qiita-image-store.s3.amazonaws.com/0/71785/420fccab-e368-9f73-5c13-1ea7dbdfa3b5.png): https://github.com/frame00/oo-elements/tree/master/src/elements

Custom Elements をひたすら書いて、[ES Modules のためにエクスポート](https://github.com/frame00/oo-elements/blob/master/src/index.ts) をするファイルと、[定義済みの Custom Elements](https://github.com/frame00/oo-elements/blob/master/src/define.ts) のためのファイルがメインです。

このリポジトリでは Custom Elements だけを管理していて、レイアウトはしません。

## テンプレート

これは `<template>` のほうではなく、サーバサイドのテンプレートです。

![oo-app](https://qiita-image-store.s3.amazonaws.com/0/71785/cc9bd820-dd97-1c42-f4ca-31da182a842c.png): https://github.com/frame00/oo-app/tree/master/src/page

テンプレートとはいえ、Custom Elements を組み合わせるのと、簡単な CSS を書くだけで、メモ帳で書くようなレベルの HTML しかありません。

Custom Elements を組み合わせてレイアウトするため専用のリポジトリになっています。

## REST API

テンプレートと同じように Lambda 関数として開発しています。これは非公開なのでお見せできません。

# 新しい API の提供方法としての Custom Elements

あるサービスの一部の機能を使いたいときの手続きも、Custom Elements で提供することができます。

REST API のために OAuth 認証をしてドキュメント通りにリクエストを投げて、エラーハンドリングして、結果をパースして、などという手間をすべて Custom Elements 側で実行すれば、SDK よりも簡単に API を利用できるようになるはずです。

実際、`<oo-ask>` 要素などはプロフィールを取得する REST API と、質問を投稿する REST API を内包していますが、利用者は REST API のことを意識することはありません。

今後、スタイルも持たない、純粋に REST API の代替として機能する Custom Elements を作っていきたいと考えています。

# さいごに

- [Double O](https://ooapp.co/) は誰でも無料で利用できます。ブログなどに `<oo-button>` を置けばいつでも AMA を受け付けることができます。ぜひ使ってみてください。
- プライベート質問に有償の回答もできます。デフォルトで ¥500 ですが、あとで自由に金額を設定できます。オファーとしても使ってみてください。
- `oo-elements` はまだ開発途上です。バグや要望などがあれば https://github.com/frame00/oo-elements から報告いただけると助かります。

私の AMA は https://ooapp.co/JEKAgXAqHmqC0jBoH2zc8l7E で受け付けています。何でも聞いてください。