Vue-与-GraphQL-应用构建指南-三-

55 阅读33分钟

Vue 与 GraphQL 应用构建指南(三)

原文:zh.annas-archive.org/md5/60CC414A1AE322EC97E6A0F8A5BBE3AD

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:创建 Chat 和 Message Vuex、页面和路由

在本章中,我们将完成应用程序并创建最终部分。本章将完成应用程序的开发,使其准备好为部署创建最终产品。

在这里,您将学习如何创建 GraphQL 查询和片段,创建 Chat Vuex 模块和业务规则,创建联系人页面和页面中使用的组件,最后创建消息页面和创建页面所需的组件。

在本章中,我们将涵盖以下食谱:

  • 创建 GraphQL 查询和片段

  • 在您的应用程序上创建 Chat Vuex 模块

  • 创建应用程序的联系人页面

  • 创建应用程序的消息页面

技术要求

在本章中,我们将使用 Node.js,AWS Amplify 和 Quasar Framework。

注意,Windows 用户!您需要安装一个名为windows-build-toolsnpm包,以便能够安装所需的包。要执行此操作,请以管理员身份打开 PowerShell 并执行以下命令:

> npm install -g windows-build-tools

要安装Quasar Framework,您需要打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:

> npm install -g @quasar/cli

要安装AWS Amplify,您需要打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:

> npm install -g @aws-amplify/cli

创建 GraphQL 查询和片段

在 GraphQL 中,可以创建一个简单的查询来获取您想要的数据。通过这样做,您的代码可以减少用户网络和处理能力的使用。这种技术也被称为片段

在这个食谱中,我们将学习如何创建 GraphQL 片段并在我们的应用程序中使用它们。

准备工作

这个食谱的先决条件如下:

  • 在第五章的食谱为您的应用程序创建用户页面和路由中的项目,创建用户 Vuex 模块、页面和路由

  • Node.js 12+

所需的 Node.js 全局对象如下:

  • @aws-amplify/cli

  • @quasar/cli

要启动我们将在应用程序中使用的 GraphQL 片段,我们将继续使用我们在第五章中创建的项目,创建用户 Vuex 模块、页面和路由。

如何做...

在这个配方中,我们将创建应用程序中所需的片段,并用这里创建的片段替换我们在上一个配方中编写的一些代码。

创建 GraphQL 片段

在这里,我们将创建我们在应用程序中将使用的所有片段:

  1. src/graphql文件夹中创建一个名为fragments.js的文件并打开它。

  2. 然后,我们需要导入graphql语言解释器:

import graphql from 'graphql-tag';
  1. 让我们创建getUser片段来获取用户信息。这个片段将获取用户的基本信息。首先,我们需要启动graphql解释器,然后传递带有我们查询的模板文字字符串。使用getUser查询作为基本查询,我们将创建一个只包含我们想要从服务器获取的数据的查询模式:
const getUser = graphql`
 query getUser($id: ID!) {
 getUser(id: $id) {
 id username avatar { bucket key region } email name } } `;

ES2015 规范中的模板文字提供了一个称为标记模板或标记函数的新功能。这些用于在使用附加到它的字符串之前预处理模板文字上的字符串。

  1. 然后我们将创建listUsers片段来获取应用程序中的所有用户。这个片段将使用从 AWS Amplify 创建的基本查询中的listUsers查询。然后它将返回我们应用程序中所有当前用户的基本信息:
const listUsers = graphql`
 query listUsers { listUsers { items { id username name createdAt avatar { bucket region key } } } } `;
  1. 为了完成用户片段,我们将创建getUserAndConversations片段来获取用户的基本信息和他们最近的 10 次对话。这个片段基于GetUser查询:
const getUserAndConversations = graphql`
 query getUserAndConversations($id:ID!) {
 getUser(id:$id) {
 id username conversations(limit: 10) {
 items { id conversation { id name associated { items { user { id name email avatar { bucket key region } } } } } } } } } `;
  1. 为了获取用户对话,我们将创建一个名为getConversation的片段,基于GetConversation查询,从当前对话 ID 的用户那里获取最后 1,000 条消息和对话成员:
