Vue.js 3时代来临:构建大规模应用程序的六大秘籍

发表时间: 2024-06-09 22:17

Vue.js 3是一个用于构建大型和小型应用程序的可靠框架。

本文将介绍我从Vue社区和我自己开发大规模Vue.js应用程序的经验中总结得出的6个技巧。

技巧1:可组合块优于mixin

Vue 3允许我们创建可重用的逻辑块,且逻辑块甚至可以包含响应性状态。

这些可重用的块通常称为“可组合块”。它们可以提供与mixin类似的功能,可组合块的优点在于:

  • 提供组件数据、方法等的透明来源
  • 消除命名冲突
  • 只是普通的JavaScript,因此IDE可执行解释和自动完成等操作。

基于此,我的建议是,对于大规模的Vue.js 3应用程序,最优选项是使用可组合块而不是mixin

可组合块是什么样子的呢?

下面是一个比较mixin和可组合块的简单示例。

两者都显示了一段可重用的代码,旨在通过ajax请求获取帖子,保持请求的状态,并保存获取的帖子。

// FetchPostMixin.jsconst REQUEST_IN_PROGRESS = "REQUEST_IN_PROGRESS";const REQUEST_ERROR = "REQUEST_ERROR";const REQUEST_SUCCESS = "REQUEST_SUCCESS";export default {  data() {    return {      requestState: null,      post: null    };  },  computed: {    loading() {      return this.requestState === REQUEST_IN_PROGRESS;    },    error() {      return this.requestState === REQUEST_ERROR;    }  },  methods: {    async fetchPost(id) {      this.post = null;      this.requestState = REQUEST_IN_PROGRESS;      try {        const res = await fetch(https://jsonplaceholder.typicode.com/posts/${id});        this.post = await res.json();        this.requestState = REQUEST_SUCCESS;      } catch (error) {        this.requestState = REQUEST_ERROR;      }    }  }};// PostComponent.vue<script>import FetchPostMixin from "./FetchPostMixin";export default {  mixins:[FetchPostMixin],};</script>

使用mixin时,你必须注意不要在组件中包含与mixin中的数据属性等同名的任何数据属性。

此外,如果你在组件上注册了多个mixin,则无法一目了然地判断特定数据、方法等的来源。

那么相同功能的可组块是怎么样的呢?

//FetchPostComposable.jsimport { ref, computed } from "vue";export const useFetchPost = () => {  // Request States  const REQUEST_IN_PROGRESS = "REQUEST_IN_PROGRESS";  const REQUEST_ERROR = "REQUEST_ERROR";  const REQUEST_SUCCESS = "REQUEST_SUCCESS";  const requestState = ref(null);  const loading = computed(() => requestState.value === REQUEST_IN_PROGRESS);  const error = computed(() => requestState.value === REQUEST_ERROR);  // Post  const post = ref(null);  const fetchPost = async (id) => {    post.value = null;    requestState.value = REQUEST_IN_PROGRESS;    try {      const res = await fetch(https://jsonplaceholder.typicode.com/posts/${id});      post.value = await res.json();      requestState.value = REQUEST_SUCCESS;    } catch (error) {      requestState.value = REQUEST_ERROR;    }  };  return { post, loading, error, fetchPost };};// PostComponent.vue<script>import { useFetchPost } from "./FetchPostComposable";export default {  setup() {    //clear source of data/methods    const {       loading: loadingPost, //can rename      error, fetchPost, post } = useFetchPost();    return { loadingPost, error, fetchPost, post };  },};</script>

在组件中,我们不仅可以按逻辑对事物进行分组,还可以清楚地看到每个数据属性、方法等的来源。

此外,我们可以轻松地重命名来自可组合块的任何内容,以防止它与组件中已经存在的其他内容发生冲突。

技巧2:始终在组件之间克隆对象

在Vue中处理响应式数据时,可以在组件之间传递对象。

虽然这可能非常方便,但也可能产生意想不到的副作用。

举个例子,登录页面上通常会有user数据属性。

