六、创建聊天和消息 Vuex、页面和路由

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

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

在本章中,我们将介绍以下配方:

  • 创建 GraphQL 查询和片段
  • 在应用上创建聊天室 Vuex 模块
  • 创建应用的联系人页面
  • 创建应用的消息页面

技术要求

在本章中,我们将使用Node.jsAWS Amplify类星体框架

**Attention, Windows users! You need to install an npm package called windows-build-tools to be able to install the required packages. To do it, open PowerShell as an administrator and execute the following command: > npm install -g windows-build-tools

要安装Quasar Framework,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:

> npm install -g @quasar/cli

要安装AWS Amplify您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:

> npm install -g @aws-amplify/cli

创建 GraphQL 查询和片段

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

在这个配方中,我们将学习如何创建 GraphQL 片段并在应用中使用它们。

准备

此配方的先决条件如下:

  • 来自第 5 章为您的应用创建用户页面和路由配方的项目创建用户 Vuex 模块、页面和路由
  • Node.js 12+

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

  • @aws-amplify/cli
  • @quasar/cli

为了启动将在应用上使用的 GraphQL 片段,我们将继续在第 5 章中创建的项目,创建用户 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
    }
  }
`;

The template literal in the ES2015 specification provides a new feature called tagged templates or tag functions. Those are used to pre-process the string on the template literal before using the string that is attached to it.

  1. 然后我们将创建listUsers片段来获取应用中的所有用户。此片段将使用来自 AWS Amplify 创建的基本查询的listUsers查询。然后,它将返回应用中的所有当前用户及其基本信息:
const listUsers = graphql`
  query listUsers {
    listUsers {
      items {
        id
        username
        name
        createdAt
        avatar {
          bucket
          region
          key
        }
      }
    }
  }