const getConversation = graphql`
 query GetConversation($id: ID!) {  getConversation(id:$id) {  id name members messages(limit: 1000) {  items {  id content author {  name avatar {  bucket key region  }
 }  authorId messageConversationId createdAt  }
 }  createdAt updatedAt  }
 } `;
  1. 要在我们的 API 中创建新的消息,我们需要创建一个名为createMessage的片段。这个片段基于CreateMessage变异。片段将接收idauthorIdcontentmessageConversationIdcreatedAt
const createMessage = graphql`mutation CreateMessage(
  $id: ID,
  $authorId: String,
  $content: String!,
  $messageConversationId: ID!
  $createdAt: String, ) {  createMessage(input: {
  id: $id,
  authorId: $authorId
 content: $content,
  messageConversationId: $messageConversationId,
  createdAt: $createdAt,
  }) {  id authorId content messageConversationId createdAt  } } `;
  1. 要在两个用户之间开始新的对话,我们需要创建一个名为createConversation的新片段。这个片段基于CreateConversation变异;它将接收对话的name和正在创建的对话的members列表:
const createConversation = graphql`mutation CreateConversation($name: String!, $members: [String!]!) {  createConversation(input: {
  name: $name, members: $members
  }) {  id name members  } } `;
  1. 然后,我们将使用基于 CreateConversationLink 变异的 createConversationLink 片段完成我们的片段。此片段将链接在我们的应用程序中创建的对话并生成唯一 ID。为使其工作,此片段需要接收 conversationLinkConversationId 和 conversationLinkUserId:
const createConversationLink = graphql`mutation CreateConversationLink(
  $conversationLinkConversationId: ID!,
  $conversationLinkUserId: ID ) {  createConversationLink(input: {
  conversationLinkConversationId: $conversationLinkConversationId,
  conversationLinkUserId: $conversationLinkUserId
  }) {  id conversationLinkUserId conversationLinkConversationId conversation {  id name  }
 } } `;
  1. 最后,我们将导出我们创建的所有片段到 JavaScript 对象中:
export {
  getUser,
  listUsers,
  getUserAndConversations,
  getConversation,
  createMessage,
  createConversation,
  createConversationLink, };  

将片段应用于 User Vuex 操作

现在我们可以更新 User Vuex 操作以使用我们创建的片段:

  1. 在 store/user 文件夹中打开 actions.js 文件。

  2. import部分,我们将从 src/graphql/queries 替换 getUser 和 listUsers 为新创建的 src/graphql/fragments。

import { listUsers, getUser } from 'src/graphql/fragments';

它是如何工作的...

使用 GraphQL 查询语言,我们能够创建小查询和变异,称为片段,可以执行原始查询或变异的部分,并返回相同的响应,但包含我们请求的数据。

通过这样做,我们的应用程序数据使用量减少了,遍历数据的处理能力也减少了。

GraphQL 片段与作为基础的查询或变异相同。这是因为 GraphQL 使用相同的模式、查询和变异作为基础。通过这样做,您可以在搜索和变异中使用在查询或变异中声明的相同变量。

因为我们在替换 User Vuex 操作中导入的代码时使用了相同的名称作为基础查询,所以我们不需要更改任何内容,因为请求的结果将与旧的结果相同。

另请参阅

在您的应用程序上创建 Chat Vuex 模块

要创建聊天应用程序,我们需要为应用程序的聊天部分创建自定义业务规则。这部分将包含获取新消息、发送消息和在用户之间开始新对话的所有逻辑。

在这个教程中,我们将在应用程序的 Vuex 中创建 Chat 模块,其中我们将存储已登录用户和其他用户之间的所有消息,获取新消息,发送新消息,并开始新对话。

准备工作

此教程的先决条件如下:

  • 来自上一个教程的项目

  • Node.js 12+

所需的 Node.js 全局对象如下:

  • @aws-amplify/cli

  • @quasar/cli

为了开始我们的 Chat Vuex 模块,我们将继续使用在 创建 GraphQL 查询和片段 教程中创建的项目。

如何做...

为了创建 Chat Vuex 模块,我们将把任务分成五个部分:创建 statemutationsgettersactions,然后将模块添加到 Vuex。

