Angularで作ったアプリをプラグインを使わずにConfluenceに埋め込む

Confluenceにない機能をHTMLマクロやユーザマクロにJavascriptを埋め込むことで実現しようとしてきたのだが、ちょっとした壁があることが分かった。HTMLマクロではユーザがページを編集する際に触れてしまう危険性があり、ユーザマクロはVelocity Template EngineがJavascriptをパースしてエラーを起こしてしまうため、Javascriptをユーザに届ける方法に工夫が必要なためだ。

> Javascriptをエスケープする手段としてユーザマクロ内で使える`$generalUtil.escapeForJavascript(String s)`メソッドがあるものの、引数にWebpackしたJavascriptをエスケープしてから入れる手間とサーバへの負担がかかる。(参考: [Class GeneralUtil](https://docs.atlassian.com/atlassian-confluence/6.1.1/com/atlassian/confluence/util/GeneralUtil.html))

そこで、Confluenceの静的ファイルを提供する機能を利用し、(多分)サーバに負担をかけることなくAngularなどで作成したアプリをConfluence上で提供するよう設定する。ユーザへの提供の仕方は以下の2通りを紹介する。この方法はAngularに限らず、Javascriptのアプリなら適用可能かと思う。

* Confluenceと関係なくAngularを表示する
* Confluenceのページ内にAngularを埋め込む(ユーザの書いたページの内容を操作するのに使える)

# 完成イメージ
「Confluenceのページ内にAngularを埋め込む」では、編集画面でAngularを配置するユーザマクロを置くと、表示画面でユーザマクロを置いた場所にAngularのアプリが表示できるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/58580616-c8dd-a826-7165-9fcfceda0341.png)

# 静的ファイルを提供するには
Confluenceには、「[How to Use Confluence to Serve Static Content](https://confluence.atlassian.com/confkb/how-to-use-confluence-to-serve-static-content-677282407.html)」に書かれている通り、`<CONFLUENCE_INSTALL>/confluence`以下にファイルを置くと、例えば`myfile.html`であれば`<CONFLUENCE_BASEURL>/myfile.html`で参照できるようになる。

* `<CONFLUENCE_INSTALL>`: Confluenceのインストールディレクトリ
* `<CONFLUENCE_BASEURL>`: ConfluenceのベースURL。「[ConfluenceとJiraをnode.jsで作った簡易プロキシの後ろで動かす](https://qiita.com/stm32p103/items/caf48e8dfc47f04216d9)」で書いたように変えられる)

# Confluenceと関係なくAngularを表示する
Angularの`index.html`はそのままでは使えないので、下記「index.htmlの変更」に示すように、必要なファイルが正しく参照できるようにする。変更後`ng build`でビルドしたファイルを、静的ファイルを提供するためのフォルダにコピーすれば開けるようになる。

## index.htmlの変更
Angularプロジェクトはそのままでは使えないので、`index.html`を以下のように変更する。

1. [CONFSERVER-53166](https://jira.atlassian.com/browse/CONFSERVER-53166?_ga=2.107832320.300459407.1569857137-422056535.1565451168) バグ対策として、デコレータを指定する`<meta>`タグを追加する。これが無いとページが表示されないので要注意。contentはnone以外NG。
1. `<base href="/">`を`<base href=".">`に変更する。Angularはデフォルトではルートを基準にしているので、この設定をしないとスクリプトを正しく読み込めなくなる。

```html:index.html
<!doctype html>
<html lang="ja">
<head>
  <!-- [1] バグ対策 -->
  <meta name="decorator" content="none"/>

  <!-- [2] スクリプト等を相対参照するのに必要 -->
  <base href=".">

  <meta charset="utf-8">
  <title>Sample</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>
```

## confluenceフォルダへのコピー
`index.html`を変更したら、`ng build`や`ng build --aot`でビルドする。ビルド結果はデフォルトで`dist/<プロジェクト名>`以下にファイルが生成される。このファイルをConfluenceの静的ファイルを置くフォルダconfluenceにコピーする。

例えば、<CONFLUENCE_INSTALL>/confluence/angular 以下にコピーした場合は、`<CONFLUENCE_BASEURL>/angular/index.html`を開くと以下のように表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/c0ea58fb-cf94-c249-152a-d64eb40fca69.png)
>`index.html`は省略不可。省略するとConfluenceのホーム画面?が表示されてしまう。

この方法は簡単な反面、Confluenceのログインとは無関係に閲覧できてしまうので注意が必要となる。

> 自分のPCで試したところ、`ng serve`した時と比べるとConfluence経由の表示はとても遅いことが分かった。Javascriptの読み込みに20秒近くかかってしまう理由が良く分からなかった。Javascriptを直接開くだけなら一瞬なのに、どうしてこんなに差が出るのかは不明。裏で何かチェックしている?

# Confluenceのページ内にAngularを埋め込む
Confluenceのページ内に、`<app-root>`要素を配置し、必要なスクリプトやスタイルシートを読み込むタグを追加することで、Confluenceのページ内にAngularのアプリを埋め込むことができる。これをユーザマクロにまとめておくことで、ページにユーザマクロを追加するだけでAngularのアプリを埋め込めるようになる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/58580616-c8dd-a826-7165-9fcfceda0341.png)

## 静的ファイルの用意
Angularで`ng build`して生成されたファイルのうち、`index.html`で参照されるファイルを`<CONFLUENCE_INSTALL>/confluence`以下にコピーする。ファイルは複数あるので、サブディレクトリにまとめておいた方が無難かと思う。

```html:index.html
<!doctype html>
<html lang="ja">
<head>
  <meta name="decorator" content="none"/>
  <base href=".">
  <meta charset="utf-8">
  <title>EmbedAngular</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- スタイルシート -->
  <link rel="stylesheet" href="styles.09e2c710755c8867a460.css">
</head>
<body>
  <app-root></app-root>
  <!-- Javascript(古いブラウザを見限る設定の場合は3種類だけ) -->
  <script src="runtime.69aad174de386c36a16c.js" defer></script>
  <script src="polyfills.18e909cd5b286120e7aa.js" defer></script>
  <script src="main.5a641e340bf9373c7a35.js" defer></script>
</body>
</html>
```
ユーザマクロから参照しやすいよう、スタイルシートとJavascriptのファイル名を以下のように整えておくと良い。

* style.css
* runtime.js
* polyfills.js
* main.js

## Javascriptをロードするユーザマクロを追加する
Angularを表示するユーザマクロにAngularのアプリを表示する`<app-root>`に加え、Javascriptを遅延ロードするスクリプトを追加する。(わざわざなんで?と思うかもしれないが、これは後述する「つまづいたところ」の良く分からない挙動の回避策として必要)

```html:ユーザマクロ
# BaseURLに対して、/confluence/angular 以下にAngular関連のファイルを置いている場合
## @noparams
<!-- ここにAngularのアプリが表示される -->
<app-root></app-root>

<!-- 遅延ロード用のスクリプト -->
<script type="text/javascript">
( () => {
  //-----------------------------------------------------------------
  const head = document.getElementsByTagName( 'head' )[0];

  //-----------------------------------------------------------------
  const link = document.createElement( 'link' );
  link.setAttribute( 'rel', 'stylesheet' );
  link.setAttribute( 'href', '/confluence/angular/styles.css' );
  head.appendChild( link );

  //-----------------------------------------------------------------
  const scripts = [
    '/confluence/angular/runtime.js',
    '/confluence/angular/polyfills.js',
    '/confluence/angular/main.js',
  ];
  scripts.forEach( path => {
    const elem = document.createElement( 'script' );
    elem.setAttribute( 'type', 'text/javascript' );
    elem.setAttribute( 'src', path );
    head.appendChild( elem );
  } );
//-----------------------------------------------------------------
} )();
</script>
```

## MIMEタイプのエラーが発生する場合
コンソールで以下のようなMIMEタイプに関連するエラーが発生する場合は、JavascriptやCSSのパスを間違えている可能性がある。静的ファイルのパスを間違えると、Confluenceはホーム画面か何かを表示するようなので、`.js`なのに`html`が返ってきてしまい、下記のようなエラーが発生する。

```
MIME タイプ (“text/html”) が許可されていないため、“...” からのモジュールの読み込みがブロックされました。
MIME タイプ (“text/html”) の不一致により “...” からのリソースがブロックされました (X-Content-Type-Options: nosniff)。
```

## つまづいたところ
Confluenceのページ内にAngularを表示させようとして、Angularの`index.html`に倣って以下のようなユーザマクロを作ったところ、うまくいかなかった。表示画面を開いたところ、Angularのアプリが表示されないことに加え、ユーザマクロ以降の要素がレンダリングされなくなってしまうことが分かった。Javascriptのロードにかかる20秒ほどの間、Confluence上のUIが動かなくなってしまうことも分かった。

```html:user-macro
## @noparams
<app-root></app-root>
<script src="/confluence/angular/runtime.js" defer></script>
<script src="/confluence/angular/polyfills.js" defer></script>
<script src="/confluence/angular/main.js" defer></script>
```
推測だが、Confluenceで使っているVelocity Template EngineかCatalinaの仕様で、タイムアウトやJavascriptのチェックが働き、レンダリングを阻害しているのかもしれない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/c4e65f5a-2810-c92a-3b68-b5032cab1023.png)

#応用
Angularのアプリを表示するマクロは他のマクロと同様に「展開」のようなマクロ以下に置くこともできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/9f286f87-90c8-bb9f-5edd-a636d67d598d.png)

表示画面で「展開」で隠されている部分を開くと、Angularのアプリを表示することもできる。これを使えば、普段は操作画面を隠しておき必要な時だけ表示する、ということもできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/7436900b-41c0-75c4-383b-28be97d70b48.png)

他にも、ユーザマクロのスクリプト・スタイルシートへのパスをパラメータ化しておけば、1つのユーザマクロで複数のアプリに対応することも可能になる。(使うときに注意が必要だが…)

#まとめ
Confluenceの静的ファイル提供機能を用いて、Angularで作成したアプリをConfluenceで表示する方法を紹介した。

単純にConfluenceをHTTPサーバとして使ってAngularで作成したアプリを提供する場合、`index.html`に少し手を加えたうえで`ng build`で生成したファイルを`<CONFLUENCE_INSTALL>/confluence`以下にコピーするだけで良い。

Confluenceのページ内にAngularで作成したアプリを埋め込んで提供する場合、`ng build`で生成したJavascript, CSSを`<CONFLUENCE_INSTALL>/confluence`以下にコピーした上で、ユーザマクロから遅延ロードさせる。(そうしないと表示が欠ける・ロード中ユーザの入力を妨げるという問題が起きる)