Javascript でファイルのダウンロード後に処理を実行する

ページ作成日 :

動作検証環境

Visual Studio
  • Visual Studio 2022
ASP.NET Core
  • 8.0 (Razpr Pages, MVC)
Web ブラウザ
  • Edge 119

動作環境

Visual Studio (サーバープログラムも含む場合)
  • Visual Studio 2022
ASP.NET Core (サーバープログラムも含む場合)
  • 8.0 (Razpr Pages, MVC)
Web ブラウザ
  • Edge
  • Google Chrome
  • ほかのブラウザ (すべて確認したわけではありません)

(※本 Tips の主体はクライアント側の処理なのでサーバー側はなんでもよいです)

はじめに

本 Tips の主体はクライアントプログラム (Javascript) であり、サーバープログラムはファイルをダウンロードさせるためだけにあります。 そのためファイルをダウンロードさせることができるのであればサーバープログラムは何でも構いません。

サーバーにアクセスされたときにファイルをダウンロードさせる

サンプルではリクエストを受けたときにファイルをダウンロードさせるようにしています。 その際ダウンロードの開始と終了が分かりやすいようにわざとサーバーで一定時間待機するようにしています。

以下は Razor Pages と MVC のサーバー側の処理です。Web API などほかのプロジェクトで実装してもかまいません。

Razor Pages : Pages/Index.cshtml.cs

// 省略

namespace DownloadCompleteNotifyRazorPages.Pages
{
  public class IndexModel : PageModel
  {
    // 省略

    /// <summary>時間をかけてファイルをダウンロードします。</summary>
    public async Task<IActionResult> OnGetDownload()
    {
      // ダウンロード開始と完了を明確にする目的で待機を入れる
      await Task.Delay(5000);

      // 適当にファイルを作って返す
      var fileSize = 10_000_000;
      var sb = new System.Text.StringBuilder(fileSize);
      for (int i = 0; i < fileSize; i++) sb.Append('a');
      using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(sb.ToString()));
      return File(stream.ToArray(), "text/plain", $"ダウンロード.txt");
    }
  }
}

MVC : Coletollers/HomeController.cs

// 省略

namespace DownloadCompleteNotifyMvc.Controllers
{
  public class HomeController : Controller
  {
    // 省略

    /// <summary>時間をかけてファイルをダウンロードします。</summary>
    public async Task<IActionResult> Download()
    {
      // ダウンロード開始と完了を明確にする目的で待機を入れる
      await Task.Delay(5000);

      // 適当にファイルを作って返す
      var fileSize = 10_000_000;
      var sb = new System.Text.StringBuilder(fileSize);
      for (int i = 0; i < fileSize; i++) sb.Append('a');
      using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(sb.ToString()));
      return File(stream.ToArray(), "text/plain", $"ダウンロード.txt");
    }
  }
}

クライアント処理

通常、ファイルのダウンロードは Web ブラウザで指定 URL にアクセスしてダウンロードを行いますが、 ダウンロードの完了後になんらかの処理をさせたいのであればダウンロードは Javascript によって実行させます。

やり方はいろいろありますが、今回は非同期処理の原点でもある XMLHttpRequest を使用します。 jQuery の ajax 関数や Fetch API を使用しても構いません。

a タグの href にダウンロード URL を記述し iddownload を設定したものをクリックしたときに処理を実行させています。

