2026年02月22日

Vue.js 開発:wxWebView を使ったバックエンド(Mac版)

 「わたしの家計簿」開発の続きです。ブラウザに依存するのはどうも使い勝手が悪いので、WebView を使える環境ではそれを使って動作するようにしたいところです。

 まず Mac 版です。ビルドは普通に wxWebView を使えばできます。macOS 11 より前では、未対応の機能がいろいろあるので(window.print() が実装されていないなど)、macOS 11 以上であることを実行時に確認します。

static bool
shouldUseWebView(void)
{
  int major;
  wxGetOsVersion(&major, NULL, NULL);
  if (major >= 11)
    return true;
  else
    return false;
}

 実行してみると、ファイルの書き出しができない、という問題点が判明しました。実は、WKWebView (macOS の WebKit を使った WebView 実装)では、ダウンロードは 11.3 以降の機能であり、しかも複雑なようです。

Stack Overflow

How to download files in wkwebview

Anybody please tell me how to download files in iOS wkwebview. I have created an iOS webview app. In the page I have loaded It has several download options but when I click download nothing happe...

 ファイル保存ダイアログでファイルパスを受け取って、それに書き出しするのがよさそうです。しかし、ファイル保存ダイアログは TypeScript 側では出せないので、バックエンド側で出さないといけません。処理の流れはこのようになります。

  1. TypeScript がバックエンドにファイル保存ダイアログの要求を POST する。
  2. バックエンドが要求を受け取る。HTTPサーバはサブスレッドで動いているため、メインスレッドにイベントを投げて、ファイル保存ダイアログを要求する。
  3. バックエンドのメインスレッドがイベントを受け取り、ファイル保存ダイアログを出す。
  4. ファイル保存ダイアログが閉じられたら、指定されたファイルパス、または空文字列(ダイアログがキャンセルされた場合)をキューに入れて、サブスレッドに知らせる。
  5. サブスレッドはキューにデータが入ったことを検出して、TypeScript にレスポンスを返す。
  6. TypeScript がデータを受け取って、空文字列でなければ、ファイル保存を行う。

 ここで問題になるのは、3 と 4 の間にどれだけ時間がかかるか、予測できないことです。TypeScript 側で普通の HTTP レスポンスを待っていると、タイムアウトになってしまいます。そこで、Server-sent Events を使うことにしました。

 TypeScript 側は下のように実装しています。fetchVueRunner() 関数は、HTTP サーバに POST リクエストを投げるものです。戻り値の res は、普通は HTTP レスポンスなのですが、ここでは Server-sent Events のイベントストリームです。この関数 (vSaveDialog) は Promise を返します。この Promise は、イベントストリームからのイベントを読み出して、ファイルパス(または空文字列)をデータとして検出したら resolve します。

export async function vSaveDialog(options?: object): Promise<string> {
  const res = await fetchVueRunner({ cmd: "saveDialog", options: options });
  //  res は event-stream
  const reader = res.body?.getReader();
  const decoder = new TextDecoder();
  return new Promise<string>(async (resolve, reject) => {
    if (reader !== undefined) {
      while (true) {
        const {done, value} = await reader.read();
        console.log(`vSaveDialog: done=${done}, value=${value}\n`);
        if (done)
          break;
        if (!value)
          continue;
        const lines = decoder.decode(value);
        let [ type, text ] = lines.split(": ");
        if (type === "data") {
          //  text may be an empty string (the save dialog was canceled)
          text = text.trim();
          console.log(`vSaveDialog: resolve(${text})\n`);
          resolve(text);
          return;
        }
      }
    }
    reject();
    return;
  });
}

 バックエンド側の、HTTP サーバの処理です。独自の wxCommandEvent を作成して、メインスレッドに投げます。このとき、POST で渡されたデータ(json 型の j)にServer-sent Events のコネクションを指定する値(c->id)を追加して、イベント内のデータとして渡します。

  if (cmd == "saveDialog") {
    j["connection_id"] = c->id;
    wxCommandEvent *anEvent = new wxCommandEvent(MyEvent);
    anEvent->SetClientData(strdup(j.dump().c_str()));
    wxGetApp().QueueEvent(anEvent);
    mg_printf(c, sSSEResponse);
    c->is_resp = 0;
    return;  //  Early return: no standard http reply
  }

 イベントハンドラはこのような実装です(一部省略)。sSSEResults は、std::queue<std::pair<unsigned long, std::string>> 型の static 変数です。

  void *d = event.GetClientData();
  json j = json::parse((const char *)d);
  free(d);  //  d must have been allocated by strdup()
  std::string cmd = j["cmd"];
  unsigned long id = j["connection_id"];
  if (cmd == "saveDialog") {
    /* ... */
    wxFileDialog dialog(NULL,
                        wxString::FromUTF8(title.c_str()),
                        wxString::FromUTF8(dir.c_str()),
                        wxString::FromUTF8(defaultPath.c_str()),
                        wxString::FromUTF8(wildcard.c_str()),
                        wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxFD_CHANGE_DIR);
    if (dialog.ShowModal() == wxID_OK) {
      result = (const char *)(dialog.GetPath().mb_str(wxConvFile));
    }
    {  //  Push the result to the queue
      std::lock_guard<std::mutex> lock(sMutex);
      sSSEResults.push(std::make_pair(id, result));
    }
  }

 HTTP サーバのスレッドでは、入力待ちのループの中に下のような処理を入れて、キューにデータが入っていたらそれを送ります。データを送ったら、Server-sent Events のコネクションを切断します。

      std::pair<unsigned long, std::string> pair;
      {
        std::lock_guard<std::mutex> lock(sMutex);
        if (sSSEResults.empty())
          break;
        pair = std::move(sSSEResults.front());
        sSSEResults.pop();
      }
      mg_connection *c = mgr.conns;
      while (c != NULL) {
        if (c->id == pair.first)
          break;
        c = c->next;
      }
      if (c != NULL) {
        mg_printf(c, "data: %s\n\n", pair.second.c_str());
        c->is_draining = 1;
      }

 ちゃんと動作しました!

20260222-1.jpg

 長くなったので、Windows 版のビルドは後日書きます。

Posted at 2026年02月22日 23:22:49
email.png