这是一个具有namepassword属性的对象,我们将它传递给LoginForm组件。

// LoginPage.vue<template>  <LoginForm :user="user" /></template><script>import LoginForm from './LoginForm.vue'export default {  components: {LoginForm},  data(){    return {      user: { name: '', password: ''}    }  }}</script>

登录表单接收user,然后直接改变其属性。

// LoginForm.vue<template>  <div>    <label>      Name <input type="text" v-model="user.name">    </label>    <label>      Password <input type="password" v-model="user.password">    </label>  </div></template><script>export default {  props:{    user: Object  }}</script>

让我们在登录页面添加一些标题,以便可以更好地了解正在发生的事情。

<template>  <h1>Parent Component</h1>  <code>{{user}}</code>  <h1>Child Component</h1>  <LoginForm :form="user" /></template>

现在键入输入,你可以发现在LoginForm中更改user,实际上也更改了父组件中的user

跨组件通信不应该采用这样的方式。当user数据更改时,LoginForm组件应该发出一个事件。

那么如何解决这个问题呢?

如果要创建将对象作为属性的组件,那么需要在更改对象之前将该对象克隆到本地数据属性。

// LoginForm.vue<template> ...<input type="text" v-model="form.name"><input type="password" v-model="form.password"></template><script>export default {  //...  data(){    return {      // spreading an object effectively clones it's top level properties      // for objects with nested objects you'll need a more thorough solution      form: {...this.user}    }  },}</script>

同样的概念也适用于将响应式对象传递到Vuex操作或组件实例之外的任何其他位置时。

技巧3:使用命名空间的Vuex存储模块

虽然Vuex为管理应用程序范围状态提供了一个很好的模式,但如果存在太多的全局状态,那么store文件很快会变得臃肿且难以导航。

为了解决这个问题,Vuex允许将store分解为不同的模块,每个模块处理各自的域(即一个模块处理帖子、一个模块处理用户、一个模块处理评论等)。

不使用模块:

// store/index.jsexport default {  state:{    posts:[],    users:[],    comments:[]  },  actions:{    fetchPost(){},    fetchPosts(){},    fetchUser(){},    fetchUsers(){},    fetchComment(){},    fetchComments(){},  }}

使用模块:

// store/index.jsimport posts from './modules/posts.js'import users from './modules/users.js'import comments from './modules/comments.js'export default{  modules:{ posts, users, comments }}//store/modules/posts.js // and the same for each of the other domains with // common state, actions named the same (possible because of namespacing)// and any other domain specific state and logic named as fitsexport default {  namespaced: true,  state:{    items:[]  },  actions:{    fetchOne(){},    fetchAll(){}  }}

你可以访问官方Vuex文档以查看使用这些模块的确切语法。

模块的使用,不但使得store更易于导航,而且动态调用操作和访问状态也更容易。

如果你正在开发中的Vuex项目还没有使用模块,那么现在是时候重构了,因为后期重构命名空间的模块会非常痛苦。

技巧4:编写测试

随着时间的推移,软件只会变得越来越复杂,因此我们需要一种机制可以快速确保代码仍然按预期工作。

那么怎么让代码库可测试呢?在我看来:

  1. “单元”越集中(无论是类、函数。还是组件等),测试就越容易。
  2. 代码片段所依赖的外部依赖项越多,通常就越难测试。

对于Vue项目,以下方法可以使得项目更易于测试:

  1. 尽可能多地使组件全部prop向下/事件向上,以便没有任何外部依赖项。
  2. 将可重用逻辑提取到可在组件上下文之外测试的帮助程序函数
  3. 程序函数只履行单一职责
  4. 测试组件使用Vue Testing Library,Vue Testing Library是建立在Vue Test Utils基础上的更高级别的抽象

对于测试,即使你现在写得满心不情愿,但我保证,以后你会感谢自己的。

哦,对了,自动化测试运行搭配测试失败阻止部署(CI/CD)的方法很不错,否则只会得到一个永远不会运行的测试套件。

