使用Nuxt.js + apollo-module(vue-apollo) + TypeScript的方式给代码添加类型

我认为在使用Vue.js开发GraphQL时,Vue Apollo是一个选择。

GraphQL的客户端可以使用fetch或XMLHttpRequest进行实现。然而,考虑到在Nuxt.js上进行SSR,我们需要考虑一些要点和实现方面的问题,比如在服务器端获取数据,与Vue的data进行合并等等。使用Vue Apollo可以更轻松地处理这些方面的问题。

然而,Apollo的高功能需要一些时间来掌握,而且根据使用情况可能会出现超标的可能性。

Vue Apollo是Vue.js的插件,可以将Apollo的GraphQL客户端整合到Vue.js中。

此外,Nuxt.js的apollo-module将Vue Apollo集成到了Nuxt.js中。

在这篇文章中,我将解释在使用Nuxt.js + TypeScript + apollo-module进行开发时如何添加类型注解。

顺便提一下,在这篇文章中使用的软件版本如下所示。

    • nuxt: 2.12.0

vue: 2.6.11

typescript: 3.8.3
nuxtjs/apollo: 4.0.0-rc19

vue-apollo: 3.0.3

另外,本文的示范代码也在下面的仓库中公开。

安装

使用以下命令创建Nuxt项目。

npx create-nuxt-app nuxt-apollo-typescript

如果设置处于设置中,编程语言为TypeScript,渲染模式为Universal(SSR),其余可以随意。

使用fetch进行的GraphQL客户端操作

首先,由于我想从简单的地方开始,所以我要尝试使用fetch创建一个最小化的GraphQL客户端。我将使用常用的SW API作为GraphQL API的示例。

<template>
  <div :class="$style.container">
    <h1>Star wars films</h1>
    <ul>
      <li v-for="film in films" :key="film.episodeID">
        {{ film.title }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}
interface ResponseData {
  allFilms: FilmConnection
}
interface Response {
  data: ResponseData
}

// Vueのdataに型をつける
interface VueData {
  films: Film[]
}

export default Vue.extend({
  data(): VueData {
    return {
      films: []
    }
  },
  async created() {
    const query = `
{
  allFilms(first: 3) {
    edges {
      node {
        episodeID
        title
      }
    }
  }
}
`
    const res = await fetch(
      'https://swapi-graphql.netlify.com/.netlify/functions/index',
      {
        method: 'POST',
        body: JSON.stringify({
          query
        }),
        headers: {
          'Content-Type': 'application/json'
        }
      }
    ).then<Response>((res) => res.json())

    this.films = res.data.allFilms.edges.map((e) => e.node)
  }
})
</script>

<style module>
.container {
  margin: 10px;
}
</style>

如果成功的话,将显示如下所示的标题列表。

Screen Shot 2020-03-25 at 1.45.08.png

然而,这段代码还存在一些令人遗憾的地方。由于fetch是浏览器的API,因此在SSR时尝试获取数据会导致错误(将created更改为asyncData,可以再现SSR时的错误)。

在解决这个问题时,有一个方案是使用node-fetch,但在这里我们尝试使用Nuxt的apollo-module来解决。

安装apollo-module

首先,按照官方安装指南,安装apollo-module模块。

yarn add @nuxtjs/apollo graphql-tag

在nuxt的配置文件中加载模块,并写入最基本的配置。

  modules: ['@nuxtjs/apollo'],
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint:
          'https://swapi-graphql.netlify.com/.netlify/functions/index'
      }
    }
  },

在这种情况下,如果将pages/index.vue的script改写如下,就可以使用Apollo来获取数据了!

<script>
import gql from 'graphql-tag'

export default {
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
}
</script>

在Vue组件的定义中,添加一个名为apollo的属性,在这里编写与GraphQL API相关的查询,响应将合并到data中。

TypeScript的类型定义

目前,我們可以通過 Apollo 來獲取數據,但是它並沒有 TypeScript 的類型支持。因此,當我們嘗試使用 lang=”ts” 和 Vue.extend() 為其添加類型時,會出現編譯錯誤。

<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

export default Vue.extend({
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>

编译错误:

No overload matches this call.
  The last overload gave the following error.
    Argument of type '{ computed: { films(): any; }; apollo: { allFilms: DocumentNode; }; }' is not assignable to parameter of type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.
      Object literal may only specify known properties, and 'apollo' does not exist in type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.

这是因为在Vue的Options API中不存在apollo字段。因此,让我们修改tsconfig.json文件,以便在Vue的Options API中可以定义apollo。

  "compilerOptions": {
    "types": [
      "@types/node",
      "@nuxt/types",
+     "vue-apollo"
    ]
  }

通过这样做,当TypeScript进行编译时,将会加载Vue Apollo所定义的Apollo类型定义,从而赋予Apollo类型。

只要修正一下this.allFilms的类型,编译就能通过了。

<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}

// Vueのdataに型をつける
interface Data {
  allFilms: FilmConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: {
        edges: []
      }
    }
  },
  computed: {
    films(): Film[] {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
...

从模式生成响应类型

目前,我们手动编写像FilmConnection这样的类型,但希望能从GraphQL的模式中自动生成这些类型。因此,首先从以下网址获取模式。

将这个文件以 `schema.graphql` 的名称放置在项目中。接下来,使用 GraphQL Code Generator 从此模式生成 TypeScript 类型。

首先需要安装库。

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript

接下来是配置文件。

overwrite: true
schema: "schema.graphql"
generates:
  lib/GraphQL/generated.ts:
    plugins:
      - "typescript"

当准备好后,输入命令。

yarn run graphql-codegen --config codegen.yml

如果顺利的话,应该能够得到以下类似的文件。

export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};
...

只需要将这个导入并使用即可。因为类型定义稍有变化,所以代码也进行了相应的修改。

<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'
import { FilmsConnection, Film } from '~/lib/GraphQL/generated'

interface Data {
  allFilms?: FilmsConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: undefined
    }
  },
  computed: {
    films(): Film[] {
      if (this.allFilms == null || this.allFilms.edges == null) return []
      return this.allFilms.edges
        .map((e) => e?.node)
        .filter((f): f is Film => f != null)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>

通过这个方法,lang=”ts” 的Vue组件现在可以使用apollo,并且还能给GraphQL的响应加上类型。

广告
将在 10 秒后关闭
bannerAds