仮想DOMの時代にDOMを返すAPIを扱う（CSR編）

[DMMグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/dmm) 12日目の記事です。

アドカレ映えしない、地味～な内容ですが、よろしくお願いします。

# TL;DR

Angular / React / Vue製サイトにWebAPI経由で返されたDOMを挿入する

|Angular|React|Vue|
|:--|:--|:--|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79697/93d73222-3f13-3ebe-180f-9106a35e6490.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79697/99bde065-6555-1721-10f8-9cd958b49364.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79697/7842c77d-933c-1a12-e5ab-180463bfe269.png)|



# 前書き
大規模Webサイトの各ページへ横断的に要素を挿入したい要件があるとします。

 * ブランドロゴ、ナビゲーション、トラッキングタグなど
 * 大規模なので、ページ間で管轄が分かれていたり、別々のアーキテクチャで構築されていたりする
 * 要素の管轄は一部署に集約したい

これらの要件を満たす手段として、 **DOM Stringを返すようなAPIが提供される** ことがあります。

他の手段としては「DOMを構築・挿入するスクリプトを配布する」という方法もありますが、それと比べて以下のようなメリットが期待できます。

* 呼び出しタイミングや組み込み位置をある程度制御できる
* サーバサイド・クライアントサイドのどちらからでも呼び出せる

「クライアントサイドからでも呼び出せる」とはいえ、必ずしもすんなりできるとは限らず、特に昨今のJSフレームワークで開発されたWebサービスに組み込むにはハマりどころが色々あります。

今回はAngular, React, Vue製でクライアントサイドレンダリングするようなWebサイトにそのようなAPIを組み込む際の方法や注意を纏めています。


# [全般]DomStringをDOMとしてrenderされるようにinnerHtmlラッパを使う

いずれのフレームワークもXSS対策のため、テンプレートやJSX中で使用される変数はHTMLエスケープされるようになっています。

そのため、APIレスポンスのDOM stringをそのままテンプレートやJSXへ書き出そうとしても、HTMLがプレーンテキストとして表示されるだけでHTML要素としては読み込まれません。

そこで、変数中のHTMLをHTML要素として表示するための機能が提供されておりますので、そちらを利用します。

大前提としてAPI側のXSS対策はなされているものとします。

|Angular|React|Vue|
|:--|:--|:--|
|[innerHTML](https://angular.jp/guide/template-syntax)|[dangerouslySetInnerHTML](https://ja.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml)|[v-html](https://vuejs.org/v2/guide/syntax.html#Raw-HTML)|

# [linkタグ]Angularの場合はlinkタグの扱いに注意

APIレスポンスの中に、スタイルシートを読み込むためのlinkタグが含まれる場合の話です。

React, Vueは上記の方法でlinkタグを出力することが出来るのですが、AngularではinnerHTMLでHTML要素を出力する際に、やはりXSS対策のためlinkタグやscriptタグが削除されるようになっています。

これを回避する方法も提供されており、[DomSanitizerのbypassSecurityTrustHtmlメソッド](https://angular.io/api/platform-browser/DomSanitizer)を利用することでlinkタグを出力することができます。

# [scriptタグ]innerHTMLではJSを実行出来ない

APIレスポンスにscriptタグが含まれる場合は厄介です。

innerHTML（やこれまで挙げたFWのinnerHTMLラッパ）を利用することで、DOMやスタイルを反映させることができますが、scriptタグ経由のJSを呼び出すことは出来ません。

これは[W3CのHTML5 scriptタグの仕様](https://www.w3.org/TR/2014/REC-html5-20141028/scripting-1.html
)として記述されています。

> When inserted using the document.write() method, script elements execute (typically synchronously), but when inserted using innerHTML and outerHTML attributes, they do not execute at all.

これについては、フレームワーク側のサポートはなく、[動的にJSを実行する方法](https://qiita.com/w650/items/adb108649a0e2a86f334)などに記載されているように、

1. `document.createElement('script');` でscript要素を作成、
2. domStringをパースしてscript要素を組み立てていき、
3. 最終的に `document.body.appendChild` などで完成したscript要素を挿入する

という、骨の折れる作業をする必要がありそうです。

# [実践]ヘッダー・フッターでコンテンツを挟み込む

ここで少し具体的な話になります。

APIのレスポンスとして、複数のUI、例えばヘッダーとフッターが一遍に返されるような場合はどのように実装すればよいでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79697/44721be2-6c0e-a959-8e63-7bf9bfbe7d26.png)

この場合、ヘッダー・フッターを1つのコンポーネントとして扱うのが良さそうです。

各フレームワークとも、似たようなかたちで任意のコンテンツを挟み込むようなコンポーネントを実装出来ます。

|Angular|React|Vue|
|:--|:--|:--|
|[ng-content](https://angular.jp/guide/hierarchical-dependency-injection)|[props.children](https://ja.reactjs.org/docs/glossary.html#propschildren)|[slot](https://vuejs.org/v2/guide/components-slots.html)|

## 実装例 / Angular

```html:app.component.html
<navigation>
  <!-- サイトコンテンツ -->
</navigation>
```

```html:navigation.component.html
<div [innerHTML]='header'></div>
<ng-content></ng-content>
<div [innerHTML]='footer'></div>
```

## 実装例 / React

```html:App.js(内のJSX)
<Navigation>
  <!-- サイトコンテンツ -->
</Navigation>
```

```Navigation.js(内のJSX)
<div dangerouslySetInnerHTML={{ __html: header }} />
  {this.props.children}
<div dangerouslySetInnerHTML={{ __html: footer }} />
```

## 実装例 / Vue
```html:index.html
<navigation>
  <!-- サイトコンテンツ -->
</navigation>
```

```html:template
<div v-html="header"></div>
  <slot></slot>
<div v-html="footer"></div>
```

# おわり

* 本記事はDOMを返すようなAPIをmanageする、というのがテーマであり、共通UIを提供する手段としてDOMを返すAPIの設計を推奨するものではありません。
* アプリケーションをSSRする場合は更なる課題がありそうですので、それはまた次の機会に。
* 明日は @uruha さんより、Virtual DOMの時代の楽しい話が聞けるんじゃないかと思うので、乞うご期待！

## サンプルアプリの元ネタ

* Vue https://jp.vuejs.org/v2/examples/ https://github.com/vuejs/vue/tree/dev/examples
* React https://reactjs.org/community/examples.html
* Angular https://github.com/gothinkster/angular-realworld-example-app