创建 Chat Vuex 状态

为了在 Vuex 模块上存储数据,我们需要一个具有存储数据的状态。在这里,我们将创建 Chat 状态:

  1. 在 store 文件夹中创建一个名为 chat 的新文件夹,然后创建一个名为 state.js 的新文件,并打开它。

  2. 创建一个名为 createState 的新函数,它返回一个具有 conversationsmessagesloadingerror 属性的 JavaScript 对象。conversationsmessages 属性将被定义为空数组,loading 属性将被定义为 falseerrorundefined

export function createState() {
  return {
  conversations: [],
  messages: [],
  loading: false,
  error: undefined,
  }; }
  1. 最后,为了将状态导出为单例,并将其作为 JavaScript 对象可用,我们需要将 createState 函数的执行导出为默认值:
export default createState();

创建 Chat Vuex mutations

现在要在状态上保存任何数据,Vuex 需要一个 mutation。为此,我们将创建 Chat mutation,用于管理此模块的 mutations:

  1. 在 store/chat 文件夹中创建一个名为 types.js 的新文件,并打开它。

  2. 在文件中,导出一个默认的 JavaScript 对象,其属性与字符串的值相同。属性将是 SET_CONVERSATIONSSET_MESSAGESLOADINGERROR

export default {
  SET_CONVERSATIONS: 'SET_CONVERSATIONS',
  SET_MESSAGES: 'SET_MESSAGES',
  LOADING: 'LOADING',
  ERROR: 'ERROR', };
  1. 在 store/chat 文件夹中创建一个名为 mutations.js 的新文件,并打开它。

  2. 导入新创建的 types.js 文件:

import MT from './types';
  1. 创建一个名为 setLoading 的新函数,以 state 作为第一个参数。在其中,我们将定义 state.loadingtrue
function setLoading(state) {
 state.loading = true; }
  1. 创建一个名为 setError 的新函数,以 state 作为第一个参数,error 作为第二个参数,其默认值为 new Error()。在其中,我们将定义 state.errorerror,并将 state.loading 定义为 false
function setError(state, error = new Error()) {
 state.error = error;
  state.loading = false; }
  1. 创建一个名为setConversations的新函数,第一个参数是state,第二个参数是 JavaScript 对象,具有items属性。通过这样做,我们将使用接收到的数组定义状态对话:
function setConversations(state, payload) {
 state.conversations = payload.items;
  state.loading = false; }
  1. 创建一个名为setMessages的新函数,第一个参数是state,第二个参数是 JavaScript 对象。在这个函数中,我们将尝试查找是否有与payload中接收到的id相等的消息,并将消息添加到状态中:
function setMessages(state, payload) {
  const messageIndex = state.messages.findIndex(m => m.id === 
   payload.id);    if (messageIndex === -1) {
 state.messages.push(payload);
  } else {
 state.messages[messageIndex].messages.items = payload.messages.items;
  }
 state.loading = false; }
  1. 最后,导出一个默认的 JavaScript 对象,其中键是导入的 mutation 类型,值是对应于每种类型的函数:
  • MT.LOADING定义为setLoading

  • MT.ERROR定义为setError

  • MT.SET_CONVERSATION定义为setConversations

  • MT.SET_MESSAGES定义为setMessages

export default {
  [MT.LOADING]: setLoading,
  [MT.ERROR]: setError,
  [MT.SET_CONVERSATIONS]: setConversations,
  [MT.SET_MESSAGES]: setMessages, };

创建 Chat Vuex getters

访问存储在状态中的数据,我们需要创建getters。在这里,我们将为 Chat 模块创建getters

getter函数中,该函数将接收的第一个参数始终是 Vuex store的当前state

  1. store/chat文件夹中创建一个名为getters.js的新文件。

  2. 创建一个名为getConversations的新函数。该函数首先接收state_getters_rootStaterootGetters作为柯里化函数的第一部分。最后,它将返回用户和应用程序中另一个用户之间的对话的筛选列表:

