オフラインの表示を出す、同期を止めて再開する、WebSocket や SSE を繋ぎ直す、といった処理のきっかけとして接続状態を監視します。手軽なのは Webview の navigator.onLine ですが、これは「ネットワークに繋がっているか」を見るだけで、インターネットやアプリのサーバーに届くかまでは分かりません。その限界と、実際にリクエストを送って確かめる方法を JS と Rust の両方で示します。
前提条件
navigator.onLine と online / offline イベントは追加設定なしで使えます。届くかどうかを JS から確かめる場合は http プラグイン(npm run tauri add http)を使い、確認先の URL を許可します。許可の書き方は HTTP GET リクエストを送る(Rust経由) を参照してください。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "http:default",
"allow": [{ "url": "https://www.gstatic.com/generate_204" }]
}
]
}
Rust で確かめる場合は、http プラグインの代わりに src-tauri で cargo add reqwest@0.12 と cargo add tokio --features time を実行します。
1. フロントエンドから実装する (TypeScript)
navigator.onLine の限界
navigator.onLine が false なら、ネットワークアダプターがどこにも繋がっていないので、オフラインと判断してかまいません。一方 true は次のような状態でも返ります。
- Wi-Fi には繋がっているが、ルーターの先の回線が切れている
- ホテルや駅の Wi-Fi で、ログイン画面(キャプティブポータル)を通っていない
- VPN や仮想化ソフトの仮想ネットワークアダプターが「接続中」のまま残っている
- ファイアウォールやプロキシで、アプリのサーバーへの通信だけが止められている
つまり false は信じてよく、true は疑うものです。online / offline イベントも、この判定が変わったときに発生します。
// ネットワークアダプターの状態の変化を受け取る(プラグイン・権限は不要)
export function watchNavigatorOnline(render: (online: boolean) => void) {
const update = () => render(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
update(); // 起動時の状態も反映する
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}
実際に届くかを確かめる
確かめる相手は「インターネット」ではなく「アプリが使うサーバー」です。自分のサーバーに、本文なしで 204 を返すだけの URL(例: /healthz)を用意するのが確実です。以下の例では、同じ用途で公開されている https://www.gstatic.com/generate_204 を使います。状態コードが 204 かどうかまで見るのは、キャプティブポータルが 200 のログイン画面やリダイレクトを返すためで、リダイレクトを追わないよう maxRedirections: 0 も付けます。
標準の fetch で確かめると、CORS を許可していない URL では必ず失敗し、「常にオフライン」と誤判定します。http プラグインの fetch なら CORS の影響を受けません。
import { fetch } from '@tauri-apps/plugin-http';
// 自分のサーバーに用意した「204 を返すだけの URL」にするのが確実
const CHECK_URL = 'https://www.gstatic.com/generate_204';
export async function checkReachable(timeoutMs = 5000): Promise<boolean> {
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(CHECK_URL, {
signal: controller.signal,
connectTimeout: timeoutMs,
maxRedirections: 0, // ログイン画面へのリダイレクトを追わない
});
return res.status === 204; // 200 のログイン画面や 302 は「届いていない」扱い
} catch {
return false; // 名前解決の失敗・接続拒否・タイムアウト
} finally {
window.clearTimeout(timer);
}
}
2 つを組み合わせて監視する
offline イベントはすぐに反映し、online イベントでは確かめてから戻します。オンラインの間も定期的に確かめますが、スリープ復帰の直後などは一時的に失敗しやすいので、1 回の失敗ではオフラインにしません。
// (続き) navigator.onLine と組み合わせて監視する
export type Connectivity = 'online' | 'offline';
export function startMonitor(onChange: (state: Connectivity) => void, intervalMs = 60_000) {
let state: Connectivity | null = null;
let failures = 0;
let running = false;
let timer: number | undefined;
const set = (next: Connectivity) => {
if (next === state) return;
state = next;
onChange(next);
};
const probe = async () => {
if (running) return;
running = true;
window.clearTimeout(timer);
try {
if (!navigator.onLine) {
set('offline'); // false は信じてよい
} else if (await checkReachable()) {
failures = 0;
set('online');
} else if (++failures >= 2 || state === null) {
set('offline'); // 2 回続けて失敗したら切り替える(起動直後は 1 回で決める)
}
} finally {
running = false;
// オフライン中や失敗の直後は 10 秒後、オンライン中は intervalMs 後に確かめる
const wait = state === 'online' && failures === 0 ? intervalMs : 10_000;
timer = window.setTimeout(() => void probe(), wait);
}
};
window.addEventListener('offline', () => set('offline')); // 「切れた」はすぐ反映する
window.addEventListener('online', () => void probe()); // 「戻った」は確かめてから
void probe();
}
startMonitor((s) => {
document.body.classList.toggle('is-offline', s === 'offline');
console.log('connectivity:', s);
});
2. バックエンドから実装する (Rust)
ウィンドウが複数ある、ウィンドウを閉じてもトレイで動き続ける、といったアプリでは、確認を Rust にまとめ、結果をイベントで全ウィンドウに配ると重複しません。
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tauri::{AppHandle, Emitter, Manager, State};
// 自分のサーバーに用意した「204 を返すだけの URL」にするのが確実
const CHECK_URL: &str = "https://www.gstatic.com/generate_204";
struct Connectivity {
online: AtomicBool,
client: reqwest::Client,
}
impl Connectivity {
fn new() -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5)) // 応答まで最長 5 秒
.redirect(reqwest::redirect::Policy::none()) // ログイン画面へのリダイレクトを追わない
.build()
.expect("failed to build HTTP client");
Self { online: AtomicBool::new(true), client }
}
// 1 回の失敗ではオフラインにしない。2 秒おいてもう一度試す
async fn reachable(&self) -> bool {
for attempt in 0..2 {
if attempt > 0 {
tokio::time::sleep(Duration::from_secs(2)).await;
}
if let Ok(res) = self.client.get(CHECK_URL).send().await {
if res.status() == reqwest::StatusCode::NO_CONTENT {
return true;
}
}
}
false
}
}
/// 確かめて、状態が変わったときだけ全ウィンドウに通知する
async fn probe(app: &AppHandle) -> bool {
let state = app.state::<Connectivity>();
let online = state.reachable().await;
if state.online.swap(online, Ordering::Relaxed) != online {
let _ = app.emit("connectivity-changed", online);
}
online
}
#[tauri::command]
async fn check_connectivity(app: AppHandle) -> bool {
probe(&app).await
}
#[tauri::command]
fn is_online(state: State<'_, Connectivity>) -> bool {
state.online.load(Ordering::Relaxed)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init()) // 1 章の fetch を使う場合
.manage(Connectivity::new())
.setup(|app| {
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
loop {
let online = probe(&handle).await;
// オフライン中は 10 秒、オンライン中は 60 秒ごとに確かめる
tokio::time::sleep(Duration::from_secs(if online { 60 } else { 10 })).await;
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![check_connectivity, is_online])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
起動直後の emit は、ページが listen を登録する前に送られて取りこぼすことがあります。現在の値を返す is_online を用意し、listen の後に一度呼んで合わせます。listen の権限は core:default に含まれています。
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
export async function watchFromRust(render: (online: boolean) => void) {
const unlisten = await listen<boolean>('connectivity-changed', (e) => render(e.payload));
render(await invoke<boolean>('is_online')); // listen より前に送られた通知の分を取り戻す
window.addEventListener('offline', () => render(false)); // 「切れた」は待たずに反映
window.addEventListener('online', async () => {
render(await invoke<boolean>('check_connectivity')); // 「戻った」はすぐ確かめる
});
return unlisten;
}
動作確認
npm run tauri dev で起動すると、コンソールに connectivity: online と出ます。Wi-Fi を切ると、offline イベントが来ればすぐに、来なくても次の確認で connectivity: offline になり、戻すと確認が済んでから online に戻ります。「繋がっているのに届かない」状態は、Rust 版の CHECK_URL を応答しない宛先(例: http://10.255.255.1/)に変えると試せます。navigator.onLine は true のままですが、確認はタイムアウトしてオフラインと判定されます。
よくあるエラーと対処法
- いつもオフラインと判定される: 標準の
fetchで CORS を許可していない URL を確かめています。http プラグインのfetchか Rust で確かめます。 - 「url not allowed on the configured scope: https://www.gstatic.com/generate_204」: http プラグインの許可リストに確認先がありません。
- キャプティブポータルの Wi-Fi でもオンラインになる:
res.okで判定しています。ログイン画面は 200 を返すので、204 と一致するかで判定します。 - 確認に 20 秒以上かかる: タイムアウトを付けていません。TCP で
1.1.1.1:53などに繋いで確かめる方法も同じで、OS の既定では長く待たされます。また TCP が繋がってもキャプティブポータルやプロキシは見抜けないので、HTTP で確かめる方が確実です。 listenしても最初の状態が届かない: 登録前のemitを取りこぼしています。is_onlineで現在の値を取ります。
OS ごとの違いと注意点
navigator.onLineの判定: Webview のエンジンと OS のネットワーク状態から決まり、細かい判定は OS によって違います。どの OS でも「trueなら届く」とは考えず、確認と組み合わせます。- 確認の頻度: 数秒ごとに確かめると、ノート PC の電池や従量課金の回線を消費します。オンライン中は 1 分程度にし、実際の通信が失敗したときや
onlineイベントのときに追加で確かめます。 - 確認先の選び方: 社内ネットワーク専用のアプリなら、社内のサーバーを確認先にします。インターネットに出られなくても、アプリとしては使えるためです。
