单页面应用(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.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可以工作得很好,但它们也可能成为性能瓶颈。所以,让我们试着把它们变得更好。

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

如何优化 Vue.js 应用程序的更多相关文章

  1. Vuex-一个专为 Vue.js 应用程序开发的状态管理模式

    为什么会出现Vuex 非父子关系的组件如何进行通信?(Event Bus)bus.js import Vue from 'vue'; export default new Vue(); foo.vue ...

  2. 【前端】vue.js环境配置以及实例运行简明教程

    vue.js环境配置以及实例运行简明教程 声明:本文档编写参考如下两篇博客,是对它们的修改与补充,欢迎点击链接查看原文: 原文1:vue.js在windows本地下搭建环境和创建项目 原文2:Vue. ...

  3. 【Vue】转-Vue.js经典开源项目汇总

    版权声明:本文为EnweiTech原创文章,未经博主允许不得转载. https://blog.csdn.net/English0523/article/details/88694219 Vue是什么? ...

  4. vue.js相关UI组件收集

    内容 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 ###UI组件 element ★9689 - 饿了么出品的Vue2的web UI工具套件 Vux ★6927 - 基于Vu ...

  5. 【前端】Vue.js经典开源项目汇总

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  6. Vue.js经典开源项目汇总

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  7. 公司内部技术分享之Vue.js和前端工程化

    今天主要的核心话题是Vue.js和前端工程化.我将结合我这两年多的工作学习经历来谈谈这个,主要侧重点是前端工程化,Vue.js侧重点相对前端工程化,比重不是特别大. Vue.js Vue.js和Rea ...

  8. Vue.js经典开源项目汇总-前端参考资源

    Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...

  9. Vue.js 面试题整理

    Vue项目结构介绍 build 文件夹:用于存放 webpack 相关配置和脚本. config 文件夹:主要存放配置文件,比如配置开发环境的端口号.开启热加载或开启gzip压缩等. dist 文件夹 ...

  10. Vue.js面试整理

    Vue项目结构介绍 build 文件夹:用于存放 webpack 相关配置和脚本. config 文件夹:主要存放配置文件,比如配置开发环境的端口号.开启热加载或开启gzip压缩等. dist 文件夹 ...

随机推荐

  1. 第2-4-2章 规则引擎Drools入门案例-业务规则管理系统-组件化-中台

    目录 3. Drools入门案例 3.1 业务场景说明 3.2 开发实现 3.3 小结 3.3.1 规则引擎构成 3.3.2 相关概念说明 3.3.3 规则引擎执行过程 3.3.4 KIE介绍 3. ...

  2. HDC2022的无障碍参会体验,手语服务是如何做到的?

    华为开发者大会2022(HDC)上,HMS Core手语数字人以全新形象亮相,并在直播中完成了长达3个多小时的实时手语翻译,向线上线下超过一千万的观众提供了专业.实时.准确的手语翻译服务,为听障人士提 ...

  3. Zabbix与乐维监控对比分析(二)——Agent管理、自动发现、权限管理

    上期我们详细介绍了Zabbix与乐维监控的架构与性能对比分析,透过架构与性能对比分析,用户可以对乐维监控之所以能成为"Zabbix企业版"有一个初步的认知.本篇是Zabbix对比乐 ...

  4. 大前端html基础学习03-定位锚点透明

    一.position 定位属性和属性值position 定位属性,检索对象的定位方式:语法:position:static /absolute/relative/fixed/sticky/unset/ ...

  5. 【Java SE】Day06 类与对象、封装和构造方法

    一.面向对象思想 1.概述:调用对象的行为实现功能,无需一步一步实现(从执行者变成指挥者) 2.类和对象 类是属性和行为的集合,可以看成描述事物的模板 对象是事物的具体体现,是类的一个实例,具备该类的 ...

  6. Python:灵活的开发环境

    以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「englyf」https://mp.weixin.qq.com/s/WTl7BPAhX5VuK-gmHaErMg 本文大概 1667 个 ...

  7. Java第一课Hello World

    java第一课 Hello World 学习 新建文件夹放写的代码 新建.txt文件,并写入java 输出Hello World 的代码  public class Hello{     public ...

  8. 视图 触发器 事务 MVCC 存储过程 MySQL函数 MySQL流程控制 索引的数据结构 索引失效 慢查询优化explain 数据库设计三范式

    目录 视图 create view ... as 触发器 简介 创建触发器的语法 create trigger 触发器命名有一定的规律 临时修改SQL语句的结束符 delimiter 触发器的实际运用 ...

  9. 自研ORM Include拆分查询(递归算法 支持无限层级) 性能优化探讨

    最近我在优化 Include 拆分查询,贴出源码供大家交流探讨是否还有优化空间. 测试代码 1 Console.WriteLine($"总记录数:{db.Query<Category& ...

  10. 使用C语言编程的7个步骤

    版权声明 本文作者:main工作室 本文链接:https://www.cnblogs.com/main-studio/p/17034891.html 版权声明:本文为 博客园 博主「main工作室」的 ...