const getConversations = (state, _getters, _rootState, rootGetters) => {
  const { conversations } = state;
  return conversations
  .reduce((acc, curr) => {
  const { conversation } = curr;    const user = rootGetters['user/getUser'].id;    const users = conversation
  .associated
        .items
        .reduce((a, c) => [...a, { ...c.user, conversation: 
           conversation.id }], [])
  .filter(u => u.id !== user);    return [...acc, users];
  }, [])
  .flat(Infinity); };

_variable(下划线变量)是 JavaScript 中用于指示创建的函数可以具有这些参数,但目前不会使用它们的技术。在我们的情况下,Vuex getters API 始终执行每个 getter 调用,传递stategettersrootStaterootGetters,因为根据 linter 规则,我们为未使用的参数添加了下划线。

  1. 创建一个名为getChatMessages的新函数,这是一个使用方法调用的 getter。首先,我们传递state,然后返回一个接收convId的函数。最后,它将返回该对话 ID 的消息列表:
const getChatMessages = (state) => (convId)  => (state.messages.length ? state.messages
  .find(m => m.id === convId).messages.items : []);
  1. 创建一个名为isLoading的新函数,返回state.loading
const isLoading = (state) => state.loading;
  1. 创建一个名为hasError的新函数,返回state.error
const hasError = (state) => state.error;
  1. 最后,导出一个默认的 JavaScript 对象,其中包含创建的函数作为属性:getConversationsgetChatMessagesisLoadinghasError
export default {
  getConversations,
  getChatMessages,
  isLoading,
  hasError, };  

创建 Chat Vuex actions

在这里,我们将创建 Chat 模块的 Vuex actions:

  1. store/chat文件夹中创建一个名为actions.js的文件,并打开它。

  2. 首先,我们需要导入在这部分中要使用的函数、枚举和类:

  • aws-amplify包中导入graphqlOperation

  • src/graphql/fragments.js导入getUserAndConversationscreateConversationcreateConversationLinkcreateMessagegetConversation

  • driver/auth.js导入getCurrentAuthUser函数。

  • driver/appsync导入AuthAPI

  • ./types.js导入 Vuex 变异类型:

import { graphqlOperation } from 'aws-amplify';
import {
  getUserAndConversations,
  createConversation,
  createConversationLink,
  createMessage,
  getConversation,
} from 'src/graphql/fragments';
import {
  getCurrentAuthUser,
} from 'src/driver/auth';
import { uid } from 'quasar';
import { AuthAPI } from 'src/driver/appsync';
import MT from './types';
  1. 创建一个名为newConversation的异步函数。在第一个参数中,我们将添加_vuex,并使用一个 JavaScript 对象作为第二个参数,接收authorIdotherUserId作为属性。在这个函数中,我们将根据接收到的载荷创建一个新的对话。然后我们需要创建对话和对话中用户之间的关系。最后,我们返回对话的 ID 和名称:
async function newConversation(_vuex, { authorId, otherUserId }) {
  try {
  const members = [authorId, otherUserId];    const conversationName = members.join(' and ');    const {
    data: {
      createConversation: {
        id: conversationLinkConversationId,
      },
    },
  } = await AuthAPI.graphql(
    graphqlOperation(createConversation,
      {
        name: conversationName,
        members,
      }),
  );    const relation = { conversationLinkConversationId };    await Promise.all([
    AuthAPI.graphql(
      graphqlOperation(createConversationLink, {
        ...relation,
        conversationLinkUserId: authorId,
      }),
    ),
    AuthAPI.graphql(
      graphqlOperation(createConversationLink, {
        ...relation,
        conversationLinkUserId: otherUserId,
      }),
    )]);    return Promise.resolve({
    id: conversationLinkConversationId,
    name: conversationName,
  });
  } catch (e) {
  return Promise.reject(e);
  } }
  1. 为了向用户发送新消息,我们需要创建一个名为newMessage的异步函数。这个函数将在第一个参数中接收一个解构的 JavaScript 对象,其中包含commit变量,并作为第二个参数,另一个解构的 JavaScript 对象,其中包含messageconversationId属性。然后,在函数中,我们需要获取用户的username并返回 GraphQL 的createMessage变异,传递变量,其中id定义为uid()authorID定义为usernamecontent定义为messagemessageConversationId定义为conversationIdcreatedAt定义为Date.now()