`;
  1. 我们将创建用户的基本对话片段,最后获取用户的对话片段。此片段基于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 中的用户处获取最后 1000 条消息和对话成员:
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。要使其工作,此片段需要接收conversationLinkConversationIdconversationLinkUserId
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,
};

在用户 Vuex 操作上应用片段

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

  1. 打开store/user文件夹中的actions.js文件。
  2. import部分,我们将src/graphql/queries中的getUserlistUsers替换为新创建的src/graphql/fragments
import { listUsers, getUser } from 'src/graphql/fragments';

它是如何工作的。。。

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

通过这样做,我们的应用中的数据使用量减少了,迭代数据的处理能力也降低了。

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

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

另见

在应用上创建聊天室 Vuex 模块

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

在此配方中,我们将在应用 Vuex 中创建聊天模块,在其中存储登录用户和其他用户之间的所有消息,获取新消息,发送新消息,并启动新对话。

准备

此配方的先决条件如下:

  • 上一个配方中的项目
  • Node.js 12+

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

  • @aws-amplify/cli
  • @quasar/cli

为了启动聊天 Vuex 模块,我们将继续在创建 GraphQL 查询和片段配方中创建的项目。

怎么做。。。

为了创建聊天 Vuex 模块,我们将任务分为五个部分:创建状态突变获取者动作,然后将模块添加到 Vuex。

创建聊天室 Vuex 状态

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

  1. store文件夹中新建一个名为chat的文件夹,然后新建一个名为state.js的文件,并将其打开。
  2. 创建一个名为createState的新函数,该函数返回一个属性为conversationsmessagesloadingerror的 JavaScript 对象。conversationsmessages属性将定义为空数组,loading属性将定义为falseerror属性将定义为undefined
export function createState() {
  return {
    conversations: [],
    messages: [],
    loading: false,
    error: undefined,
  };
}
  1. 最后,为了将状态导出为单例,并使其作为 JavaScript 对象可用,我们需要export default执行createState函数:
export default createState();

创建聊天室 Vuex

现在,要保存状态的任何数据,Vuex 需要进行变异。为此,我们将创建聊天变异,该变异将管理此模块的变异:

  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.loading定义为true
function setLoading(state) {
  state.loading = true;
}
  1. 创建一个名为setError的新函数,第一个参数为state,第二个参数为error,默认值为new Error()。我们将state.error定义为errorstate.loadingfalse
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等于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 对象,其中键为导入的变异类型,值为对应于每种类型的函数:

  2. MT.LOADING定义为setLoading

  3. MT.ERROR定义为setError
  4. MT.SET_CONVERSATION定义为setConversations
  5. MT.SET_MESSAGES定义为setMessages
export default {
  [MT.LOADING]: setLoading,
  [MT.ERROR]: setError,
  [MT.SET_CONVERSATIONS]: setConversations,
  [MT.SET_MESSAGES]: setMessages,
};

创建聊天室 Vuex getter

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

In a getter function, the first argument that that function will receive will always be the current state of the Vuex store.

  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 (underscore variable) is a technique used in JavaScript to indicate that the function created can have those arguments, but it won't use them for now. In our case, the Vuex getters API always executes every getter call passing state, getters, rootState, and rootGetters, because with the linter rule, we added underscores to the unused arguments.

  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. 最后,导出一个defaultJavaScript 对象,将创建的函数作为属性:getConversationsgetChatMessagesisLoadinghasError
export default {
  getConversations,
  getChatMessages,
  isLoading,
  hasError,
};

创建聊天室 Vuex 操作

在这里,我们将创建聊天模块的 Vuex 操作:

  1. store/chat文件夹中创建一个名为actions.js的文件,并将其打开。
  2. 首先,我们需要导入此部分中要使用的函数、枚举和类:

  3. aws-amplify包中导入graphqlOperation

  4. src/graphql/fragments.js进口getUserAndConversationscreateConversationcreateConversationLinkcreateMessagegetConversation
  5. driver/auth.js导入getCurrentAuthUser功能。
  6. driver/appsync导入AuthAPI
  7. ./types.js导入 Vuex 突变类型:
import { graphqlOperation } from 'aws-amplify';
import {
  getUserAndConversations,
  createConversation,
  createConversationLink,
  createMessage,
  getConversation,
} from 'src/graphql/fragments';
import {
  getCurrentAuthUser,
} from '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. 为了向用户发送新消息,我们需要创建一个名为newMessageasynchronous函数。此函数将在第一个参数中接收一个带commit变量的解构 JavaScript 对象,作为第二个参数,接收另一个带messageconversationId属性的解构 JavaScript 对象。然后在函数中,我们需要获取用户的username并返回 GraphQLcreateMessage突变,传递变量,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,然后执行 GraphQLgetUserAndConversations变异,获取当前所有用户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属性。在这个函数中,我们将使用 GraphQLgetConversation查询通过传递会话 ID 来获取会话中的消息。最后,接收到的消息数组将通过 VuexSET_MESSAGES变异添加到状态中,并返回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,
};

将聊天模块添加到 Vuex

现在,我们将创建的聊天模块导入 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的新属性,并将其定义为 JavaScript 对象。然后将导入的用户文件添加到此属性:
export default function (/* { ssrContext } */) {
  const Store = new Vuex.Store({
    modules: {
      user,
      chat,
    },
    strict: process.env.DEV,
  });

  return Store;
}

它是如何工作的。。。

在此配方中,我们创建了聊天室 Vuex 模块。此模块包括管理应用内的对话和消息所需的所有业务逻辑。

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

最后,用户可以通过 Vuex getter 访问所有数据。getter 是作为一个 currying 函数开发的,因此在执行它获取会话消息时,可以访问状态并在其中执行搜索,并使用完整的 API 获取用户会话。

另见

创建应用的联系人页面

在聊天应用中,通常会有一个起始页,用户可以在其中从旧对话中选择继续发送消息,或启动新对话。此实践可用作应用的主页。在我们的应用中,不会有什么不同。

在此配方中,我们将创建一个联系人页面,用户可以使用该页面开始对话或继续旧的对话。

准备

此配方的先决条件如下:

  • 上一个配方中的项目
  • Node.js 12+

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

  • @aws-amplify/cli
  • @quasar/cli

要启动我们的用户联系页面,我们将继续在中创建的项目,该项目是在您的应用配方上创建聊天 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属性中,首先,我们将从调用listAllUsers函数的用户模块解构mapActions。然后我们将对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();
    }
  },
},

单文件组件

现在让我们为NewConversation组件创建<template>部分:

  1. 创建一个QDialog组件,其value属性定义为value。同时创建定义为$emit函数的事件监听器input,以$event作为数据发送'input'事件:
<q-dialog
  :value="value"
  @input="$emit('input', $event)"
></q-dialog>
  1. QDialog组件内部,创建一个QCard组件,其style属性定义为min-width: 400px; min-height: 100px;。在QCard组件内部,创建两个QCardSection子组件。在第一个组件中,添加定义为row items-center q-pb-noneclass属性:
<q-card
  style="min-width: 400px; min-height: 100px"
>
  <q-card-section class="row items-center q-pb-none">
     </q-card-section>
  <q-card-section></q-card-section>
</q-card>
  1. 在第一个QCardSection组件上,添加一个div,其class属性为text-h6,内部 HTML 为New Conversation。然后添加一个QSpace组件。最后,添加QBtn,其中icon属性为closeflatrounddense属性为true,并添加v-close-popup指令:
<q-card-section class="row items-center q-pb-none">
  <div class="text-h6">New Conversation</div>
  <q-space/>
  <q-btn icon="close" flat round dense v-close-popup/>
</q-card-section>
  1. 在第二个QCardSection组件中,创建一个具有QItem子级的QList组件。在QItem子组件中,添加v-for指令以迭代contactList。然后将key变量属性定义为contact.idclass属性定义为q-my-smclickable属性定义为true添加v-ripple指令。最后,在click事件上添加一个事件监听器,调度createConversation方法并发送contact.id作为参数:
<q-list>
  <q-item
    v-for="contact in contactList"
    :key="contact.id"
    class="q-my-sm"
    clickable
    v-ripple
    @click="createConversation(contact.id)"
  ></q-item>
</q-list>
  1. QItem组件内部,创建一个QItemSection组件,其avatar属性定义为true。然后创建一个QAvatar组件作为子组件,创建一个AvatarDisplay组件作为QAvatar的子组件。在AvatarDisplay组件上,添加avatar-object动态属性contact.avatarname动态属性contact.name
<q-item-section avatar>
  <q-avatar>
    <avatar-display
      :avatar-object="contact.avatar"
      :name="contact.name"
    />
  </q-avatar>
</q-item-section>
  1. 在第一个QItemSection组件之后,创建另一个QItemSection作为同级元素。在此QItemSection中,添加两个QItemLabel组件。第一个添加contact.name作为内部 HTML,第二个添加caption属性为truelines属性为1,内部 HTML 为contact.email
<q-item-section>
  <q-item-label>{{ contact.name }}</q-item-label>
  <q-item-label caption lines="1">{{ contact.email }}</q-item-label>
</q-item-section>
  1. 然后创建另一个QItemSection组件作为第三个同级,其side属性为true。在其内部添加一个QIcon组件,其name属性为add_commentcolor属性为green
<q-item-section side>
 <q-icon name="add_comment" color="green"/>
</q-item-section>
  1. 最后,作为QList组件的兄弟,创建一个QInnerLoading组件,其showing属性定义为pending。在其内部添加一个QSpinner组件,其size属性为50px,而color属性定义为primary
<q-inner-loading
  :showing="pending">
  <q-spinner
    size="50px"
    color="primary"/>
</q-inner-loading>

以下是组件的渲染版本:

创建联系人页面

现在是创建联系人页面的时候了。此页面将是认证用户应用的初始页面。在这里,用户将能够进入用户更新页面,输入并恢复旧对话,或创建新对话。

单文件组件

在这里,我们将创建单文件组件的<script>部分,该部分将作为联系人页面:

  1. 打开src/pages文件夹中的Contacts.vue文件。在文件的<script>部分,从vuex导入mapActionsmapGetters
import { mapActions, mapGetters } from 'vuex';
  1. 导出具有以下属性的defaultJavaScript 对象:namemixinscomponentsdatamountedmethods。将name属性定义为ChatContacts,在mixins属性中,将数组添加到导入的getAvatarmixin 中。在components属性中,在其内部添加两个新属性NewConversationAvatarDisplay,这将接收一个匿名函数,返回导入的组件。最后,在data属性上,创建一个具有dialogNewConversation属性且值为false的对象:
export default {
  name: 'ChatContacts',
  components: {
    AvatarDisplay: () => import('components/AvatarDisplay'),
    NewConversation: () => import('components/NewConversation'),
  },
  data: () => ({
    dialogNewConversation: false,
  }),
  async mounted() {},
  computed: {},
  methods: {},
};
  1. computed属性中,首先调用getUser从用户模块解构mapGetters。然后我们将对getConversations的聊天模块进行同样的操作:
computed: {
  ...mapGetters('user', ['getUser']),
  ...mapGetters('chat', ['getConversations']),
},
  1. methods属性中,我们将通过调用getMessages函数从聊天模块解构mapActions
methods: {
  ...mapActions('chat', [
    'getMessages',
  ]),
},
  1. 最后,在mounted生命周期钩子上,我们需要使其异步,并添加对getMessage函数的调用:
async mounted() {
  await this.getMessages();
},

单文件组件

现在,让我们为页面创建<template>部分:

  1. 创建一个QPage组件,然后添加一个bordered属性定义为trueQList组件作为子元素:
<q-page>
  <q-list bordered>
  </q-list>
</q-page> 
  1. QList组件内部,创建一个QItem组件,其中v-for指令在getConversations上迭代。定义组件属性如下:keycontact.idto为带路由目的地信息的 JavaScript 对象、classq-my-smclickabletrue,然后添加v-ripple指令:
<q-item
  v-for="contact in getConversations"
  :key="contact.id"
  :to="{
    name: 'Messages',
     params: {
      id: contact.conversation,
      name: contact.name,
    },
  }"
  class="q-my-sm"
  clickable
  v-ripple
></q-item>
  1. QItem组件内部,创建一个QItemSection组件,其avatar属性定义为true。然后创建一个QAvatar组件作为子组件,创建一个AvatarDisplay组件作为QAvatar的子组件。在AvatarDisplay组件上,添加avatar-object动态属性为contact.avatar,添加name动态属性为contact.name
<q-item-section avatar>
  <q-avatar>
    <avatar-display
      :avatar-object="contact.avatar"
      :name="contact.name"
    />
  </q-avatar>
</q-item-section>
  1. 在第一个QItemSection之后,创建另一个QItemSection作为同级元素。在此QItemSection中,添加两个QItemLabel组件。第一个添加contact.name作为内部 HTML,第二个添加caption属性为truelines属性为1,内部 HTML 为contact.email
<q-item-section>
  <q-item-label>{{ contact.name }}</q-item-label>
  <q-item-label caption lines="1">{{ contact.email }}</q-item-label>
</q-item-section>
  1. 然后创建另一个QItemSection组件作为第三个同级,其side属性为true。在其内部添加一个QIcon组件,其name属性为chat_bubblecolor属性为green
<q-item-section side>
  <q-icon name="chat_bubble" color="green"/>
</q-item-section>
  1. 最后,作为QList组件的同级,创建一个QPageSticky组件,position属性定义为bottom-right,而offset属性定义为[18, 18]。在组件内部,创建一个新的子QBtn组件,其fab属性定义为trueiconchatcoloraccent,并且click事件侦听器将dialogNewConversation更改为当前dialogNewConversation的否定。然后,将NewConversation组件添加为QBtn的同级,将v-model指令定义为dialogNewConversation
<q-page-sticky position="bottom-right" :offset="[18, 18]">
  <q-btn
    fab
    icon="chat"
    color="accent"
    @click="dialogNewConversation = !dialogNewConversation"
  />
  <new-conversation
    v-model="dialogNewConversation"
  />
</q-page-sticky>

以下是页面外观的预览:

它是如何工作的。。。

“联系人”页面是创建的所有 Vuex 模块的聚合,因此用户可以在应用上获得更好的体验。此页面包含用户最初导航并开始使用所需的所有信息。

NewConversation组件的<template>部分和联系人页面的<template>部分之间的相似性是故意的,因此用户在创建新对话和查看当前联系人列表时具有相同的体验。

mixin 的使用对于使代码更干净、代码重复更少以及使重用相同代码更简单来说至关重要。

另见

创建应用的消息页面

什么是没有消息的聊天应用?只是一个简单的联系人列表。在这最后的配方中,我们将完成应用的整个周期,为用户直接与其他用户通信创造了可能性。

在此配方中,我们将创建聊天页面、ChatInput组件和消息布局。

准备

此配方的先决条件如下:

  • 上一个配方中的项目
  • Node.js 12+

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

  • @aws-amplify/cli
  • @quasar/cli

要启动我们的用户消息页面,我们将继续在创建应用的联系人页面中创建的项目。

怎么做。。。

在这个配方中,我们需要将其分为三个部分:创建ChatInput组件、创建消息布局,最后创建聊天页面。

创建聊天室输入组件

这里我们将创建ChatInput组件。该组件的职责是接收来自用户的新消息输入并将其发送到服务器。

单文件组件

在本部分中,我们将为页面创建<script>部分:

  1. src/components文件夹中创建一个名为ChatInput.vue的新文件,并将其打开。
  2. vuex包装中导入mapActions
import { mapActions } from 'vuex';
  1. 导出属性为namedatamethodsdefaultJavaScript 对象。将name属性定义为ChatInput
export default {
  name: 'ChatInput',
  data: () => ({}),
  methods: {},
};
  1. data属性上,添加一个名为text的新属性,默认值为空字符串:
data: () => ({
  text: '',
}),
  1. methods属性中,我们将从聊天模块解构mapActions,调用newMessagefetchNewMessages函数。然后我们需要创建一个名为sendMessage的新函数,它将在服务器上创建一条新消息,并从服务器获取新消息:
methods: {
  ...mapActions('chat', ['newMessage', 'fetchNewMessages']),
  async sendMessage() {
    await this.newMessage({
      message: this.text,
      conversationId: this.$route.params.id,
    });

    await this.fetchNewMessages({
      conversationId: this.$route.params.id,
    });

    this.text = '';
  },
},

单文件组件

是时候创建单文件组件的<template>组件部分了:

  1. 创建一个QInput组件,其v-model指令绑定到text。然后将bottom-slots属性定义为true,将label属性定义为"Message"。最后,在enter按钮上定义keypress事件监听器,执行sendMessage功能:
<q-input
  v-model="text"
  bottom-slots
  label="Message"
  @keypress.enter="sendMessage"
></q-input>
  1. QInput组件内部,以after为名称创建一个具有v-slot指令的Template组件。然后创建一个子QBtn组件,其属性为roundflat定义为true,然后icon定义为"send"。最后,在@click事件上添加一个事件监听器,执行sendMessage函数:
<template v-slot:after>
  <q-btn
    round
    flat
    icon="send"
    @click="sendMessage"
  />
</template>

以下是组件的渲染:

创建消息布局

在聊天页面中,我们需要有一个页脚组件供用户输入他们的消息,这需要对我们在前面的食谱中创建的聊天布局进行大量修改。为了使它更简单和更容易维护,我们将创建一个聊天页面专用的新布局,并将其称为 Messages 布局。

单文件组件

现在,让我们创建消息布局的<script>部分:

  1. layouts文件夹中创建一个名为Messages.vue的新文件。
  2. driver/auth.js文件导入signOut函数,从components/ChatInput导入ChatInput组件:
import {
  signOut,
} from 'driver/auth';
import ChatInput from '../components/ChatInput';
  1. 导出一个defaultJavaScript 对象,该对象的name属性定义为"ChatLayout"components属性和另一个名为methods的属性:
export default {
  name: 'MessagesLayout',
  components: {},
  methods: {
  },
};
  1. components属性中,添加导入的ChatInput组件:
components: { ChatInput },
  1. methods属性中,添加一个名为logOff的新异步函数。在此函数中,我们将执行signOut函数,并在其之后重新加载浏览器:
methods: {
 async logOff() {
  await signOut();
  window.location.reload();
 },
}

单文件组件

在这里,我们将创建聊天版面的<template>部分:

  1. 创建一个QLayout组件,其view属性定义为"hHh lpR fFf"
<q-layout view="hHh lpR fFf">
</q-layout>
  1. QLayout组件中,我们需要添加一个具有elevated属性的QHeader组件:
<q-header elevated>
</q-header>
  1. QHeader组件上,我们将添加一个QToolbar组件,其中一个QToolbarTitle组件作为子元素,一个文本作为插槽占位符:
<q-toolbar>
  <q-toolbar-title>
    Chat App - {{ $route.params.name }}
  </q-toolbar-title>
</q-toolbar>
  1. QToolbar组件上,在QToolbarTitle组件之前,我们将添加一个QBtn组件,其属性为denseflatround定义为trueicon属性将显示一个back图标,v-go-back指令定义为$route.meta.goBack,因此目的地在路由文件上定义:
<q-btn
  v-go-back="$route.meta.goBack"
  dense
  flat
  round
  icon="keyboard_arrow_left"
/>
  1. QToolbarTitle组件之后,我们将添加一个QBtn组件,其属性为denseflatround定义为true。我们将icon属性定义为exit_to_app,并在@click指令上传递logOff方法:
<q-btn
  dense
  flat
  round
  icon="exit_to_app"
  @click="logOff"
/>
  1. 作为QHeader组件的同级,创建一个QPageContainer组件,将RouterView组件作为直接子组件:
<q-page-container>
  <router-view />
</q-page-container>
  1. 最后,创建一个QFooter组件,其class属性定义为bg-white。添加一个子QToolbar组件和一个子QToolbarTitle组件。在QToolbarTitle组件内部,添加ChatInput组件:
<q-footer class="bg-white">
  <q-toolbar>
    <q-toolbar-title>
      <chat-input />
    </q-toolbar-title>
  </q-toolbar>
</q-footer>

更改应用路由

创建消息布局后,我们需要更改聊天页面路由的装载方式,以便它可以使用新创建的消息布局:

  1. 打开router文件夹中的routes.js文件。
  2. 找到/chat路由,提取Messages路由对象。在/chat路由之后,创建一个具有pathcomponentchildren属性的新 JavaScript 对象。将path属性定义为/chat/messages,然后在component属性上,我们需要延迟加载新创建的Messages布局。最后,将提取的路由对象放在children属性上,将children数组中新增对象的path属性改为:id/name
{
  path: '/chat/messages',
  component: () => import('layouts/Messages.vue'),
  children: [
    {
      path: ':id/:name',
      name: 'Messages',
      meta: {
        autenticated: true,
        goBack: {
          name: 'Contacts',
        },
      },
      component: () => import('pages/Messages.vue'),
    },
  ],
},

创建消息页面

在配方的最后一部分,我们将创建消息页面。在这里,用户将向联系人发送消息并接收消息。

单文件组件

让我们创建单文件组件的<script>部分:

  1. 打开src/pages文件夹中的Messages.vue文件。在文件的<script>部分,从vuex导入mapActionsmapGetters,从quasar导入date
import { mapActions, mapGetters } from 'vuex';
import { date } from 'quasar';
  1. 导出属性为namecomponentsdatabeforeMountbeforeDestroywatchcomputedmethodsdefaultJavaScript 对象。将name属性定义为MessagesPage。在components属性中,在其中添加一个新属性AvatarDisplay,该属性将接收一个匿名函数,该函数返回导入的组件。最后,在data属性上,使用interval属性创建一个值为null的对象:
export default {
  name: 'MessagesPage',
  components: {
    AvatarDisplay: () => import('components/AvatarDisplay'),
  },
  data: () => ({
    interval: null,
  }),
  async beforeMount() {},
  beforeDestroy() {},
  watch: {},
  computed: {},
  methods: {},
};
  1. 关于computed属性,首先,我们将解构mapGetters函数,传递user模块作为第一个参数,getUser作为第二个参数。然后我们将对getChatMessages的聊天模块进行同样的操作。最后,创建一个currentMessages函数,获取当前对话的消息,并返回格式化为createdAt日期的消息:
computed: {
  ...mapGetters('chat', ['getChatMessages']),
  ...mapGetters('user', ['getUser']),
  currentMessages() {
    const messages = this.getChatMessages(this.$route.params.id);
    if (!messages.length) return [];
    return messages.map((m) => ({
      ...m,
      createdAt: date.formatDate(new Date(parseInt(m.createdAt, 
        10)), 'YYYY/MM/DD HH:mm:ss'),
    }));
  },
},
  1. methods属性调用fetchNewMessageschat模块解构mapActions
methods: {
  ...mapActions('chat', ['fetchNewMessages']),
},
  1. watch属性中,创建一个名为currentMessages的属性,它是一个 JavaScript 对象,具有三个属性handlerdeepimmediate。将handler属性定义为具有newValueoldValue参数的函数。此功能将检查newValue是否大于oldValue。然后创建一个超时,它将滚动屏幕到最后一个可见的元素。deep属性定义为trueimmediate属性定义为false
watch: {
  currentMessages: {
    handler(newValue, oldValue) {
      if (newValue.length > oldValue.length) {
        setTimeout(() => {
          const lastMessage = [...newValue].pop();
          const [{ $el: el }] = this.$refs[`${lastMessage.id}`];
          el.scrollIntoView();
        }, 250);
      }
    },
    deep: true,
    immediate: false,
  },
},
  1. 我们需要使beforeMount生命周期挂钩异步。然后我们需要将interval分配给一个新的setInterval,它将每 1 秒获取一次新消息:
async beforeMount() {
  this.interval = setInterval(async () => {
    await this.fetchNewMessages({
      conversationId: this.$route.params.id,
    });
  }, 1000);
},
  1. 最后,在beforeDestroy生命周期钩子上,我们将清除interval循环,并将interval定义为null
beforeDestroy() {
  clearInterval(this.timeout);
  this.timeout = null;
},

单文件组件

现在,让我们创建单文件组件的<template>部分

  1. 创建一个QPage组件,其class属性定义为q-pa-md row justify-center,并添加一个QChatMessage组件作为子组件。
  2. QChatMessage子组件中,首先在currentMessages上迭代v-for指令。
  3. refkey组件属性定义为message.idstampmessage.createdAttext[message.content]
  4. 然后将sent属性定义为评估message.authorId是否与getUser.id相同,name是否与message.author.name相同,avatar是否与getAvatar方法相同,并将message.author.avatarmessage.author.name作为参数传入。
  5. 然后,将class属性定义为col-12
  6. 最后,在QChatMessage组件内部,在avatar插槽上创建一个template组件,并添加AvatarDisplay组件。将avatar-object动态属性定义为message.author.avatarname动态属性定义为message.author.nametag属性定义为'img'class属性定义为'q-message-avatar',类动态属性定义为三元运算符,检查getUser.id是否与message.authorId不同,返回'q-message-avatar--received''q-message-avatar--sent'如果邮件来自发件人:
<template>
  <q-page class="q-pa-md row justify-center">
    <q-chat-message
      v-for="message in currentMessages"
      :ref="`${message.id}`"
      :key="message.id"
      :stamp="message.createdAt"
      :text="[message.content]"
      :sent="getUser.id === message.authorId"
      :name="message.author.name"
      class="col-12"
    >
      <template v-slot:avatar>
        <avatar-display
          :avatar-object="message.author.avatar"
          :name="message.author.name"
          tag="img"
          class="q-message-avatar"
          :class="getUser.id !== message.authorId
          ? 'q-message-avatar--received'
          : 'q-message-avatar--sent'"
        />
      </template>
    </q-chat-message>
  </q-page>
</template>

以下是页面外观的预览:

它是如何工作的。。。

消息页面由三部分组成:布局、ChatInput组件和页面。使用这种组合,我们能够将代码划分为不同的职责,以增加维护代码的难度。

ChatInput组件中,我们使用 Chat Vuex 模块直接发送消息,无需通过页面或布局等容器,使组件具有状态。

我们需要添加新的布局和路由修改,因为应用的布局需要一个固定在应用页脚上的组件。此页脚是消息输入,用户需要始终可见。

最后,Messages 页面是一个自动刷新页面,每秒获取新内容,并始终为用户显示新消息。

另见