技巧5:通过SDK与REST API交互

首先,什么是SDK?

你可以将SDK视为语言特定的与实际API进行交互的API。

因此,直接在组件中调用axiosfetch,还不如使用post.find(1)之类的东西。

那么,你会选哪种方法呢?硬编码请求到API?

axios('https://someapi.com/posts/1')

还是资源类上编写可自动完成的方法?

post.find(1)

对我来说,答案是显而易见的。利用SDK或为自己的REST API终结点创建SDK具有以下优势:

  1. 减少对API文档的倾注。而是通过IDE的智能感知功能浏览SDK方法。
  2. 保持API URL结构与实际应用程序无关,简化对实际API的更新。
  3. 不太可能打错字。因为,IDE可帮助键入。
  4. 能够抽象出与向API发出请求相关的问题。例如,检查请求状态可以像这样简单:if(post.isLoading){}
  5. 与前端资源交互的语法与后端类似(当然,由于安全性的缘故会受到限制)。例如,如果你使用Laravel来支持Rest API,那么可以将Spatie的laravel-query-builder包与许多前端库结合起来,这些前端库通过模仿Laravel模型API在客户端发出API请求。
  6. 当REST API更改时(例如,当终结点名称或身份验证方法更改时),可以更轻松地重构API集成

这种方法是有缺点的。

除了REST API之外,你还必须花时间编写SDK代码以及文档化SDK。

但是,根据我的经验,效率方面的回报是值得的。

技巧6:包装第三方库

构建大规模Vue.js应用程序的最后一个技巧是围绕第三方库创建一个包装器。

例如,不要直接在代码库中使用axios,而是创建一个通用命名的类,如Http,然后在后台调用axios。就像这样:

// Http.jsimport axios from 'axios'export default class Http{  async get(url){    const response = await axios.get(url);    return response.data;  }  // ...}

好的,乍一看,这似乎毫无意义。但是,这种做法有一些非常方便的优点。

在不更改接口的情况下更改依赖项

假设出于某种原因,你必须放弃axios。毕竟它是第三方代码,无法控制。

因此,你决定对http解决方案使用fetch

不用在整个代码库中搜索正在使用的axios实例,只要在Http.js文件进行切换即可。

// Http.jsexport default class Http{  async get(url){    const response = await fetch(url);    return await response.json();  }  // ...}

这样就可以用fetch替代axios发出HTTP请求。

此外,即使单个开发人员进行切换,但团队中的其他人不知道也没关系。大家仍然可以继续以同样的方式使用Http类。

更明显地指向扩展功能

包装第三方依赖项的另一个优点是,它通常会公开扩展类API的方法以处理相关功能。

例如,如果你希望在ajax请求失败时提醒用户,那么不需要在发出请求的所有位置手动使用try catch。相反,Http类可完成此操作。

export default class Http{  async get(url){    try {      const response = await fetch(url);      return await response.json();    } catch (err) {      alert(err);    }  }  // ...}

也可以为请求添加缓存。

// Http.jsimport Cache from './Cache.js' // some random cache implementation of your choiceexport default class Http{  async get(url){    const cached = Cache.get(url)    if(cached) return cached    const response = await fetch(url);    return await response.json();  }  // ...}

例子举不胜举。

这里值得注意的是,axios库确实提供了一个称为interceptors的概念,可以处理一些扩展,但不大起眼。

使用包装器类以及进行扩展,无需查看文档就可以找到“挂钩”到第三方代码的适当位置。

不仅如此,一些第三方依赖项可能不如axios全面,甚至会暴露所需的钩子。

最后,此列表中的其他技巧可应用于大规模和小规模的应用程序,但这个技巧仅适用于大规模应用。对于小规模应用程序,这样做反而得不偿失。

结论

Vue 3是开发大规模Web应用程序的绝佳解决方案,但如果你不小心,反而会弄巧成拙。

为了充分利用Vue 3,这6个技巧可以帮助你轻松应对大规模应用程序!