async function newMessage({ commit }, { message, conversationId }) {
  try {
  commit(MT.LOADING);    const { username } = await getCurrentAuthUser();    return AuthAPI.graphql(graphqlOperation(
    createMessage,
    {
      id: uid(),
      authorId: username,
      content: message,
      messageConversationId: conversationId,
      createdAt: Date.now(),
    },
  ));
  } catch (e) {
  return Promise.reject(e);
  } finally {
  commit(MT.LOADING);
  } }
  1. 为了获取初始用户消息,我们需要创建一个名为getMessages的异步函数。这个函数将在第一个参数中接收一个解构的 JavaScript 对象,其中包含commit变量。在这个函数内部,我们需要获取经过身份验证的用户的id,然后执行 GraphQL 的getUserAndConversations变异来获取所有当前用户的conversations,将它们传递给变异,并返回它们:
async function getMessages({ commit }) {
  try {
  commit(MT.LOADING);    const { id } = await getCurrentAuthUser();    const {
    data: {
      getUser: {
        conversations,
      },
    },
  } = await AuthAPI.graphql(graphqlOperation(
    getUserAndConversations,
    {
      id,
    },
  ));    commit(MT.SET_CONVERSATIONS, conversations);    return Promise.resolve(conversations);
  } catch (err) {
  commit(MT.ERROR, err);
  return Promise.reject(err);
  } }
  1. 然后我们需要完成聊天操作,创建fetchNewMessages函数。这个异步函数将在第一个参数中接收一个解构的 JavaScript 对象,其中包含commit变量,第二个参数包含conversationId属性。在这个函数中,我们将使用 GraphQL 的getConversation查询通过传递对话 ID 来获取对话中的消息。最后,接收到的消息数组将通过 Vuex 的SET_MESSAGESmutation 添加到状态中,并返回true
async function fetchNewMessages({ commit }, { conversationId }) {
  try {
  commit(MT.LOADING);    const { data } = await AuthAPI.graphql(graphqlOperation(
    getConversation,
    {
      id: conversationId,
    },
  ));    commit(MT.SET_MESSAGES, data.getConversation);    return Promise.resolve(true);
  } catch (e) {
  return Promise.reject(e);
  } }
  1. 最后,我们将导出所有创建的函数:
export default {
  newConversation,
  newMessage,
  getMessages,
  fetchNewMessages, }; 

将 Chat 模块添加到 Vuex

现在我们将 Chat 模块导入到 Vuex 状态管理中:

  1. store/chat文件夹中创建一个名为index.js的新文件。

  2. 导入我们刚刚创建的state.jsactions.jsmutation.jsgetters.js文件:

import state from './state'; import actions from './actions'; import mutations from './mutations'; import getters from './getters';
  1. 创建一个带有 JavaScript 对象的export default,其中属性为stateactionsmutationsgettersnamespaced(定义为true):
export default {
  namespaced: true,
  state,
  actions,
  mutations,
  getters, };  
  1. 打开store文件夹中的index.js文件。

  2. 将新创建的index.js文件导入到store/chat文件夹中:

import Vue from 'vue'; import Vuex from 'vuex'; import user from './user';
import chat form './chat';
  1. 在创建 Vuex 存储时,添加一个名为modules的新属性,并将导入的用户文件添加到此属性中:
export default function (/* { ssrContext } */) {
  const Store = new Vuex.Store({
  modules: {
  user,
      chat,
  },
  strict: process.env.DEV,
  });    return Store; }  

它是如何工作的...

在这个示例中,我们创建了 Chat Vuex 模块。该模块包括了管理应用程序内对话和消息所需的所有业务逻辑。

在 Vuex 操作中,我们使用了AppSync API Driver和 GraphQL 片段来创建新的对话和消息,并在 API 上获取它们。在获取后,所有消息和对话都通过 Vuex mutations 存储在 Vuex 状态中。

最后,所有数据都可以通过 Vuex getter 访问到。getter 被开发为柯里化函数,因此在执行时可以访问状态并在其中进行搜索,以获取对话消息,并使用完整的 API 获取用户对话。

