神楽坂のシステムやさん通信

Teams タブアプリでビルド後に404が出る?HTMLキャッシュが原因かも

うぱるぱ tech
Teams タブアプリでビルド後に404が出る?HTMLキャッシュが原因かも

Teams タブアプリでビルド後に404が出る?HTMLキャッシュが原因かも

はじめに

Microsoft Teams タブアプリを開発していると、ローカル開発時に困る問題があります。

フロントエンドのコードを変更してビルドすると、Vite/Rollup がファイル名にハッシュを付けます(例: TeamsInitializer.bBRVpIft.js)。しかし、ブラウザ(Teams の WebView)が HTML をキャッシュしていると、古い HTML が古いハッシュ付きファイル名を参照し続けて 404 エラー が発生します。

この記事では、Teams SDK v2 のローカルサーバーの内部構造を調査して見つけた解決方法を紹介します。

一般的な解決策とその問題点

方法1: ファイル名からハッシュを除去する

// astro.config.mjs または vite.config.js
export default {
  vite: {
    build: {
      rollupOptions: {
        output: {
          entryFileNames: 'assets/[name].js',
          chunkFileNames: 'assets/[name].js',
          assetFileNames: 'assets/[name].[ext]',
        },
      },
    },
  },
};

問題点: ハッシュを外すと、ブラウザがファイルをキャッシュしてしまい、コード更新が反映されないことがあります。キャッシュバスティングの仕組みを失うことになります。

方法2: 開発サーバー(HMR)を使う

Vite や Astro の開発サーバーを使えば HMR(Hot Module Replacement)でキャッシュ問題は起きません。

問題点: Teams アプリは Teams クライアント(WebView)から特定のポートにアクセスする構成のため、開発サーバーの別ポートを直接使うのは難しい場合があります。

Teams SDK v2 の内部構造を調査する

Teams SDK v2 の @microsoft/teams.apps パッケージを調べてみると、HttpPlugin が内部で Express を使っていることがわかりました。

// node_modules/@microsoft/teams.apps/dist/plugins/http/plugin.js より抜粋
const express_1 = __importDefault(require("express"));

let HttpPlugin = class HttpPlugin {
    constructor(server, options) {
        this.express = (0, express_1.default)();
        // ...
        this.use = this.express.use.bind(this.express);  // ← use が公開されている!
    }

    static(path, dist) {
        this.express.use(path, express_1.default.static(dist));
        return this;
    }
}

重要な発見:

  1. HttpPlugin は内部で Express インスタンスを持っている
  2. use() メソッドが公開されており、Express の app.use() にバインドされている
  3. つまり、任意の Express ミドルウェアを追加できる

解決策: ミドルウェアでキャッシュ制御ヘッダーを設定

この発見を活かして、ローカル開発時のみ HTML ファイルのキャッシュを無効化するミドルウェアを追加します。

// src/index.ts
import fs from "fs";
import https from "https";
import path from "path";

import { App, HttpPlugin, IPlugin } from "@microsoft/teams.apps";
import { ConsoleLogger } from "@microsoft/teams.common/logging";

const sslOptions = {
  key: process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined,
  cert: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined,
};

const httpPlugin = new HttpPlugin(
  sslOptions.cert && sslOptions.key ? https.createServer(sslOptions) : undefined
);

// ローカル開発時のみ、HTMLファイルのキャッシュを無効化
if (!process.env.RUNNING_ON_AZURE) {
  httpPlugin.use("/tabs", (req, res, next) => {
    // HTMLファイルとindex.htmlへのリクエストはキャッシュを無効化
    if (req.path.endsWith(".html") || req.path === "/" || !req.path.includes(".")) {
      res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      res.setHeader("Pragma", "no-cache");
      res.setHeader("Expires", "0");
    }
    next();
  });
}

const plugins: IPlugin[] = [httpPlugin];
const app = new App({
  logger: new ConsoleLogger("tab", { level: "debug" }),
  plugins: plugins,
});

app.tab("home", path.join(__dirname, "./client"));

(async () => {
  await app.start(+(process.env.PORT || 3978));
})();

ポイント解説

  1. 環境変数で制御: RUNNING_ON_AZURE 環境変数がない場合(ローカル開発時)のみミドルウェアを追加
  2. パス判定: .html で終わるパス、ルートパス、拡張子がないパス(SPA のルーティング)をHTML リクエストとみなす
  3. キャッシュ無効化ヘッダー: Cache-Control, Pragma, Expires の3つを設定して確実にキャッシュを無効化

この方法のメリット

  1. ハッシュ付きファイル名を維持: JS/CSS ファイルはハッシュ付きのままなので、本番環境でのキャッシュ効率は変わらない
  2. 本番環境に影響なし: 環境変数で制御するため、Azure デプロイ時にはミドルウェアが追加されない
  3. 他のミドルウェアも追加可能: 同じ手法でロギング、認証、カスタムヘッダーなども追加できる

応用: 他のミドルウェアを追加する

この手法を使えば、様々なミドルウェアを Teams SDK に追加できます。

// リクエストログを追加
httpPlugin.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// カスタムヘッダーを追加
httpPlugin.use((req, res, next) => {
  res.setHeader("X-Custom-Header", "my-value");
  next();
});

// 特定パスに認証を追加
httpPlugin.use("/api/private", (req, res, next) => {
  if (!req.headers.authorization) {
    res.status(401).send("Unauthorized");
    return;
  }
  next();
});

注意点

  • この方法は Teams SDK v2 (@microsoft/teams.apps v2.x) の内部実装に依存しています
  • 将来のバージョンで API が変わる可能性があります
  • 公式ドキュメントには記載されていない使い方です

まとめ

Teams SDK v2 の HttpPlugin が内部で Express を使っており、use() メソッドが公開されていることを発見しました。これを活用することで:

  • ローカル開発時の HTML キャッシュ問題を解決
  • ハッシュ付きファイル名によるキャッシュ無効化の仕組みを維持
  • 本番環境に影響を与えずに開発体験を改善

Teams SDK v2 を使っている方で同じ問題に悩んでいる方の参考になれば幸いです。