单页面利用(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,并传递signalfetch申请参数中:

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);

而后咱们将从咱们的异步组件定义中删除loadingComponenterrorComponent选项,因为咱们将应用SWRV来处理错误和加载状态。

// In setup()const AsyncComponent = defineAsyncComponent({  loader: importFunction,  delay: 200,  timeout: 5000,});

这意味着,咱们须要在模板文件中蕴含LoaderError组件,展现或暗藏取决于状态。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.jsimport { 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能够工作得很好,但它们也可能成为性能瓶颈。所以,让咱们试着把它们变得更好。

以上就是本文的全部内容,如果帮忙到了你,欢送点赞、珍藏、转发~