另请参阅

创建应用程序的联系人页面

在聊天应用程序中,通常会有一个起始页面,用户可以从旧对话中选择继续发送消息,或者开始新的对话。这种做法可以作为应用程序的主页面。在我们的应用程序中,也不会有所不同。

在这个示例中,我们将创建一个联系人页面,用户可以使用它来开始对话或继续旧对话。

准备工作

这个示例的先决条件如下:

  • 来自上一个示例的项目

  • Node.js 12+

所需的 Node.js 全局对象如下:

  • @aws-amplify/cli

  • @quasar/cli

要开始我们的用户联系人页面,我们将继续使用在在应用程序中创建 Chat Vuex 模块示例中创建的项目。

操作步骤...

在这个示例中,我们需要将我们的工作分为两部分:首先是一个新的组件来开始新的对话,最后是联系人页面本身。

创建 NewConversation 组件

首先,我们需要创建一个组件,在应用程序中的用户和另一个用户之间开始新的对话。

单文件组件

在这里,我们将创建组件的<script>部分:

  1. src/components文件夹中创建一个名为NewConversation.vue的新文件并打开它。

  2. vuex中导入mapActionsmapGetters

import { mapActions, mapGetters } from 'vuex';
  1. 导出一个带有七个属性的defaultJavaScript 对象:namepropsdatawatchcomputedmethods
export default {
  name: 'NewConversation',
  components: {},
  props: {},
  data: () => ({}),
  watch: {},
  computed: {},
  methods: {},
};
  1. components属性中,将AvatarDisplay组件导入为 lazyload 组件:
components: {
  AvatarDisplay: () => import('components/AvatarDisplay'), },
  1. props属性中,我们将添加一个名为value的新属性,类型为Boolean,默认值为false
props: {
  value: {
  type: Boolean,
  default: false,
  }, },
  1. data属性上,我们需要定义两个属性:userList作为一个数组,pending作为一个布尔值,定义为false
data: () => ({
  userList: [],
  pending: false, }),
  1. methods属性中,首先,我们将从用户模块中解构mapActions调用listAllUsers函数。然后我们将对聊天模块做同样的操作,调用newConversation函数。现在我们将创建一个名为fetchUser的异步函数,设置组件为pending,获取所有用户,并将userList设置为过滤掉当前用户的响应。最后,我们需要创建一个名为createConversation的异步函数,它接收一个otherUserId参数,创建一个新的对话,并将用户重定向到消息页面:
methods: {
  ...mapActions('user', ['listAllUsers']),
  ...mapActions('chat', ['newConversation']),
  async fetchUsers() {
  this.pending = true;
  try {
  const users = await this.listAllUsers();
  this.userList = users.filter((u) => u.id !== this.getUser.id);
  } catch (e) {
  this.$q.dialog({
  message: e.message,
  });
  } finally {
  this.pending = false;
  }
 },
  async createConversation(otherUserId) {
  try {
  const conversation = await this.newConversation({
  authorId: this.getUser.id,
 otherUserId,
  });
 await this.$router.push({
  name: 'Messages',
  params: conversation,
  });
  } catch (e) {
  this.$q.dialog({
  message: e.message,
  });
  }
 }, },
  1. computed属性上,首先,我们将从用户模块调用getUser解构mapGetters。然后我们将对聊天模块的getConversations做同样的操作。现在我们将创建一个名为contactList的函数,它返回当前userList,并通过当前用户已经开始对话的用户进行筛选:
computed: {
  ...mapGetters('user', ['getUser']),
  ...mapGetters('chat', ['getConversations']),
  contactList() {
  return this.userList
      .filter((user) => this.getConversations
        .findIndex((u) => u.id === user.id) === -1);
  }, },
  1. 最后,在watch属性上,我们将添加一个名为value的异步函数,它接收一个名为newVal的参数。这个函数检查newVal的值是否为true;如果是,它将在 API 中获取用户列表:
watch: {
  async value(newVal) {
  if (newVal) {
  await this.fetchUsers();
  }
 }, },
单文件组件的