Featured image of post 【转】组织并解耦你在 NuxtJs 中调用的 api

【转】组织并解耦你在 NuxtJs 中调用的 api

你的 Nuxt 应用总是和与之匹配的后端服务共同成长,慢慢发展壮大的。这时,你的 API 也从屈指可数直到变成了如广袤丛林般的庞大资源,而你依然想要在这“丛林”中称王。这意味着你必须合理地组织这些 API 以保证它们都是有迹可循的,而非一团乱麻。假设这样一个场景,你想要重命名一个资源,把它的名字从 images 改为 photos,你绞尽脑汁,试图在茫茫代码中寻找出所有需要更改的点,并避免错误地改变其他不应该被涉及的变量。光是在大脑里想想就觉得很恶心了吧。或者再想象一下,当你需要为了鉴权 (Authorization) 在 http 请求头里添加另一个值的时候吧。

所以,在前端合理组织你的 API 调用是非常有必要的。但在 Nuxt 中应该怎么搞呢?

关于 Nuxt 的两面性

在 Nuxt 中,想要合理组织 API 请求会让人感觉很麻烦,因为你要同时兼顾客户端和服务端。你可能需要在以下的几种情境中取回数据:

  • 在服务端的 asyncDatafetch 方法中;
  • 在 Vuex store (action) 中;
  • 在处于客户端环境下的 Vue 组件中。

这意味着,我们需要有一个能满足以上所有情境的解决方案。

关于 Nuxt axios 模块

如果你还没有使用 Nuxt 官方提供的 axios 模块 (@nuxtjs/axios[2]),你真的应该切换到这上面来了,现在!立刻!马上!然后,你就不需要再到处引入 axios 了。引入 Nuxt 官方的 axios 模块后,你可以在客户端的组件中通过 this.$axios 调用 axios,也可以在像 asyncDatafetch 这样的可以在服务端执行的方法中通过 Nuxt 的上下文调用 (ctx.$axios),也可以在 Vuex 中通过 this.$axios 调用。并且远不止如此,你还可以设置默认的请求头,基于你的环境设置 baseURL,以及设置一些其他你认为需要的选项。另外,它还为所有的 HTTP 请求类型提供了简短的变量 ($get…) 来方便你的使用。

但这就够了吗?当然不!我们找到了一个集中配置 axios 的方案,但这仅仅解决了一部分麻烦。

(译者有话说:事实上,nuxtjs/axios 模块除了可以同时侵入服务端和客户端的上下文中这一点,axios 本身也具备上文提及的别的优点,并且更加灵活易用。nuxtjs/axios 的定制一定程度上方便了使用,但也带来了很大的制约。)

抽象我们的 REST API

现在开始有趣的部分。在下文中,假设使用 RESTful 风格的 API。下文中涉及的技术适用于大部分 API (风格),但需要做一些细微的调整。

在我们的例子中,我们将使用 JSONPlaceholder API[3],因为它涵盖了所有的 HTTP 请求类型,且并不复杂。

让我们看一下帖子资源。有如下操作:

  • 新建帖子
  • 展示一个帖子的内容
  • 在其首页展示帖子列表
  • 更新帖子
  • 删除帖子

再考虑一下其他的资源,比如用户,模式如下:

  • 创建用户
  • 展示某个用户详情
  • 展示用户列表
  • 更新用户
  • 删除用户

基于以上,我们可以抽象我们的 API 为一个简单的 JS 对象,并在其中为每一个动作创建方法。创建一个 (class) 是一个好的选择。

我们这里参考的设计模式为仓库模式 (Repository Pattern)。

让我们创建如下文件,~/api/repository.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export default {
  create(payload) {},

  show(id) {},

  index() {},

  update(payload, id) {},

  delete(id) {}
}

ok,我们已经有了一个基本的 API 抽象的框架,虽然我们还需要继续完善这些方法。这时,由 Nuxt axios 提供的 $axios 就派上了用场,但怎样才能把它注入到一个外部的模块中呢?直接通过 this.$axios 调用反正是不行了。

依赖注入来拯救我们

这时,另一个术语闪过脑海:依赖注入 (Dependency Injection)。在像 PHP 或 Java 这样的面向对象的语言中,这是经常用到的模式,当然,它也会在我们的应用场景中提供助力。

通常,如果你依赖一个像 axios 这样的工具库,你大概会这么做:

1
2
3
4
5
6
7
8
import axios from 'axios'

export default {
  async index() {
    const result = await axios.get('...')
    return result.data
  }
}

在多数场景下,这没问题。但在我们的场景下,这么做会有如下劣势:

  • 当你需要的时候,你无法很容易地将这一实现替换为另一个;
  • 在运行时之前,依赖需要已知

前者在我们的案例中并不是大问题,但是后者却无法忽视。虽然我们已知有 axios 作为依赖,但是我们并不能在实际的应用中引入它。

现在换个思路,我们将 axios 作为依赖注入。这意味着我们只是简单的把它作为一个参数传递进一个函数中(如果你使用的是,则传入构造器中)。

