Vue3 - 懶加載與非同步組件

tags: Vue
category: Front-End
description: Vue3 - 懶加載(lazy loading)與非同步組件
created_at: 2022/07/23 20:00:00

cover image


回到 手把手開始寫 Vue3


前言

這兩個雖然不太一樣,但反正都跟組件有關就放一起了。

一個是讓頁面跑出來的速度快一些,讓部分組件需要的時候再去發 request 而不是一開始就都送。

一個是組件本身的行為是非同步(async)的,例如有發 fetch 之類的,那麼在 pending 的過程可能需要做一些 loading 畫面在用的。


懶加載?


其實我也不是很確定這個中文是不是這樣,不過主要就是在組件需要的時候才去發 request,而不是在第一時間就全部發。

直接上範例

<script setup>
import { ref } from "vue";
import LoadingComponent from "./components/LoadingComponent.vue";

const show = ref(false);
</script>

<template>
  <button class="px-4 py-2 bg-blue-500 text-white" @click="show = !show">
    Switch
  </button>
  <LoadingComponent v-if="show"></LoadingComponent>
</template>

而那個 LoadingComponent 可以替換成任何你想要的東西。

目前這樣的執行結果是沒有懶加載的,所以即使一開始並沒有呈現出來,但還是發出了 request

而要加入懶加載就要使用一個 defineAsyncComponent 的函數,基本使用方式如下:

<script setup>
import { ref, defineAsyncComponent } from "vue";

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

const show = ref(false);
</script>

<template>
  <button class="px-4 py-2 bg-blue-500 text-white" @click="show = !show">
    Switch
  </button>
  <LoadingComponent v-if="show"></LoadingComponent>
</template>

這時候你會發現一開始並不會去發 LoadingComponentrequest,而是在你點下按鈕要他顯示的時候才會去送。

而這個函數也有一些 options 可以下,如下(官方範例):

const AsyncComp = defineAsyncComponent({
  // loader function,要回傳一個 promise
  // resolve component (若有等待會顯示 loadingComponent)
  // 或
  // reject 掉,會進入 errorComponent
  loader: () => import('./Foo.vue'),

  // 就 loadingComponent
  loadingComponent: LoadingComponent,
  // 等待多久在顯示loadingComponent. 預設是200ms.
  delay: 200,

  // 就 errorComponent
  errorComponent: ErrorComponent,
  // 超時的時間. 預設是不會超時.
  timeout: 3000
})

用下面這種設定去玩,應該可以玩出一些心得:

const AComponent = defineAsyncComponent({
  loader: () =>
    new Promise((resolve, reject) => {
      // setTimeout(() => {
      //   resolve(import("./components/A.vue"));
      // }, 4000);
      setTimeout(() => {
        reject("hi");
      }, 1500);
    }),
  loadingComponent: LoadingComponent,
  delay: 1000,
  errorComponent: ErrorComponent,
  timeout: 3000,
});

可以切換 setTimeout 的區塊,或是修改時間。

然後會發現那個 loader 可以直接帶 import(),這是因為實際上 import() 他所回傳的也是一個 promise,而裡面的值就是那個模組的東西,可以看ES module dynamic import


處理非同步組件


先做一個假設,有一個組件包含 setTimeout,假設他是去打 API 拿回資料。

src/components/AsyncChild.vue

<script setup>
import { ref } from "vue";

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const data = ref([]);
sleep(2000).then(() => {
  data.value = [
    {
      id: 1,
      value: 1,
    },
    {
      id: 2,
      value: 2,
    },
    {
      id: 3,
      value: 3,
    },
  ];
});
</script>

<template>hi {{ data }}</template>

src/App.vue

<script setup>
import AsyncChild from "./components/AsyncChild.vue";
</script>

<template>
  <AsyncChild />
</template>

這時候你會看到2秒後呈現出 data 的內容,然後因為還沒做 loading 的處理,所以會有點乾。

假設稍微簡單做一下:

<script setup>
import { ref } from "vue";
import LoadingComponent from "./LoadingComponent.vue";

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const data = ref(null);
sleep(2000).then(() => {
  data.value = [
    {
      id: 1,
      value: 1,
    },
    {
      id: 2,
      value: 2,
    },
    {
      id: 3,
      value: 3,
    },
  ];
});
</script>

<template>
  <div v-if="data">hi {{ data }}</div>
  <LoadingComponent v-else />
</template>

那可想而知,如果我有 10 個這樣的組件,這樣的行為大概就要做 10 次。

那麼這時可以使用 <Suspense> ,但是官方有寫說目前這還是屬於實驗性質的 API,可能還不穩定。

大概會長成這樣

<script setup>
import AsyncChild from "./components/AsyncChild.vue";
import LoadingComponent from "./components/LoadingComponent.vue";
</script>

<template>
  <Suspense>
    <AsyncChild />
    <template #fallback>
      <LoadingComponent />
    </template>
  </Suspense>
</template>

然後這時的 AsyncChild.vue,主要要修改的是 async 相關的程式,讓回傳組件變成一個 promise

await sleep(2000).then(() => {
    //   ...
});

在頂層 <script setup></script> 語法糖之中是可以直接用 await 的,這時就會回傳一個 promise

這樣再去看就會先進入 2 秒的 Loading 在顯示 AsyncChild 的內容了。

而在這時應該也注意到了,在 VSCode 當中他會噴 ESLint 的錯誤 (await)

Parsing error: Cannot use keyword 'await' outside an async functioneslint

解決方案在這: https://eslint.vuejs.org/user-guide/#parsing-error-with-top-level-await

只要根據對應版本加入一些設定就可以了。


事件


使用這個 <Suspense> 還可以監聽一些事件,分別是

  • pending
  • resolve
  • fallback

可以像這樣子玩玩:

<script setup>
import AsyncChild from "./components/AsyncChild.vue";
import LoadingComponent from "./components/LoadingComponent.vue";

const onPending = () => {
  console.log("onPending");
};

const onResolve = () => {
  console.log("onResolve");
};

const onFallback = () => {
  console.log("onFallback");
};
</script>

<template>
  <Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
    <AsyncChild />
    <template #fallback>
      <LoadingComponent />
    </template>
  </Suspense>
</template>

這三個事件應該很好理解。




最後更新時間: 2022年07月23日.