构建大规模Vue.js 3应用程序的6个提示

104 阅读10分钟

Vue.js 3是一个坚实的框架,用于构建大型和小型的应用程序。在 "如何构建大型Vue.js应用程序"系列中,我们一直在探索如何在大型项目中最好地利用它。我们已经研究了一个好的文件结构是什么样子的,一些可预测性的标准,以及使用ESLint和Prettier来获得干净和一致的代码。

在这篇文章中,让我们看看我从Vue社区和我自己开发大规模Vue.js应用程序的经验中获得的6个技巧。

Tip 1: 优先选择组合式而不是混合式

Vue 3让我们有能力创建可重复使用的逻辑块,甚至包括反应式状态。这些可重用的逻辑块通常被称为 "可组合"。它们的功能与混杂函数类似(还有其他一些功能),但有一些优势。可组合物。

  • 提供一个透明的组件数据、方法等的来源
  • 消除命名上的冲突
  • 只是普通的JavaScript,你的IDE可以解释和自动完成,等等。

由于这些原因,我建议你在下一个大规模的Vue.js 3应用程序中使用可组合组件而不是混合组件。

可组合的东西是什么样子的?这里有一个简单的例子,比较了mixin和composable。两者都显示了一段可重复使用的代码,旨在通过ajax请求获取一个帖子,保持请求的状态,并保持获取的帖子。

// FetchPostMixin.js
const 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>

对于这个混合器,你必须注意不要在你的组件中包含任何与混合器中的数据属性等有相同名称的数据属性。另外,如果你在组件上注册了多个混合器,就不可能一眼看出特定的数据、方法等是来自哪里。

现在把它与可组合的相比。

//FetchPostComposable.js
import { 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>

我们现在不仅可以按逻辑关系来分组,而且在组件中,我们现在可以明确看到每个数据属性、方法等的来源。此外,我们可以很容易地重命名来自可组合的东西,以防止它与组件中已经存在的东西发生冲突。

Tip 2:在组件之间总是克隆对象

在Vue中处理反应式数据时,有可能在组件之间传递对象。虽然这很方便,但也可能产生意想不到的副作用。以这个例子为例。一个登录页面有一个user data属性。它是一个带有名字和密码属性的对象,它被传递到LoginForm组件中。

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

登录表单接收了用户,然后直接突变了它的属性。

// 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

gif of object data reacting in parent component

这不是我们应该在组件间进行交流的方式。相反,当user 数据发生变化时,LoginForm组件应该发出一个事件,如果LoginPage想监听这个事件并更新它的用户数据,那么它可以,但它不一定要这样做。

我知道当我刚开始使用Vue的时候,这个问题很早就出现了,而且经常出现。那么我们该如何解决这个问题呢?如果你正在创建一个将对象作为属性的组件,那么请确保在突变对象之前将其克隆为一个本地数据属性。

// 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>

gif of object in child not affecting object in parent as it should be

同样的概念也适用于将你的反应式对象传入Vuex动作或组件实例之外的其他地方。

Tip 3: 使用命名的Vuex存储模块

虽然Vuex为管理你的应用程序范围内的状态提供了一个很好的模式,但如果有太多的全局状态存在,你很快就会有一个臃肿和难以浏览的存储文件。

为了解决这个问题,Vuex提供了将你的商店分成不同的模块的能力,每个模块处理它自己的领域(即一个模块处理帖子,另一个处理用户,另一个处理评论,等等)。

没有模块

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

有模块

// store/index.js
import 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 fits
export default {
  namespaced: true,
  state:{
    items:[]
  },
  actions:{
    fetchOne(){},
    fetchAll(){}
  }
}

要了解使用这些模块的确切语法,你可以访问Vuex官方文档。不仅你的商店会更容易浏览,而且调用动作和动态访问状态也会更容易。

如果你在一个成长中的项目中使用Vuex,并且还没有使用模块,那么现在就是重构的时候了,因为在晚期状态下重构到命名的模块是非常痛苦的。

Tip 4: 编写测试

软件只会随着它的成长而变得更加复杂,因此,在它成长的过程中,我们有一种机制可以快速地确保事情仍在按预期进行。

那么,是什么让一个代码库可以测试呢?虽然我不是一个测试大师,但我的经验告诉我一些关于测试的事情。

  1. 一个 "单元 "越集中(不管是类、函数、组件还是什么),就越容易测试。
  2. 而一段代码所依赖的外部关系越多,通常就越难测试。

对于你的Vue项目,这意味着你可以采取一些实际的步骤来使事情变得更容易测试。

  1. 在可行的情况下,让所有的组件都关闭道具/事件,这样它们就不会有任何外部依赖。
  2. 将可重复使用的逻辑提取到辅助函数中,以便在组件的上下文之外进行测试。
  3. 给你的辅助函数一个单一的责任,不要让它们产生副作用。
  4. 当测试组件时,利用Vue测试库,它是对Vue测试工具的更高层次的抽象(并建立在其之上)。

最重要的是,一定要写测试,即使你不想写......我保证,你以后会感谢自己的。哦,要有一个自动测试运行的好方法,让测试失败阻止部署(CI/CD),否则你会有一个从未运行的测试套件。

Tip 5:通过SDK与REST API互动

首先,什么是SDK?你可以把SDK看作是与你的实际API交互的特定语言API。因此,与其在你的组件中直接调用axios或fetch,你可以使用像post.find(1)

那么,你愿意写哪一种呢?一个硬编码的请求到一个API端点。

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

还是在一个资源类上的可自动完成的方法?

post.find(1)

对我来说,答案是显而易见的。但也许你需要更多一点说服力。利用SDK,或者为你自己的REST API端点创建自己的SDK,可以提供以下优势。

  1. 不需要翻阅API文档。相反,通过你的IDE的intellisense功能浏览SDK方法。
  2. 保持你的API URL结构与你的实际应用无关,使得对实际API的更新更简单。是的,你的SDK必须知道这个结构,但它可能只限于一两个地方,而不是在你的应用程序代码库中到处都是。
  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。但根据我的经验,效率的提高是非常值得的。

Tip 6:包装第三方库

我对构建大规模Vue.js应用程序的最后一个建议是在第三方库周围创建一个包装器。例如,不要在你的代码库中直接使用axios ,你可以创建一个更通用的类,如Http ,其方法在引擎盖下调用axios。它可能看起来像这样。

// Http.js
import 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.js
export 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.js
import Cache from './Cache.js' // some random cache implementation of your choice
export 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 的概念,可以为你处理其中的一些扩展,但是它们的存在并不明显。使用封装类,做扩展只是知道用JavaScript可以做什么的问题,不需要翻阅文档来找到适当的地方来 "钩 "到第三方代码。不仅如此,一些第三方的依赖性可能不像axios那样全面,甚至会暴露出需要的钩子。

最后,虽然这个列表中的一些其他提示可以适用于大规模和小规模的应用程序,但这个提示真的只是针对大规模的一方。在我看来,对于小规模的应用来说,其好处并不值得开销这么大。

总结

Vue 3是开发大规模Web应用的一个很好的解决方案,但如果你不小心,你最终会让它对你不利,而不是为你所用。为了充分利用Vue 3,请考虑在你的下一个大规模应用中采用这6个提示吧

你有什么要补充的提示吗?请在下面留言,帮助我们都成为更好的Vue.js开发者!

本教程是我们的《如何构建大规模的Vue.js应用程序》系列的一部分

上一篇:使用Vite和Vue.js 3的ESLint和Prettier (2 of 3)