$('#download').click(function (e) {
  console.log('ダウンロードを開始します。');
  e.preventDefault();  // href による画面遷移を抑止
  let url = $(e.target).attr('href');  // href からダウンロード URL 取得

  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);   // ダウンロード先 URL 設定
  xhr.responseType = 'blob';   // バイナリデータ取得であることを指示

  // ダウンロード完了後の処理を設定。この時点ではデータは取得してクライアントにありますが保存はしていません。
  xhr.onload = function (oEvent) {
    if (xhr.status !== 200) {
      console.log(`データの取得に失敗しました。(status=${xhr.status})`);
    } else {
      // 取得したデータ
      let blob = xhr.response;

      // レスポンスヘッダーからサーバーから送られてきたファイル名を取得する
      let fileName = '';
      let disposition = xhr.getResponseHeader('Content-Disposition');
      if (disposition && disposition.indexOf('attachment') !== -1) {
        let filenameRegex = /filename[^;=\n]=((['"]).*?\2|[^;\n]*)/;
        let matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) {
          // 「filename*=UTF-8''%E3%83%87%E3%83%BC%E3%82%BF.txt;」からファイル名を取得
          fileName = decodeURI(matches[1].replace(/['"]/g, '').replace('utf-8', '').replace('UTF-8', ''));
        }
      }
      if (fileName === '') {
        // ファイル名を取得できなかったら念のため URL をファイル名とする
        let fileName = url.match('.+/(.+?)([\?#;].*)?$')[1];
      }

      // Blob オブジェクトを指す URL オブジェクトを作る
      let objectURL = window.URL.createObjectURL(blob);
      // リンク(<a>要素)を生成し、JavaScript からクリックする
      let link = document.createElement('a');
      document.body.appendChild(link);
      link.href = objectURL;
      link.download = fileName;   // download を指定するとブラウザで開くことなくダウンロードになる
      link.click();
      document.body.removeChild(link);

      console.log('ダウンロードを完了しました。');
    }
  };

  // リクエスト開始
  xhr.send();
});

普通に a 要素をクリックさせたままにすると既定のダウンロード処理が動いてしまうので e.preventDefault(); を呼んで既定の処理を無効化しています。

href からダウンロード URL を取得し XMLHttpRequestGET で取得するように設定します。

XMLHttpRequest.responseTypeblob を設定することによりバイナリデータのダウンロードを行うように指示します。

XMLHttpRequest.onload イベントにはデータのダウンロードが完了したあとの処理を記述できるので、ここにダウンロード完了後の処理を書くことになります。 あくまでの「データがクライアントにダウンロードされた」だけですので、実際にローカルにファイルは保存されません。 ローカルにファイルを保存するには疑似的にデータを含んだ a タグを追加してクリック動作させることによってあたかも普通にリンクをクリックしてファイルを保存したかのような動作をさせることができます。 その際のデータは window.URL.createObjectURL 関数で URL に変換することができるので href にセットして動作させることができるようになります。

保存するファイル名はサーバーからのレスポンスに含まれていればレスポンスヘッダーの Content-Disposition から取得することができます。 若干取得が複雑なのでコードをそのまま流用すればよいかと思います。 サーバーでファイル名が指定されていない場合はしょうがないのでデフォルトのファイル名をセットするか URL のパスの最後をファイル名にする形になるかと思います。

今回ファイルを保存した後にコンソールに表示するだけにしているので、この箇所を任意のコードに書き換えれば目的の動作を実行できると思います。

データダウンロードの処理を記述したら最後に XMLHttpRequest.send 関数を呼んで実際にリクエストを開始させます。

ダウンロード実行リンク

サンプルコードでは直接ファイルをダウンロードするパターンと Javascript でダウンロードさせるパターンのリンクを用意しています。 a タグの href に直接 URL を記述してもよいですが、ASP.NET Core なら以下のように書いておけば自動的に href に URL が展開されます。

Razor Pages : Pages/Index.cshtml

<ul>
  <li><a asp-page-handler="Download">ダウンロード (Javascript なし)</a></li>
  <li><a asp-page-handler="Download" id="download">ダウンロード (Javascript あり)</a></li>
</ul>

MVC : Views/Home/Index.cshtml

<ul>
  <li><a asp-controller="Home" asp-action="Download">ダウンロード (Javascript なし)</a></li>
  <li><a asp-controller="Home" asp-action="Download" id="download">ダウンロード (Javascript あり)</a></li>
</ul>