单页面利用(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 能够工作得很好,但它们也可能成为性能瓶颈。所以,让咱们试着把它们变得更好。
以上就是本文的全部内容,如果帮忙到了你,欢送点赞、珍藏、转发~