RedmineのView Customizeを用いたJavaScript OSSの組み込み

## 目的

Github上には大量のJavaScript OSSが公開されており、それらをRedmineに組み込めれば機能を大きく拡張できるようになる。

redmine_view_customizeプラグインはv2.1.0から従来のCSS/JavaScriptに加えHTMLを埋め込めるようになった。
この機能とプロジェクトの添付ファイルを利用してRedmineのシステム管理者権限のみで（つまりサーバーへログインせずに）JavaScript OSSを組み込める。

この記事では例としてJavaScriptのOSS「[BigPicture.js](https://github.com/henrygd/bigpicture)」を組み込み、チケットの添付ファイルの画像／音声／動画のプレビュー機能を強化する

Redmine 4.0.3とView Customize v2.1.0で確認した。

## スクリプト配置用プロジェクトの作成

スクリプト配置用プロジェクトを作成する。ここでは「ViewCustomizeVault」とした。

このプロジェクトの「ファイル」メニューにJavaScriptを配置する。
[BigPicture.js](https://github.com/henrygd/bigpicture)では [BigPicture.min.js](https://github.com/henrygd/bigpicture/tree/master/dist) を添付する。

![Greenshot 2019-05-17 18.36.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85304/7623ea51-c18c-d059-4867-382e45ec81f8.png)

## View Customizeの設定

BigPicture.min.jsとビジネスロジックを埋め込む。

\<script>タグのsrc属性には添付した BigPicture.min.js のダウンロードURLを入れる。

このコードでは画像／音声／動画を内部で判別して該当するプレビューAPIを呼び出している。
実際に表示可能かどうかはブラウザ次第になるものの、ブラウザのアドオンやプラグイン側でプレビュー機能を拡張することで追従しやすくなる。

* Path pattern: /issues/[0-9]+
* Type: HTML

```html
<script src="/redmine/attachments/download/1/BigPicture.min.js"></script>
<script>
window.addEventListener('load', function() {
    const mimeTypes = {
        "apng": "image/apng",
        "bmp": "image/x-ms-bmp",
        "gif": "image/gif",
        "jpeg": "image/jpeg",
        "jpg": "image/jpeg",
        "ktx": "image/ktx",
        "pic": "image/x-pict",
        "png": "image/png",
        "psd": "image/vnd.adobe.photoshop",
        "rgb": "image/x-rgb",
        "svg": "image/svg+xml",
        "svgz": "image/svg+xml",
        "tfx": "image/tiff-fx",
        "tga": "image/x-tga",
        "tif": "image/tiff",
        "tiff": "image/tiff",

        "aac": "audio/x-aac",
        "aif": "audio/x-aiff",
        "aiff": "audio/x-aiff",
        "m3a": "audio/mpeg",
        "m4a": "audio/x-m4a",
        "mid": "audio/midi",
        "midi": "audio/midi",
        "mp3": "audio/mpeg",
        "mp4a": "audio/mp4",
        "oga": "audio/ogg",
        "ogg": "audio/ogg",
        "wav": "audio/x-wav",
        "weba": "audio/webm",
        "wma": "audio/x-ms-wma",

        "asf": "video/x-ms-asf",
        "asx": "video/x-ms-asf",
        "avi": "video/x-msvideo",
        "h264": "video/h264",
        "jpgv": "video/jpeg",
        "m2v": "video/mpeg",
        "m4v": "video/x-m4v",
        "mov": "video/quicktime",
        "mp4": "video/mp4",
        "mp4v": "video/mp4",
        "mpeg": "video/mpeg",
        "mpg": "video/mpeg",
        "mpg4": "video/mp4",
        "ogv": "video/ogg",
        "webm": "video/webm",
        "wmv": "video/x-ms-wmv",
        "wmx": "video/x-ms-wmx"
    };

    var click_OpenThumbnail = function(e){
        const ext = e.target.getAttribute('data-dp').split(".").pop();
        const mime = mimeTypes[ext];
        if(/^image\//.test(mime)){
            BigPicture({el: e.target, imgSrc:e.target.getAttribute('data-dp')});
        }
        else if(/^audio\//.test(mime)){
            BigPicture({el: e.target, audio:e.target.getAttribute('data-dp')});
        }
        else if(/^video\//.test(mime)){
            BigPicture({el: e.target, vidSrc:e.target.getAttribute('data-dp')});
        }
        else{
            BigPicture({el: e.target, iframeSrc:e.target.getAttribute('data-dp')});
        }
        e.preventDefault();
    }

    document.querySelectorAll('a:not(.icon-only)[href*="/attachments/"]').forEach(link => {
        const src = link.pathname.replace("/attachments/", "/attachments/download/") + "/" + link.text;
        const thumb = link.pathname.replace("/attachments/", "/attachments/thumbnail/")+"/200";

        link.setAttribute('data-dp', src);
        link.setAttribute('data-caption', link.text);
        link.addEventListener('click', click_OpenThumbnail);

        // create thumbnail
        const ext = src.split(".").pop();
        const mime = mimeTypes[ext];
        if(/^image\//.test(mime)){
            let img = document.createElement("img");
            img.src = thumb;
            img.alt = link.text;
            img.style = "max-width: 100px; max-height: 100px;";
            img.setAttribute('data-dp', src);
            img.setAttribute('data-caption', link.text);
            img.addEventListener('click', click_OpenThumbnail);
            link.insertBefore(img, link.firstChild);
        }
        else if(/^video\//.test(mime)){
            let video = document.createElement("video");
            video.src = src;
            video.style = "max-width: 100px; max-height: 100px;";
            video.setAttribute("preload", "metadata");
            video.setAttribute('data-dp', src);
            video.setAttribute('data-caption', link.text);
            video.addEventListener('click', click_OpenThumbnail);
            link.insertBefore(video, link.firstChild);
        }
    })
});
</script>
```

## 実行結果

![Redmineファイルプレビュー機能の強化.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85304/b75445d1-6065-ba79-b9ae-20f600e6eca5.gif)

## メリット

* Redmineサーバーに入る必要がないため、サーバーのログイン権限が無い人でも組込みや更新ができる。
* GitやSVNの運用フローや更新の仕組みを考える必要が無いため手頃に実現できる。

## デメリット

### OSSのソースコードは上書き更新できない

Redmineの添付ファイルは上書きできないためJavaScriptのOSS更新時には *.js の上げ直しと、View Customize側での\<script\>タグのsrc属性の更新が必要になる。頻繁に更新されるものには不向き。

### ページの読み込み負荷が上がる

**Redmineの添付ファイルはブラウザキャッシュされない設定になっているらしく、リクエスト事にファイルの通信が発生してしまう**。

特に**従量課金のクラウドサーバーやパフォーマンスにシビアな大規模Redmineでは、巨大なJavaScriptや補助のデータファイルを大量に組み込みたい場合に問題になる可能性がある**。

そういったケースではこの添付ファイルを利用した方法は使わずに、ブラウザがファイルをキャッシュするようにCDNにファイルを置いたり、ファイルをGit等で管理して更新はJenkins等で自動検知してRedmineの /public ディレクトリ下へ展開してそのURLを組み込む等の仕組みが必要になる。