背景
筆者は、React 18で本格的に導入されるデータ受信が終わるまで仮の部品(Loading...など)を表示する機能がとても待ち遠しいです。
仕事でVueを使っているので、Vueでも同じことができたら、と思いましたが、VueのSuspenseはまだ開発中でProductionの製品では使えません。
それと、データ受信の間もいいですが、部品のJavaScriptを読み込んでいる間(Lazy Loading)も、データ受信を待っている間も、Suspense部品を出したい!というのが、筆者の狙いです。
目次
- 環境セットアップ
- Lazy Loading部品のセットアップ
- データ取得のロジック
- Suspense導入
環境セットアップ
今回は簡単に見せたいのでVue CLIでセットアップします。
yarn global add @vue/cli
vue create vue-custom-suspense
終わったら、App.vueの中身は全て削除して、このようにします。
<script setup lang="ts">
</script>
<template>
<h1>Hello World</h1>
</template>
Lazy Loading部品のセットアップ
次はsrc/componentsにCommentsList.vueを作成して以下のようにします。
この部品はあくまでもApp.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を読み込みます。
<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変数を作りましょう。
<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も入れるのです!
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...の部品も追加すると、出来上がり!
<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もどき部品を作ってきました。いかがでしょうか?