1
2
3
4
5
6
export default axios => ({
  async index() {
    const result = await axios.get('...')
    return result.data
  })
}

Duang~依赖被注入了!让我们完善我们之前基于仓库模式的框架并愉快地将 $axios 用起来吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default $axios => ({
  index() {
    return $axios.$get('/posts')
  },

  create(payload) {
    return $axios.$post(`/posts`, payload)
  },

  show(id) {
    return $axios.$get(`/posts/${id}`)
  },


  update(payload, id) {
    return $axios.$put(`/posts/${id}`, payload)
  },

  delete(id) {
    return $axios.$delete(`/posts/${id}`)
  }

})

概括一下我们的实现

ok,一切顺利!目前为止,我们获取帖子资源的方法依然是写死的。为了增强复用性,我们希望动态传递各类资源(接口)的 URL。

我们不会直接在我们的默认导出方法中再次增加第二个参数。作为替代,我们将改写这个函数为一个高阶函数(说人话就是在一个函数中返回另一个函数):

1
2
3
4
5
6
export default $axios => resource => ({
  index() {
    return $axios.$get(`/${resource}`)
  },
  // ...
}

现在让我们在某个地方引入这个函数,我们可以通过复用它来组织更多的资源获取的接口,同时仅仅需要单次引入 axios 实例即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import createRepository from '~/api/Repository.js'

// 首先调用这个方法并传入 axios 对象

const $axios = getAxiosMagicallyFromSomewhere // 接下来我们将找到如何获取它的方法
const repositoryWithAxios = createRepository($axios)

// 接下来你可以创建“仓库”并复用 `repositoryWithAxios` 函数

const postRepository = repositoryWithAxios('posts')
const userRepository = repositoryWithAxios('users')
// ...

基于这一模式,你不必再一遍又一遍传递配置项了。

借用 NuxtJs 的插件的力量

走到现在,我们已经成功地抽象出了 API resources,并找到了复用抽象的恰当方式。但依然有两个问题摆在我们面前:

  • 我们如何使这一抽象在 Nuxt 应用中随处可用?
  • 如何将来自 Nuxt 模块的 axios 实例注入?

我们将使用一个 Nuxt 插件解决这两个问题。Nuxt 的插件机制通常被用来添加全局 Vue 组件或者方法库,以及更多其他的用途。值得注意的是,插件们将在 Vue 根实例被创建之前执行。

ok,让我们创建一个插件 ~/plugins/repository.js

如果一个插件有一个默认导出,被导出的函数将获得两个参数,一个是Nuxt 的上下文(context),另一个是 inject

1
2
3
4
5
import createRepository from '~/api/Repository.js'

export default (ctx, inject) => {
 // Here we will do it
}

在这个函数内部,我们可以拿到所有需要的东西,从而让我们在整个 Nuxt 应用中都可以调用到 API 仓库,并成功将 axios 实例注入到我们的架构之中。

传入 axios 实例

因为在插件的默认导出方法中, context 唾手可得,所以传入 axios 实例的难题也就迎刃而解了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import createRepository from '~/api/Repository.js'

export default (ctx, inject) => {
  const repositoryWithAxios = createRepository(ctx.$axios)

  const repositories = {
    posts: repositoryWithAxios('posts'),
    users: repositoryWithAxios('users')
    //...
  }
}

注入它!

现在重点来了。为了让我们的以上实现在整个 Nuxt 应用中(组件中,asyncData 方法中以及更多的地方,参考上文)可用,要怎么办呢?当然不可能去手动执行了,我们将通过 inject 方法,也就是第二个参数来实现这一需求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import createRepository from '~/api/Repository.js'

export default (ctx, inject) => {
  const repositoryWithAxios = createRepository(ctx.$axios)

  const repositories = {
    posts: repositoryWithAxios('posts'),
    users: repositoryWithAxios('users')
    //...
  }

  inject('repositories', repositories)
}

inject 方法接收一个 name(使用的时候需要在这个 key 前加一个 $ 前缀)作为第一个参数,接收与这个 key 对应的值作为第二个参数,这个值可以是原始数据类型,对象或者函数。

inject 方法将做如下事情:

  • 添加 kv 对到 Vue 的原型中,这样就可以在组件或 store 中通过 this.$key 调用了
  • 添加 kv 对到 ctx.app 对象中,这样就可以在 asycData | fetch 等方法以及其他地方使用了

使用它!

我们已经完成了所有的配置,唯一需要做的就是在实际开发中使用它了。
上下文环境中 this.$repositories 就可以获得对象

总结

通过 API 仓库的形式组织你的 api 调用,使它们统一到一起从而有迹可循,同时减少了一股脑的代码复制粘贴,并且能够更容易地进行 debug 和修改。

注入一个对象到上下文中,以及通过 Nuxt 插件注入其他依赖的思想,同样可以作为一个常用的模式应用到其他的使用场景中。期待举一反三。

原文地址: https://blog.lichter.io/posts/nuxt-api-call-organization-and-decoupling/