單頁面應用(SPAs)當處理實時、非同步資料時,可以提供豐富的、可互動的使用者體驗。但它們也可能很重,很臃腫,而且效能很差。在這篇文章中,我們將介紹一些前端最佳化技巧,以保持我們的Vue應用程式相對精簡,並且只在需要的時候提供必需的JS。
注意:這裡假設你對Vue和Composition API有一定的熟悉程度,但無論你選擇哪種框架,都希望能有一些收穫。
本文作者是一名前端開發工程師,職責是構建Windscope應用程式。下面介紹基於該程式所做的一系列最佳化。
選擇框架
我們選擇的JS框架是Vue,部分原因是它是我最熟悉的框架。以前,Vue與React相比,整體包規模較小。然而,自從最近的React更新以來,平衡似乎已經轉移到React身上。這並不重要,因為我們將在本文中研究如何只匯入我們需要的東西。這兩個框架都有優秀的文件和龐大的開發者生態系統,這是另一個考慮因素。Svelte是另一個可能的選擇,但由於不熟悉,它需要更陡峭的學習曲線,而且由於較新,它的生態系統不太發達。
Vue Composition API
Vue 3引入了Composition API,這是一套新的API用於編寫元件,作為Options API的替代。透過專門使用Composition API,我們可以只匯入我們需要的Vue函式,而不是整個包。它還使我們能夠使用組合式函式編寫更多可重用的程式碼。使用Composition API編寫的程式碼更適合於最小化,而且整個應用程式更容易受到tree-shaking的影響。
注意:如果你正在使用較老版本的Vue,仍然可以使用Composition API:它已被補丁到Vue 2.7,並且有一個適用於舊版本的官方外掛。
匯入依賴
一個關鍵目標是減少透過客戶端下載的初始JS包的尺寸。Windscope廣泛使用D3進行資料視覺化,這是一個龐大的庫,範圍很廣。然而,Windscope只需要使用D3的一部分。
讓我們的依賴包儘可能小的一個最簡單的方法是,只匯入我們需要的模組。
讓我們來看看D3的selectAll
函式。我們可以不使用預設匯入,而只從d3-selection
模組中匯入我們需要的函式:
// Previous:
import * as d3 from 'd3'
// Instead:
import { selectAll } from 'd3-selection'
程式碼分割
有一些包在整個Windscope的很多地方都有使用,比如AWS Amplify認證庫,特別是Auth
方法。這是一個很大的依賴,對我們的JS包的大小有很大貢獻。比起在檔案頂部靜態匯入模組,動態匯入允許我們在程式碼中需要的地方準確匯入模組。
比起這麼匯入:
import { Auth } from '@aws-amplify/auth'
const user = Auth.currentAuthenticatedUser()
我們可以在想要使用它的地方匯入模組:
import('@aws-amplify/auth').then(({ Auth }) => {
const user = Auth.currentAuthenticatedUser()
})
這意味著該模組將被分割成一個單獨的JS包(或 "塊"),只有該模組被使用時,才會被瀏覽器下載。
除此之外,瀏覽器可以快取這些依賴,比起應用程式的其他部分程式碼,這些模組基本不會改變。
懶載入
我們的應用程式使用Vue Router作為導航路由。與動態匯入類似,我們可以懶載入我們的路由元件,這樣就可以在使用者導航到路由時,它們才會被匯入(連同其相關的依賴關係)。
index/router.js
檔案:
// Previously:
import Home from "../routes/Home.vue";
import About = "../routes/About.vue";
// Lazyload the route components instead:
const Home = () => import("../routes/Home.vue");
const About = () => import("../routes/About.vue");
const routes = [
{
name: "home",
path: "/",
component: Home,
},
{
name: "about",
path: "/about",
component: About,
},
];
當使用者點選About連結並導航到路由時,About路由所對應的程式碼才會被載入。
非同步元件
除了懶載入每個路由外,我們還可以使用Vue的defineAsyncComponent
方法懶載入單個元件。
const KPIComponent = defineAsyncComponent(() => import('../components/KPI.vue'))
這意味著KPI元件的程式碼會被非同步匯入,正如我們在路由示例中看到的那樣。當元件正在載入或者處於錯誤狀態時,我們也可以提供展示的元件(這個在載入特別大的檔案時非常有用)。
const KPIComponent = defineAsyncComponent({
loader: () => import('../components/KPI.vue'),
loadingComponent: Loader,
errorComponent: Error,
delay: 200,
timeout: 5000,
});
分割API請求
我們的應用程式主要關注的是資料視覺化,並在很大程度上依賴於從伺服器獲取大量的資料。其中一些請求可能相當慢,因為伺服器必須對資料進行一些計算。在最初的原型中,我們對每個路由的REST API進行了一次請求。不幸地是,我們發現這會導致使用者必須等待很長時間。
我們決定將API分成幾個端點,為每個部件發出請求。雖然這可能會增加整體的響應時間,但這意味著應用程式應該更快可用,因為使用者將看到頁面的一部分被渲染,而他們仍在等待其他部分。此外,任何可能發生的錯誤都會被本地化,而頁面的其他部分仍然可以使用。
有條件載入元件
現在我們可以把它和非同步元件結合起來,只在我們收到伺服器的成功響應時才載入一個元件。下面示例中我們獲取資料,然後在fetch
函式成功返回時匯入元件:
<template>
<div>
<component :is="KPIComponent" :data="data"></component>
</div>
</template>
<script>
import {
defineComponent,
ref,
defineAsyncComponent,
} from "vue";
import Loader from "./Loader";
import Error from "./Error";
export default defineComponent({
components: { Loader, Error },
setup() {
const data = ref(null);
const loadComponent = () => {
return fetch('<https://api.npoint.io/ec46e59905dc0011b7f4>')
.then((response) => response.json())
.then((response) => (data.value = response))
.then(() => import("../components/KPI.vue") // Import the component
.catch((e) => console.error(e));
};
const KPIComponent = defineAsyncComponent({
loader: loadComponent,
loadingComponent: Loader,
errorComponent: Error,
delay: 200,
timeout: 5000,
});
return { data, KPIComponent };
}
}
該模式可以擴充套件到應用程式的任意地方,元件在使用者互動後進行渲染。比如說,當使用者點選Map標籤時,載入map
元件以及相關依賴。
CSS
除了動態匯入JS模組外,在元件的<style>
塊中匯入依賴也會懶載入CSS:
// In MapView.vue
<style>
@import "../../node_modules/leaflet/dist/leaflet.css";
.map-wrapper {
aspect-ratio: 16 / 9;
}
</style>
完善載入狀態
在這一點上,我們的API請求是並行執行的,元件在不同時間被渲染。可能會注意到一件事,那就是頁面看起來很糟糕,因為佈局會有很大的變化。
一個讓使用者感覺更順暢的快速方法,是在部件上設定一個與渲染的元件大致對應的長寬比,這樣使用者就不會看到那麼大的佈局變化。我們可以傳入一個引數以考慮到不同的元件,並用一個預設值來回退。
// WidgetLoader.vue
<template>
<div class="widget" :style="{ 'aspect-ratio': loading ? aspectRatio : '' }">
<component :is="AsyncComponent" :data="data"></component>
</div>
</template>
<script>
import { defineComponent, ref, onBeforeMount, onBeforeUnmount } from "vue";
import Loader from "./Loader";
import Error from "./Error";
export default defineComponent({
components: { Loader, Error },
props: {
aspectRatio: {
type: String,
default: "5 / 3", // define a default value
},
url: String,
importFunction: Function,
},
setup(props) {
const data = ref(null);
const loading = ref(true);
const loadComponent = () => {
return fetch(url)
.then((response) => response.json())
.then((response) => (data.value = response))
.then(importFunction
.catch((e) => console.error(e))
.finally(() => (loading.value = false)); // Set the loading state to false
};
/* ...Rest of the component code */
return { data, aspectRatio, loading };
},
});
</script>
取消API請求
在一個有大量API請求的頁面上,如果使用者在所有請求還沒有完成時離開頁面,會發生什麼?我們可能不想這些請求繼續在後臺執行,拖慢了使用者體驗。
我們可以使用AbortController介面,這使我們能夠根據需要中止API請求。
在setup
函式中,我們建立一個新的controller
,並傳遞signal
到fetch
請求引數中:
setup(props) {
const controller = new AbortController();
const loadComponent = () => {
return fetch(url, { signal: controller.signal })
.then((response) => response.json())
.then((response) => (data.value = response))
.then(importFunction)
.catch((e) => console.error(e))
.finally(() => (loading.value = false));
};
}
然後我們使用Vue的onBeforeUnmount
函式,在元件被解除安裝之前中止請求:
onBeforeUnmount(() => controller.abort());
如果你執行該專案並在請求完成之前導航到另一個頁面,你應該看到控制檯中記錄的錯誤,說明請求已經被中止。
Stale While Revalidate
目前為止,我們已經做了相當好的一部分最佳化。但是當使用者前往下個頁面後,然後返回上一頁,所有的元件都會重新掛載,並返回自身的載入狀態,我們又必須再次等待請求有所響應。
Stale-while-revalidate是一種HTTP快取失效策略,瀏覽器決定是在內容仍然新鮮的情況下從快取中提供響應,還是在響應過期的情況下"重新驗證 "並從網路上提供響應。
除了在我們的HTTP響應中應用cache-control
頭部(不在本文範圍內,但可以閱讀Web.dev的這篇文章以瞭解更多細節),我們可以使用SWRV庫對我們的Vue元件狀態應用類似的策略。
首先,我們必須從SWRV庫中匯入組合式內容:
import useSWRV from "swrv";
然後,我們可以在setup
函式使用它。我們把loadComponent
函式改名為fetchData
,因為它將只處理資料的獲取。我們將不再在這個函式中匯入我們的元件,因為我們將單獨處理這個問題。
我們將把它作為第二個引數傳入useSWRV
函式呼叫。只有當我們需要一個自定義函式來獲取資料時,我們才需要這樣做(也許我們需要更新一些其他的狀態片段)。因為我們使用的是Abort Controller,所以我們要這樣做;否則,第二個引數可以省略,SWRV將使用Fetch API:
// In setup()
const { url, importFunction } = props;
const controller = new AbortController();
const fetchData = () => {
return fetch(url, { signal: controller.signal })
.then((response) => response.json())
.then((response) => (data.value = response))
.catch((e) => (error.value = e));
};
const { data, isValidating, error } = useSWRV(url, fetchData);
然後我們將從我們的非同步元件定義中刪除loadingComponent
和errorComponent
選項,因為我們將使用SWRV來處理錯誤和載入狀態。
// In setup()
const AsyncComponent = defineAsyncComponent({
loader: importFunction,
delay: 200,
timeout: 5000,
});
這意味著,我們需要在模板檔案中包含Loader
和Error
元件,展示或隱藏取決於狀態。isValidating
的返回值告訴我們是否有一個請求或重新驗證發生。
<template>
<div>
<Loader v-if="isValidating && !data"></Loader>
<Error v-else-if="error" :errorMessage="error.message"></Error>
<component :is="AsyncComponent" :data="data" v-else></component>
</div>
</template>
<script>
import {
defineComponent,
defineAsyncComponent,
} from "vue";
import useSWRV from "swrv";
export default defineComponent({
components: {
Error,
Loader,
},
props: {
url: String,
importFunction: Function,
},
setup(props) {
const { url, importFunction } = props;
const controller = new AbortController();
const fetchData = () => {
return fetch(url, { signal: controller.signal })
.then((response) => response.json())
.then((response) => (data.value = response))
.catch((e) => (error.value = e));
};
const { data, isValidating, error } = useSWRV(url, fetchData);
const AsyncComponent = defineAsyncComponent({
loader: importFunction,
delay: 200,
timeout: 5000,
});
onBeforeUnmount(() => controller.abort());
return {
AsyncComponent,
isValidating,
data,
error,
};
},
});
</script>
我們可以將其重構為自己的組合式程式碼,使我們的程式碼更簡潔一些,並使我們能夠在任何地方使用它。
// composables/lazyFetch.js
import { onBeforeUnmount } from "vue";
import useSWRV from "swrv";
export function useLazyFetch(url) {
const controller = new AbortController();
const fetchData = () => {
return fetch(url, { signal: controller.signal })
.then((response) => response.json())
.then((response) => (data.value = response))
.catch((e) => (error.value = e));
};
const { data, isValidating, error } = useSWRV(url, fetchData);
onBeforeUnmount(() => controller.abort());
return {
isValidating,
data,
error,
};
}
// WidgetLoader.vue
<script>
import { defineComponent, defineAsyncComponent, computed } from "vue";
import Loader from "./Loader";
import Error from "./Error";
import { useLazyFetch } from "../composables/lazyFetch";
export default defineComponent({
components: {
Error,
Loader,
},
props: {
aspectRatio: {
type: String,
default: "5 / 3",
},
url: String,
importFunction: Function,
},
setup(props) {
const { aspectRatio, url, importFunction } = props;
const { data, isValidating, error } = useLazyFetch(url);
const AsyncComponent = defineAsyncComponent({
loader: importFunction,
delay: 200,
timeout: 5000,
});
return {
aspectRatio,
AsyncComponent,
isValidating,
data,
error,
};
},
});
</script>
更新指示
如果我們能在我們的請求重新驗證的時候向使用者顯示一個指示器,這樣他們就知道應用程式正在檢查新的資料,這可能會很有用。在這個例子中,我在元件的角落裡新增了一個小的載入指示器,只有在已經有資料,但元件正在檢查更新時才會顯示。我還在元件上新增了一個簡單的fade-in
過渡(使用Vue內建的Transition
元件),所以當元件被渲染時,不會有突兀的跳躍。
<template>
<div
class="widget"
:style="{ 'aspect-ratio': isValidating && !data ? aspectRatio : '' }"
>
<Loader v-if="isValidating && !data"></Loader>
<Error v-else-if="error" :errorMessage="error.message"></Error>
<Transition>
<component :is="AsyncComponent" :data="data" v-else></component>
</Transition>
<!--Indicator if data is updating-->
<Loader
v-if="isValidating && data"
text=""
></Loader>
</div>
</template>
總結
在建立我們的網路應用程式時,優先考慮效能,可以提高使用者體驗,並有助於確保它們可以被儘可能多的人使用。我希望這篇文章提供了一些關於如何使你的應用程式儘可能高效的觀點--無論你是選擇全部還是部分地實施它們。
SPA可以工作得很好,但它們也可能成為效能瓶頸。所以,讓我們試著把它們變得更好。
以上就是本文的全部內容,如果幫助到了你,歡迎點贊、收藏、轉發~