VisualforceでContent-Type=text/plain;charset=Shift-JISで出力する方法

## 概要
全銀データ形式でデータを出力する必要があり、Visualforceで実装した。
MIMEタイプが`text/plain`形式（拡張子が「.txt」）で文字コードをShift-JISで出力する必要があった。
MIMEタイプが`csv`形式で文字コードをShift-JISで出力するケースはよくあるが、plainテキスト形式で出力するのはあまりないと思われる。
今回色々試行錯誤して一つの実現方法にたどり着いたので、こちらに備忘として記録する。

## やりたいこと
- Salesforceから下記の出力形式を設定し、全銀データ形式でデータを出力する
- 出力形式
  - [MIMEタイプ](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types)：text/plain
  - 文字コード：Shift-JIS

##  課題
- plainテキストはcsvやExcelのように`<apex:outputText>`タグでダウンロードできない。（普通に出力内容が画面に表示されてしまう）
- LWCやVisualforce内でJavascriptを使って`<a>`タグ要素を用いてダウンロードリンクを作って、普通にダウンロードすると文字コードはUTF-8などのUnicodeで扱われてしまう([参考](https://262.ecma-international.org/10.0/#sec-ecmascript-language-types-string-type))

## 解決方法
- 外部ライブラリを使用する
- 使用するライブラリ
  - `encoding.js`
 
https://github.com/polygonplanet/encoding.js/blob/master

- ライブラリのREADME_ja.mdから簡単な説明を引用
> encoding.js は、文字コードの変換や判定をする JavaScript ライブラリです。
Shift_JIS や EUC-JP、ISO-2022-JP など日本語の文字コードや、 UTF-8、UTF-16 などの Unicode に対応しています。

## 実装
- LWCからダウンロードボタン押下時にテキストファイルを出力する

```javascript:lwc
const windowFeatures = 'left=100,top=100,width=320,height=320'
// ダウンロード後にページを削除するためポップアップを使用
window.open('/apex/ExportBankData', 'データ出力', windowFeatures)
```

- window.open関数によって開かれたVisualforceページが読み込まれた際にApex=>JSで定義した関数を実行させたいので、addEventListenerの[DOMContentLoadedイベント](https://developer.mozilla.org/ja/docs/Web/API/Document/DOMContentLoaded_event)にハンドラーを設定
- やっていること
  - 出力する文字列を文字コード(Unicode)に変換
  - Unicode=>Shift-JISに変換
  - 文字コードをバイト配列に変換してblobデータを作成
  - aタグ要素を作成して、ダウンロード
  - ポップアップした新規ウィンドウを閉じる
  

```html:ExportBankData.page
<apex:page controller="ExportBankDataController" cache="false" readOnly="true">
    <!-- cdnでライブラリをインポートする -->
    <apex:includeScript value="https://unpkg.com/encoding-japanese@2.1.0/encoding.min.js" />
 
    <div style="display: none;">
        <apex:form>
            <apex:actionFunction name="initAction" action="{!doExport}" oncomplete="doExport('{!fileName}', '{!fileData}', '{!mimeType}');"
            />
        </apex:form>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            initAction(); // actionFunctionを呼び出す
        });

        function doExport(fileName, fileData, mimeType) {
            // データがないなら処理を実行しない
            if (!fileData) {
                return;
            }

            // 文字列から文字コード値の配列に変換
            const unicodeArray = Encoding.stringToCode(fileData);

            // Shift-JISのバイト配列にエンコード
            const sjisArray = Encoding.convert(unicodeArray, 'SJIS', 'UNICODE');

            // バイト配列からBlobオブジェクトを生成
            // Uint8Array：0~255の値を格納するTypedArrayオブジェクトの一つ
            const blobData = new Blob([new Uint8Array(sjisArray)], { type: mimeType });

            // ダウンロードを実行
            const link = document.createElement("a");
            link.download = fileName;
            link.href = window.URL.createObjectURL(blobData);
            link.click();
            URL.revokeObjectURL(link.href);
            window.close();
        }
    </script>
</apex:page>
```

- 出力するデータやファイル名、Mimeタイプなどを設定する
```Apex:ExportBankDataController
public with sharing class ExportBankDataController {
    public String fileName { get; set; }

    transient public String fileData { get; set; }

    public String mimeType { get; set; }

    public ExportBankDataController() {
    }

    public void doExport() {
        this.fileData = 'ホゲホゲ';
        this.fileName = 'hogehoge.txt';
        this.mimeType = 'text/plain';
    }
}

```

## 動作確認
- ダウンロードボタンを押下して`.txt`でファイルがダウンロードされる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/644299e2-815e-2fd5-7ea1-73d1c49bb316.png)

- 文字コードの確認は、encode.jsが用意しているファイルから[文字コードを判定するツール](https://polygonplanet.github.io/encoding.js/tests/detect-file-encoding.html)を使用
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/a4ea91cc-42bc-21ea-f5c1-3ceff48e6f24.png)


- 一応VSCodeでも確認すると、Shift-JIS形式でエンコードした際に文字化けしていない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531653/83afba56-c1f4-b2db-8b70-2c0420286f8b.png)


## まとめ
今回はユースケースとしては少ないと思われるため、参考記事があまりない中で辿り着いた解決方法を記載しました。なので、おそらく他にもっとより良いやり方もあるのでは？と思っています。
私と同様の要件で出力する必要などがあり、困っている方の一助になれば幸いです。