SvelteKitのMSW

SvelteKitでローカル開発のためにMSWを実装する方法

SvelteKitでMSWを使用する

これは、SvelteKitアプリケーションに「MockServiceWorker」ライブラリであるcaleldMSWを実装する方法を示すチュートリアルです。このガイドでは、Jestを使用してMSWを設定する方法ではなく、ライブ開発中にMSWを使用する方法を示していることに注意してください。

完全な実装は、spikze.clubのリポジトリにあり、ページの最後にある補遺にもリンクされています。

このガイドが最初は圧倒されるように見えても心配しないでください。変更は非常に簡単です。 SvelteKitでMSWを使用して、ローカル開発中にリクエストをモックするには、もう少し手間がかかります。

依存関係のインストール

まず、必要なすべての依存関係をインストールしましょう。

npm i -D msw ts-node concurrently

サーバーモックも起動するために「ts-node」を使用しています。先に進む前に、SvelteKitプロジェクトのルートで次のコマンドを呼び出してください。

npx msw init ./static

これにより、MSWによるデフォルトのServiceWorkerハンドラーを使用してJavascriptファイルが生成されます。 SvelteKitアプリには、パブリックディレクトリと同等の「静的」を使用しています。最後に、package.json-fileを更新して、MSWがファイルを探す場所を認識できるようにします。

{
  "scripts": ...
  ...
  "msw": {
    "workerDirectory": "static"
  }
}

便利なスクリプトとして、次のコマンドを「スクリプト」に追加しました。これは、クライアントとサーバー(サーバー側のモックの場合)の両方を介してMSWを起動する場合に必要な構成です。

{
  "scripts": {
    "dev": "svelte-kit dev",
    "dev:msw-server": "concurrently \"cd msw && ts-node server.ts\" \"npm run dev\"",
    ...
  },
}

MSWモジュールの準備

実際のコードを書き始める前に、SvelteKitのconfigとTypescript-configでMSWの新しい作業ディレクトリを定義する必要があります。 Typescriptファイルから始めましょう。

{
  "compilerOptions": {
    ...,
    "paths": {
      "$lib": ["src/lib"],
      "$lib/*": ["src/lib/*"],
      "$msw": ["src/msw"],
      "$msw/*": ["src/msw/*"]
    }
  }
}

次に、SvelteKit-configを更新しましょう。

import adapter from "@sveltejs/adapter-auto";
import preprocess from "svelte-preprocess";
import path from "path";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),
  kit: {
    adapter: adapter(),
    vite: {
      ...,
      resolve: {
        alias: {
          $msw: path.resolve("./src/msw"),
          $lib: path.resolve("./src/lib")
        }
      }
    }
  }

モックとハンドラーの追加

了解しました。ガイドの最初の部分を完了しました。次に、MSWを使用するときに実行される実際のコードを追加します。これは最小限の例であることに注意してください。したがって、すべてを機能させるために必要なものを追加しましたが、それ以上は追加しませんでした。

SvelteKit-configの変更からお察しのとおり、すべてのコードは「src」に直接存在する「msw」という新しいディレクトリに配置されます。

|- app/
|-- src/
|--- pages/
|--- msw/
|---- fixtures/
...

次に追加するモジュールのコードは次のとおりです。それらを簡単にコピーして貼り付けることができるはずです。ファイル名+ディレクトリパスは、上部の各ブロックにコメントとして書き留められます。

//
// app/src/msw/handlers.server.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

export const handlers = [
  // Here, you can mock absolute URL requests,
  // e.g. to a database. For the current implementation,
  // no data is mocked in this place.
  //
  // Note: This is also the place to mock absolute
  // SSR-imports. Everything in 'handlers.workers.ts'
  // is mocked client-side.
];
//
// app/src/msw/handlers.worker.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

// Mock relative URLs that map to your
// routes' data endpoints. This mock only
// happens for client-side requests.
//
// Note that if you use shadow endpoints, this still works
// as the endpoint gets created by SvelteKit.
export const handlers = [
  rest.get("/msw/demo/__data.json", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ values }));
  })
];
//
// app/src/msw/server.ts
//

import { setupServer } from "msw/node";
import { handlers } from "./handlers.server";

export const server = setupServer(...handlers);
//
// app/src/msw/worker.ts 
//

import { setupWorker } from "msw";
import { handlers } from "./handlers.worker";

export const worker = setupWorker(...handlers);
//
// app/src/msw/fixtures/msw-demo.ts
//

export const values = ["foo", "bar"];
//
// app/src/msw/index.ts 
//

import { browser, dev } from "$app/env";

/**
 * Lazy-inject the MSW handler
 * so that no errors happen during
 * build/runtime due to invalid
 * imports from server/client.
 */
