如何优化 Vue.js 应用程序
单页面应用(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可以工作得很好,但它们也可能成为性能瓶颈。所以,让我们试着把它们变得更好。
以上就是本文的全部内容,如果帮助到了你,欢迎点赞、收藏、转发~
如何优化 Vue.js 应用程序的更多相关文章
- Vuex-一个专为 Vue.js 应用程序开发的状态管理模式
为什么会出现Vuex 非父子关系的组件如何进行通信?(Event Bus)bus.js import Vue from 'vue'; export default new Vue(); foo.vue ...
- 【前端】vue.js环境配置以及实例运行简明教程
vue.js环境配置以及实例运行简明教程 声明:本文档编写参考如下两篇博客,是对它们的修改与补充,欢迎点击链接查看原文: 原文1:vue.js在windows本地下搭建环境和创建项目 原文2:Vue. ...
- 【Vue】转-Vue.js经典开源项目汇总
版权声明:本文为EnweiTech原创文章,未经博主允许不得转载. https://blog.csdn.net/English0523/article/details/88694219 Vue是什么? ...
- vue.js相关UI组件收集
内容 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 ###UI组件 element ★9689 - 饿了么出品的Vue2的web UI工具套件 Vux ★6927 - 基于Vu ...
- 【前端】Vue.js经典开源项目汇总
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- Vue.js经典开源项目汇总
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- 公司内部技术分享之Vue.js和前端工程化
今天主要的核心话题是Vue.js和前端工程化.我将结合我这两年多的工作学习经历来谈谈这个,主要侧重点是前端工程化,Vue.js侧重点相对前端工程化,比重不是特别大. Vue.js Vue.js和Rea ...
- Vue.js经典开源项目汇总-前端参考资源
Vue.js经典开源项目汇总 原文链接:http://www.cnblogs.com/huyong/p/6517949.html Vue是什么? Vue.js(读音 /vjuː/, 类似于 view) ...
- Vue.js 面试题整理
Vue项目结构介绍 build 文件夹:用于存放 webpack 相关配置和脚本. config 文件夹:主要存放配置文件,比如配置开发环境的端口号.开启热加载或开启gzip压缩等. dist 文件夹 ...
- Vue.js面试整理
Vue项目结构介绍 build 文件夹:用于存放 webpack 相关配置和脚本. config 文件夹:主要存放配置文件,比如配置开发环境的端口号.开启热加载或开启gzip压缩等. dist 文件夹 ...
随机推荐
- Vue 路由跳转显示空白页面的问题
在写一个登录界面跳转到首页时,路由如下 export default new VueRouter({ routes: [ { path: "/", name: "Logi ...
- linux学习相关资料整理
linux常用指令记录 Python3.9.9安装 supervisor安装与监控nginx 使用supervisor监控mysql supervisor监控tomcat配置文件 nginx-1.22 ...
- MYSQL快速安装整理
参考教程:https://www.cnblogs.com/brad93/p/16650780.html [检查是否已安装过] find / -name mysql [快速安装开始] groupadd ...
- JavaEE Day06 JDBC连接池&JDBCTemplate
今日内容: 数据库连接池 简化操作--Spring JDBC提供的 JDBC Template(JDBC的封装) 一.数据库连接池 1.引入 之前:每一次都要获取连接.释放连接-- 现在:连接重复使用 ...
- 【Shell案例】【awk map计数&sort按指定列排序】9、统计每个单词出现的个数
描述写一个 bash脚本以统计一个文本文件 nowcoder.txt 中每个单词出现的个数. 为了简单起见,你可以假设:nowcoder.txt只包括小写字母和空格.每个单词只由小写字母组成.单词间由 ...
- jjava基础语法
java基础语法1 注释 注意: SDK要选择JDK1.8,且下面的语言要选择8,配置完这个环境后,JAVA才能正常运行. 单行注释 //+text 快捷键:ctrl+/ 多行注释 /*+text+ ...
- vue设计与实现 第6章 ref 响应原理 笔记
ref 函数实现代码 const a = ref(1); function ref(value){ const wrapper = {value} Object.defineProperty(wrap ...
- PHP7.2 装mongodb 遇到的坑,完美解决!
公司要做QA安全测试,组长就丢了一个源码包给我,什么资料都无. 系统是个Laravel框架,源码都是从线上git下来.然后看了本地composer.json 没有生成vendor 第一步安装 comp ...
- SQLMap入门——获取表中的字段名
查询表名之后,查询表中的字段名 python sqlmap.py -u http://localhost/sqli-labs-master/Less-1/?id=1 -D xssplatform -T ...
- 微服务项目Git仓库自动化脚本
说明 基于微服务项目,产生的的多项目仓库管理脚本. 目录结构 xxxx Xxx1Api/ Xxx2Api/ git_clone_api.sh git_branch_dev.sh git_pull_al ...