Vue 与 GraphQL 应用构建指南(三)
原文:
zh.annas-archive.org/md5/60CC414A1AE322EC97E6A0F8A5BBE3AD译者:飞龙
第六章:创建 Chat 和 Message Vuex、页面和路由
在本章中,我们将完成应用程序并创建最终部分。本章将完成应用程序的开发,使其准备好为部署创建最终产品。
在这里,您将学习如何创建 GraphQL 查询和片段,创建 Chat Vuex 模块和业务规则,创建联系人页面和页面中使用的组件,最后创建消息页面和创建页面所需的组件。
在本章中,我们将涵盖以下食谱:
-
创建 GraphQL 查询和片段
-
在您的应用程序上创建 Chat Vuex 模块
-
创建应用程序的联系人页面
-
创建应用程序的消息页面
技术要求
在本章中,我们将使用 Node.js,AWS Amplify 和 Quasar Framework。
注意,Windows 用户!您需要安装一个名为windows-build-tools的npm包,以便能够安装所需的包。要执行此操作,请以管理员身份打开 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 片段
在这里,我们将创建我们在应用程序中将使用的所有片段:
-
在
src/graphql文件夹中创建一个名为fragments.js的文件并打开它。 -
然后,我们需要导入
graphql语言解释器:
import graphql from 'graphql-tag';
- 让我们创建
getUser片段来获取用户信息。这个片段将获取用户的基本信息。首先,我们需要启动graphql解释器,然后传递带有我们查询的模板文字字符串。使用getUser查询作为基本查询,我们将创建一个只包含我们想要从服务器获取的数据的查询模式:
const getUser = graphql`
query getUser($id: ID!) {
getUser(id: $id) {
id username avatar { bucket key region } email name } } `;
ES2015 规范中的模板文字提供了一个称为标记模板或标记函数的新功能。这些用于在使用附加到它的字符串之前预处理模板文字上的字符串。
- 然后我们将创建
listUsers片段来获取应用程序中的所有用户。这个片段将使用从 AWS Amplify 创建的基本查询中的listUsers查询。然后它将返回我们应用程序中所有当前用户的基本信息:
const listUsers = graphql`
query listUsers { listUsers { items { id username name createdAt avatar { bucket region key } } } } `;
- 为了完成用户片段,我们将创建
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 } } } } } } } } } `;
- 为了获取用户对话,我们将创建一个名为
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 }
} `;
- 要在我们的 API 中创建新的消息,我们需要创建一个名为
createMessage的片段。这个片段基于CreateMessage变异。片段将接收id、authorId、content、messageConversationId和createdAt:
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 } } `;
- 要在两个用户之间开始新的对话,我们需要创建一个名为
createConversation的新片段。这个片段基于CreateConversation变异;它将接收对话的name和正在创建的对话的members列表:
const createConversation = graphql`mutation CreateConversation($name: String!, $members: [String!]!) { createConversation(input: {
name: $name, members: $members
}) { id name members } } `;
- 然后,我们将使用基于 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 }
} } `;
- 最后,我们将导出我们创建的所有片段到 JavaScript 对象中:
export {
getUser,
listUsers,
getUserAndConversations,
getConversation,
createMessage,
createConversation,
createConversationLink, };
将片段应用于 User Vuex 操作
现在我们可以更新 User Vuex 操作以使用我们创建的片段:
-
在 store/user 文件夹中打开 actions.js 文件。
-
在
import部分,我们将从 src/graphql/queries 替换 getUser 和 listUsers 为新创建的 src/graphql/fragments。
import { listUsers, getUser } from 'src/graphql/fragments';
它是如何工作的...
使用 GraphQL 查询语言,我们能够创建小查询和变异,称为片段,可以执行原始查询或变异的部分,并返回相同的响应,但包含我们请求的数据。
通过这样做,我们的应用程序数据使用量减少了,遍历数据的处理能力也减少了。
GraphQL 片段与作为基础的查询或变异相同。这是因为 GraphQL 使用相同的模式、查询和变异作为基础。通过这样做,您可以在搜索和变异中使用在查询或变异中声明的相同变量。
因为我们在替换 User Vuex 操作中导入的代码时使用了相同的名称作为基础查询,所以我们不需要更改任何内容,因为请求的结果将与旧的结果相同。
另请参阅
-
在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals找到有关模板文字标签函数的更多信息。 -
在
graphql.org/learn/queries/找到有关 GraphQL 查询、变异和片段的更多信息。
在您的应用程序上创建 Chat Vuex 模块
要创建聊天应用程序,我们需要为应用程序的聊天部分创建自定义业务规则。这部分将包含获取新消息、发送消息和在用户之间开始新对话的所有逻辑。
在这个教程中,我们将在应用程序的 Vuex 中创建 Chat 模块,其中我们将存储已登录用户和其他用户之间的所有消息,获取新消息,发送新消息,并开始新对话。
准备工作
此教程的先决条件如下:
-
来自上一个教程的项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli -
@quasar/cli
为了开始我们的 Chat Vuex 模块,我们将继续使用在 创建 GraphQL 查询和片段 教程中创建的项目。
如何做...
为了创建 Chat Vuex 模块,我们将把任务分成五个部分:创建 state、mutations、getters 和 actions,然后将模块添加到 Vuex。
创建 Chat Vuex 状态
为了在 Vuex 模块上存储数据,我们需要一个具有存储数据的状态。在这里,我们将创建 Chat 状态:
-
在 store 文件夹中创建一个名为
chat的新文件夹,然后创建一个名为state.js的新文件,并打开它。 -
创建一个名为
createState的新函数,它返回一个具有conversations、messages、loading和error属性的 JavaScript 对象。conversations和messages属性将被定义为空数组,loading属性将被定义为false,error为undefined:
export function createState() {
return {
conversations: [],
messages: [],
loading: false,
error: undefined,
}; }
- 最后,为了将状态导出为单例,并将其作为 JavaScript 对象可用,我们需要将
createState函数的执行导出为默认值:
export default createState();
创建 Chat Vuex mutations
现在要在状态上保存任何数据,Vuex 需要一个 mutation。为此,我们将创建 Chat mutation,用于管理此模块的 mutations:
-
在 store/chat 文件夹中创建一个名为
types.js的新文件,并打开它。 -
在文件中,导出一个默认的 JavaScript 对象,其属性与字符串的值相同。属性将是
SET_CONVERSATIONS、SET_MESSAGES、LOADING和ERROR:
export default {
SET_CONVERSATIONS: 'SET_CONVERSATIONS',
SET_MESSAGES: 'SET_MESSAGES',
LOADING: 'LOADING',
ERROR: 'ERROR', };
-
在 store/chat 文件夹中创建一个名为
mutations.js的新文件,并打开它。 -
导入新创建的
types.js文件:
import MT from './types';
- 创建一个名为
setLoading的新函数,以state作为第一个参数。在其中,我们将定义state.loading为true:
function setLoading(state) {
state.loading = true; }
- 创建一个名为
setError的新函数,以state作为第一个参数,error作为第二个参数,其默认值为new Error()。在其中,我们将定义state.error为error,并将state.loading定义为false:
function setError(state, error = new Error()) {
state.error = error;
state.loading = false; }
- 创建一个名为
setConversations的新函数,第一个参数是state,第二个参数是 JavaScript 对象,具有items属性。通过这样做,我们将使用接收到的数组定义状态对话:
function setConversations(state, payload) {
state.conversations = payload.items;
state.loading = false; }
- 创建一个名为
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; }
- 最后,导出一个默认的 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。
-
在
store/chat文件夹中创建一个名为getters.js的新文件。 -
创建一个名为
getConversations的新函数。该函数首先接收state,_getters,_rootState和rootGetters作为柯里化函数的第一部分。最后,它将返回用户和应用程序中另一个用户之间的对话的筛选列表:
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 调用,传递state,getters,rootState和rootGetters,因为根据 linter 规则,我们为未使用的参数添加了下划线。
- 创建一个名为
getChatMessages的新函数,这是一个使用方法调用的 getter。首先,我们传递state,然后返回一个接收convId的函数。最后,它将返回该对话 ID 的消息列表:
const getChatMessages = (state) => (convId) => (state.messages.length ? state.messages
.find(m => m.id === convId).messages.items : []);
- 创建一个名为
isLoading的新函数,返回state.loading:
const isLoading = (state) => state.loading;
- 创建一个名为
hasError的新函数,返回state.error:
const hasError = (state) => state.error;
- 最后,导出一个默认的 JavaScript 对象,其中包含创建的函数作为属性:
getConversations,getChatMessages,isLoading和hasError:
export default {
getConversations,
getChatMessages,
isLoading,
hasError, };
创建 Chat Vuex actions
在这里,我们将创建 Chat 模块的 Vuex actions:
-
在
store/chat文件夹中创建一个名为actions.js的文件,并打开它。 -
首先,我们需要导入在这部分中要使用的函数、枚举和类:
-
从
aws-amplify包中导入graphqlOperation。 -
从
src/graphql/fragments.js导入getUserAndConversations,createConversation,createConversationLink,createMessage和getConversation。 -
从
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';
- 创建一个名为
newConversation的异步函数。在第一个参数中,我们将添加_vuex,并使用一个 JavaScript 对象作为第二个参数,接收authorId和otherUserId作为属性。在这个函数中,我们将根据接收到的载荷创建一个新的对话。然后我们需要创建对话和对话中用户之间的关系。最后,我们返回对话的 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);
} }
- 为了向用户发送新消息,我们需要创建一个名为
newMessage的异步函数。这个函数将在第一个参数中接收一个解构的 JavaScript 对象,其中包含commit变量,并作为第二个参数,另一个解构的 JavaScript 对象,其中包含message和conversationId属性。然后,在函数中,我们需要获取用户的username并返回 GraphQL 的createMessage变异,传递变量,其中id定义为uid(),authorID定义为username,content定义为message,messageConversationId定义为conversationId,createdAt定义为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);
} }
- 为了获取初始用户消息,我们需要创建一个名为
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);
} }
- 然后我们需要完成聊天操作,创建
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);
} }
- 最后,我们将导出所有创建的函数:
export default {
newConversation,
newMessage,
getMessages,
fetchNewMessages, };
将 Chat 模块添加到 Vuex
现在我们将 Chat 模块导入到 Vuex 状态管理中:
-
在
store/chat文件夹中创建一个名为index.js的新文件。 -
导入我们刚刚创建的
state.js、actions.js、mutation.js和getters.js文件:
import state from './state'; import actions from './actions'; import mutations from './mutations'; import getters from './getters';
- 创建一个带有 JavaScript 对象的
export default,其中属性为state、actions、mutations、getters和namespaced(定义为true):
export default {
namespaced: true,
state,
actions,
mutations,
getters, };
-
打开
store文件夹中的index.js文件。 -
将新创建的
index.js文件导入到store/chat文件夹中:
import Vue from 'vue'; import Vuex from 'vuex'; import user from './user';
import chat form './chat';
- 在创建 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 获取用户对话。
另请参阅
-
在
vuex.vuejs.org/api/#getters找到有关 Vuex getters API 的更多信息。 -
在
vuex.vuejs.org/guide/getters.html#method-style-access找到有关 Vuex getters 方法数据访问的更多信息。
创建应用程序的联系人页面
在聊天应用程序中,通常会有一个起始页面,用户可以从旧对话中选择继续发送消息,或者开始新的对话。这种做法可以作为应用程序的主页面。在我们的应用程序中,也不会有所不同。
在这个示例中,我们将创建一个联系人页面,用户可以使用它来开始对话或继续旧对话。
准备工作
这个示例的先决条件如下:
-
来自上一个示例的项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli -
@quasar/cli
要开始我们的用户联系人页面,我们将继续使用在在应用程序中创建 Chat Vuex 模块示例中创建的项目。
操作步骤...
在这个示例中,我们需要将我们的工作分为两部分:首先是一个新的组件来开始新的对话,最后是联系人页面本身。
创建 NewConversation 组件
首先,我们需要创建一个组件,在应用程序中的用户和另一个用户之间开始新的对话。
单文件组件
在这里,我们将创建组件的<script>部分:
-
在
src/components文件夹中创建一个名为NewConversation.vue的新文件并打开它。 -
从
vuex中导入mapActions和mapGetters:
import { mapActions, mapGetters } from 'vuex';
- 导出一个带有七个属性的
defaultJavaScript 对象:name,props,data,watch,computed和methods:
export default {
name: 'NewConversation',
components: {},
props: {},
data: () => ({}),
watch: {},
computed: {},
methods: {},
};
- 在
components属性中,将AvatarDisplay组件导入为 lazyload 组件:
components: {
AvatarDisplay: () => import('components/AvatarDisplay'), },
- 在
props属性中,我们将添加一个名为value的新属性,类型为Boolean,默认值为false:
props: {
value: {
type: Boolean,
default: false,
}, },
- 在
data属性上,我们需要定义两个属性:userList作为一个数组,pending作为一个布尔值,定义为false:
data: () => ({
userList: [],
pending: false, }),
- 在
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,
});
}
}, },
- 在
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);
}, },
- 最后,在
watch属性上,我们将添加一个名为value的异步函数,它接收一个名为newVal的参数。这个函数检查newVal的值是否为true;如果是,它将在 API 中获取用户列表:
watch: {
async value(newVal) {
if (newVal) {
await this.fetchUsers();
}
}, },