export async function inject() {
  if (dev && browser) {
    const { worker } = await import("../msw/worker");
    // For live development, I disabled all warnings
    // for requests that are not mocked. Change how
    // you think it best fits your project.
    return worker.start({ onUnhandledRequest: "bypass" }).catch(console.warn);
  }
  if (dev && !browser) {
    const { server } = await import("../msw/server");
    // Same as in worker-mock above.
    return server.listen({ onUnhandledRequest: "bypass" });
  }
}

MSWの開始

ライブ開発中にMSWを使用する場合の1つの課題は、競合状態がないことを確認する必要があることです。実行順序を定義する必要があります。そうしないと、すべての要求がすでに行われた後、MSWのサービスワーカーがアクティブになる可能性があります。

この目標を達成するために、ルートレイアウトファイルを変更します。このファイルはすべてのページにマウントされるため、MSWが終了するまでそれ以降のすべての実行をブロックするのに適した場所です。

<script>
  import "../app.css";
  import { dev } from "$app/env";
  
  // Loaded from .env.local, guide covers this
  // step in a moment.
  const isMswEnabled = dev && import.meta.env.VITE_MSW_ENABLED === "true";
  // Flag to defer rendering of components
  // until certain criteria are met on dev,
  // e.g. MSW init.
  let isReady = !isMswEnabled;
  
  if (isMswEnabled) {
    import("$msw")
      .then((res) => res.inject())
      .then(() => (isReady = true));
  }
</script>

{#if isReady}
  <slot />
{/if}

デモページの追加

次のコードスニペットは、このチュートリアルで使用されている2つのデモページと1つのエンドポイントのコンテンツを示しています。

<!-- app/src/routes/msw/index.svelte -->

<script>
  import DisplayProse from "$lib/display/views/DisplayProse.svelte";
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
</script>

<PageLayout>
  <SectionLayout withContentTopSpacing withHeaderSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Landing Page</ProminentDisplayTitle>
    <DisplayProse>
      <p>
        This compoonent has no purpose other than being part of an MSW demo implementation. See <a
          href="https://flaming.codes"
          alt="Link to flaming.codes with blog posts">flaming.codes</a
        > for more details.
      </p>
      <p>
        This page doesn't fetch any data for shows how client-side fetches are mocked with MSW in
        SvelteKit.
      </p>
      <p>Simply click the link below to access the page with data.</p>
      <p>
        <a href="/msw/demo" alt="Link to demo page with data">msw/demo</a>
      </p>
    </DisplayProse>
  </SectionLayout>
</PageLayout>
<!-- app/src/routes/msw/demo.svelte -->

<script lang="ts">
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
  export let values: string[];
</script>

<PageLayout>
  <SectionLayout withHeaderSpacing withContentTopSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Demo</ProminentDisplayTitle>
    <p>
      This compoonent has no purpose other than being part of an MSW demo implementation. See <a
        href="https://flaming.codes"
        alt="Link to flaming.codes with blog posts">flaming.codes</a
      > for more details.
    </p>
    <p>
      Values: {values}
    </p>
  </SectionLayout>
</PageLayout>
//
// app/src/routes/msw/demo.ts
//

import type { RequestHandler } from "@sveltejs/kit";

// Just for demo purposes.
export const get: RequestHandler = async () => ({
  status: 200,
  body: {
    values: ["production", "data", "not", "msw"]
  }
});

環境変数の追加

ほぼ完了です。不足しているのは、「VITE_MSW_ENABLED」フラグを環境に追加することです。フラグを保持するファイルとして「.env.local」を使用しています。これはViteによって消費され、gitに追加されないためです。

VITE_MSW_ENABLED=true

アプリケーションの実行

了解しました。これですべてを使用できるようになります。クライアントでモックを有効にするには、フラグが設定されていることを確認してください。次に、一般的な「dev」コマンドを実行して、クライアント側の要求をモックします。

npm run dev

サーバー側のリクエストもモックするには、最初に追加した新しいコマンドを実行するだけです。

npm run dev:msw-server

これで、ローカル開発中にエンドポイントをモックする準備が整いました。コード例に示されているように、このデモページには、すべてが準備されていますが、サーバー側のモックは含まれていません。つまり、ローカルでは、リンクをクリックしてデモサイトのインデックスページから実際のデモページに移動した場合にのみ、MSW呼び出しがトリガーされます。

サーバーリクエストをシミュレートするために、データベースからデータをフェッチする、より複雑なエンドポイントがあります。これらのリクエストは、サーバーハンドラーでモックすることができます。サーバーモックの場合、絶対URLのみが有効であることに注意してください。