3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【TypeScript】Vue 3 SFCでLazy LoadingのSuspenseを自前で作る

Last updated at Posted at 2022-02-18

背景

筆者は、React 18で本格的に導入されるデータ受信が終わるまで仮の部品(Loading...など)を表示する機能がとても待ち遠しいです。
仕事でVueを使っているので、Vueでも同じことができたら、と思いましたが、VueのSuspenseはまだ開発中でProductionの製品では使えません。
それと、データ受信の間もいいですが、部品のJavaScriptを読み込んでいる間(Lazy Loading)も、データ受信を待っている間も、Suspense部品を出したい!というのが、筆者の狙いです。

目次

  1. 環境セットアップ
  2. Lazy Loading部品のセットアップ
  3. データ取得のロジック
  4. Suspense導入

環境セットアップ

今回は簡単に見せたいのでVue CLIでセットアップします。

yarn global add @vue/cli
vue create vue-custom-suspense

終わったら、App.vueの中身は全て削除して、このようにします。

App.vue
<script setup lang="ts">

</script>

<template>
  <h1>Hello World</h1>
</template>

Lazy Loading部品のセットアップ

次はsrc/componentsにCommentsList.vueを作成して以下のようにします。

この部品はあくまでもApp.vueで保持するデータを表示するための部品です。

CommentsList.vue
<script setup lang="ts">
import { defineProps } from "vue"; // 本当はインポートする必要はないですが、Vue CLIがエラーを出すのでインポートしましょう。

defineProps<{
  comments: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>();
</script>

<template>
  <div class="card" v-for="comment in comments" :key="comment.id">
    <h1>{{ comment.name }}</h1>
    <p>{{ comment.body }}</p>
  </div>
</template>

<style>
.card {
  width: 90%;
  padding: 1rem;
  margin: 1rem auto 0 auto;
  background-color: white;
  border: 1px solid white;
  border-radius: 8px;
  box-shadow: 1px 2px 8px rgba(0, 0, 0, 0.226);
}
</style>

次、App.vueでLazy Loadingを使ってこのCommentsList.vueを読み込みます。

App.vue
<script setup lang="ts">
import { defineAsyncComponent } from "vue";

const CommentsList = defineAsyncComponent(
  () => import("./components/CommentList.vue")
);

const DUMMY_DATA = [
  { postId: 1, id: 1, name: "Hello World", email: "", body: "Hello World" },
];
</script>

<template>
  <h1>Comments</h1>
  <CommentsList :comments="DUMMY_DATA" />
</template>

Vueで部品のLazy Loadingを導入するにはdefineAsyncComponentを使います。Webpackのimportも使います(Vue CLIはwebpackを使っています)。

webpackのimport()はPromiseを返します。defineAsyncComponentはそのPromiseが解決されると、普通の部品を返してくれます。

CommentList.vueの場合は、chunkサイズがおそらく数KBなので、一瞬で読み込まれてしまいます。今回は、EChartsなど、大きいchunkを想定したいので、以下のように工夫して、読み込み時間を偽装します。

const CommentsList = defineAsyncComponent(() =>
  Promise.all([
    import("./components/CommentList.vue"),
    new Promise((resolve) => setTimeout(() => resolve(true), 2000)),
  ]).then(([loadedComponent, _]) => {
    return loadedComponent;
  })
);

このようにすると、2秒待って、CommentList.vueが表示されます!
JavaScriptぬPromiseや、あんしどぅやっさんたみ?

データ取得のロジック

次、jsonplaceholderのAPIを使って、模擬APIを組み込みます。

まずは、データを保持してくれるstateのreactive変数を作りましょう。

App.vue
<script setup lang="ts">
import { defineAsyncComponent, reactive } from "vue";

const CommentsList = defineAsyncComponent(() =>
  Promise.all([
    import("./components/CommentList.vue"),
    new Promise((resolve) => setTimeout(() => resolve(true), 2000)),
  ]).then(([loadedComponent, _]) => {
    return loadedComponent;
  })
);

const state = reactive<{
  startup: boolean;
  comments: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>({
  startup: true,
  comments: [],
});
</script>

<template>
  <h1>Comments</h1>
  <CommentsList :comments="state.comments" />
</template>

次、Fetch APIでデータ取得しますが、ここからがちょっと変わったやり方になります。
fetchと、部品のlazy loadingの両方のプロセスが完了するまで、読み込み部品(<p>Loading...</p>)を表示したいので、このPromise.allのArrayの中に、fetchのPromiseも入れるのです!

App.vue
import { defineAsyncComponent, reactive } from "vue";

const CommentsList = defineAsyncComponent(() =>
  Promise.all([
    import("./components/CommentList.vue"),
    fetch(
      "https://jsonplaceholder.typicode.com/comments?_page=1&_limit=25"
    ).then((response) => response.json()), // ここでJSONを開梱しておく
    new Promise((resolve) => setTimeout(() => resolve(true), 2000)),
  ])
    .then(([loadedComponent, fetchedData, _]) => {
      state.comments = fetchedData;
      return loadedComponent;
    })
);

言うまでもないかもしれませんが、const CommentListの下にconst stateを定義しているのに、どうしてstate.commentsをいじってもエラーが出ないのかというと、state.commentsの再定義が実行されるのは、stateが定義されてからevent loopが何回か回ってから、だからです。

Suspense模倣を導入

ここでfinallyをPromise.allに追加して、Loading...の部品も追加すると、出来上がり!

App.vue
<script setup lang="ts">
import { defineAsyncComponent, reactive } from "vue";

const CommentsList = defineAsyncComponent(() =>
  Promise.all([
    import("./components/CommentList.vue"),
    fetch(
      "https://jsonplaceholder.typicode.com/comments?_page=1&_limit=25"
    ).then((response) => response.json()), // ここでJSONを開梱しておくと
    new Promise((resolve) => setTimeout(() => resolve(true), 2000)),
  ])
    .then(([loadedComponent, fetchedData, _]) => {
      state.comments = fetchedData;
      return loadedComponent;
    })
    .finally(() => (state.startup = false))
);

const state = reactive<{
  startup: boolean;
  comments: {
    postId: number;
    id: number;
    name: string;
    email: string;
    body: string;
  }[];
}>({
  startup: true,
  comments: [],
});
</script>

<template>
  <h1>Comments</h1>
  <CommentsList :comments="state.comments" />
  <p v-if="state.startup">Loading...</p>
</template>

まとめ

これで部品の読み込みとデータの読み込みの両方を待つ、Suspenseもどき部品を作ってきました。いかがでしょうか?

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?