六、创建聊天和消息 Vuex、页面和路由
在本章中,我们将完成应用并创建最终部分。本章将完成应用的开发,为创建用于部署的最终产品做好准备。
在这里,您将学习如何创建 GraphQL 查询和片段,创建聊天室 Vuex 模块和业务规则,创建联系人页面和页面中使用的组件,最后是消息页面以及创建页面所需的组件。
在本章中,我们将介绍以下配方:
- 创建 GraphQL 查询和片段
- 在应用上创建聊天室 Vuex 模块
- 创建应用的联系人页面
- 创建应用的消息页面
技术要求
在本章中,我们将使用Node.js、AWS 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 片段
在这里,我们将创建我们将在应用中使用的所有片段:
- 在
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
}
}
`;
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.
- 然后我们将创建
listUsers
片段来获取应用中的所有用户。此片段将使用来自 AWS Amplify 创建的基本查询的listUsers
查询。然后,它将返回应用中的所有当前用户及其基本信息:
const listUsers = graphql`
query listUsers {
listUsers {
items {
id
username
name
createdAt
avatar {
bucket
region
key
}
}
}
}
`;
- 我们将创建用户的基本对话片段,最后获取用户的对话片段。此片段基于
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 中的用户处获取最后 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
}
}
`;
- 要在 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,
};
在用户 Vuex 操作上应用片段
现在,我们可以更新用户 Vuex 操作以使用我们创建的片段:
- 打开
store/user
文件夹中的actions.js
文件。 - 在
import
部分,我们将src/graphql/queries
中的getUser
和listUsers
替换为新创建的src/graphql/fragments
:
import { listUsers, getUser } from 'src/graphql/fragments';
它是如何工作的。。。
使用 GraphQL 查询语言,我们能够创建称为片段的小查询和突变,这些查询和突变可以执行原始查询或突变的一部分,并返回相同的响应,但包含我们请求的数据。
通过这样做,我们的应用中的数据使用量减少了,迭代数据的处理能力也降低了。
GraphQL 片段的工作原理与用作基础的查询或变异相同。这是因为 GraphQL 使用与基相同的模式、查询和突变。通过这样做,您可以在搜索和突变中使用查询或突变中声明的相同变量。
因为在替换用户 Vuex 操作上导入的代码时,我们使用了与基本查询相同的名称,所以我们不需要更改任何内容,因为请求的结果将与旧的相同。
另见
- 有关模板文字标记函数的更多信息,请参见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals 。
- 在找到更多关于 GraphQL 查询、突变和片段的信息 https://graphql.org/learn/queries/ 。
在应用上创建聊天室 Vuex 模块
要创建聊天应用,我们需要为应用的聊天部分创建自定义业务规则。这部分将包含获取新消息、发送消息和在用户之间开始新对话之间的所有逻辑。
在此配方中,我们将在应用 Vuex 中创建聊天模块,在其中存储登录用户和其他用户之间的所有消息,获取新消息,发送新消息,并启动新对话。
准备
此配方的先决条件如下:
- 上一个配方中的项目
- Node.js 12+
所需的 Node.js 全局对象如下:
@aws-amplify/cli
@quasar/cli
为了启动聊天 Vuex 模块,我们将继续在创建 GraphQL 查询和片段配方中创建的项目。
怎么做。。。
为了创建聊天 Vuex 模块,我们将任务分为五个部分:创建状态、突变、获取者和动作,然后将模块添加到 Vuex。
创建聊天室 Vuex 状态
为了将数据存储在 Vuex 模块上,我们需要一个存储数据的状态。在这里,我们将创建聊天状态:
- 在
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 对象可用,我们需要
export default
执行createState
函数:
export default createState();
创建聊天室 Vuex
现在,要保存状态的任何数据,Vuex 需要进行变异。为此,我们将创建聊天变异,该变异将管理此模块的变异:
- 在
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
等于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 对象,其中键为导入的变异类型,值为对应于每种类型的函数:
-
将
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,
};
创建聊天室 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
.
- 在
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
(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.
- 创建一个名为
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;
- 最后,导出一个
default
JavaScript 对象,将创建的函数作为属性:getConversations
、getChatMessages
、isLoading
和hasError
:
export default {
getConversations,
getChatMessages,
isLoading,
hasError,
};
创建聊天室 Vuex 操作
在这里,我们将创建聊天模块的 Vuex 操作:
- 在
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 '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
的asynchronous
函数。此函数将在第一个参数中接收一个带commit
变量的解构 JavaScript 对象,作为第二个参数,接收另一个带message
和conversationId
属性的解构 JavaScript 对象。然后在函数中,我们需要获取用户的username
并返回 GraphQLcreateMessage
突变,传递变量,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
,然后执行 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);
}
}
- 然后我们需要完成聊天动作,创建
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);
}
}
- 最后,我们将导出所有创建的函数:
export default {
newConversation,
newMessage,
getMessages,
fetchNewMessages,
};
将聊天模块添加到 Vuex
现在,我们将创建的聊天模块导入 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
的新属性,并将其定义为 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 获取用户会话。
另见
- 有关 Vuex getters API 的更多信息,请访问https://vuex.vuejs.org/api/#getters 。
- 有关 Vuex getters 方法数据访问的更多信息,请访问https://vuex.vuejs.org/guide/getters.html#method-样式访问。
创建应用的联系人页面
在聊天应用中,通常会有一个起始页,用户可以在其中从旧对话中选择继续发送消息,或启动新对话。此实践可用作应用的主页。在我们的应用中,不会有什么不同。
在此配方中,我们将创建一个联系人页面,用户可以使用该页面开始对话或继续旧的对话。
准备
此配方的先决条件如下:
- 上一个配方中的项目
- Node.js 12+
所需的 Node.js 全局对象如下:
@aws-amplify/cli
@quasar/cli
要启动我们的用户联系页面,我们将继续在中创建的项目,该项目是在您的应用配方上创建聊天 Vuex 模块。
怎么做。。。
在这个食谱中,我们需要将我们的工作分为两部分:首先是一个新的组件来开始新的对话,最后是联系人页面本身。
创建 NewConversation 组件
首先,我们需要创建组件,以便在用户和应用上的另一个用户之间开始新的对话。
单文件组件
在这里,我们将创建组件的<script>
部分:
- 在
src/components
文件夹中创建一个名为NewConversation.vue
的新文件并打开它。 - 从
vuex
导入mapActions
和mapGetters
:
import { mapActions, mapGetters } from 'vuex';
- 导出具有七个属性的
default
JavaScript 对象: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
属性中,首先,我们将从调用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,
});
}
},
},
- 在
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();
}
},
},
单文件组件
现在让我们为NewConversation
组件创建<template>
部分:
- 创建一个
QDialog
组件,其value
属性定义为value
。同时创建定义为$emit
函数的事件监听器input
,以$event
作为数据发送'input'
事件:
<q-dialog
:value="value"
@input="$emit('input', $event)"
></q-dialog>
- 在
QDialog
组件内部,创建一个QCard
组件,其style
属性定义为min-width: 400px; min-height: 100px;
。在QCard
组件内部,创建两个QCardSection
子组件。在第一个组件中,添加定义为row items-center q-pb-none
的class
属性:
<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>
- 在第一个
QCardSection
组件上,添加一个div
,其class
属性为text-h6
,内部 HTML 为New Conversation
。然后添加一个QSpace
组件。最后,添加QBtn
,其中icon
属性为close
,flat
、round
、dense
属性为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>
- 在第二个
QCardSection
组件中,创建一个具有QItem
子级的QList
组件。在QItem
子组件中,添加v-for
指令以迭代contactList
。然后将key
变量属性定义为contact.id
、class
属性定义为q-my-sm
、clickable
属性定义为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>
- 在
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>
- 在第一个
QItemSection
组件之后,创建另一个QItemSection
作为同级元素。在此QItemSection
中,添加两个QItemLabel
组件。第一个添加contact.name
作为内部 HTML,第二个添加caption
属性为true
,lines
属性为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>
- 然后创建另一个
QItemSection
组件作为第三个同级,其side
属性为true
。在其内部添加一个QIcon
组件,其name
属性为add_comment
、color
属性为green
:
<q-item-section side>
<q-icon name="add_comment" color="green"/>
</q-item-section>
- 最后,作为
QList
组件的兄弟,创建一个QInnerLoading
组件,其showing
属性定义为pending
。在其内部添加一个QSpinner
组件,其size
属性为50px
,而color
属性定义为primary
:
<q-inner-loading
:showing="pending">
<q-spinner
size="50px"
color="primary"/>
</q-inner-loading>
以下是组件的渲染版本:
创建联系人页面
现在是创建联系人页面的时候了。此页面将是认证用户应用的初始页面。在这里,用户将能够进入用户更新页面,输入并恢复旧对话,或创建新对话。
单文件组件
在这里,我们将创建单文件组件的<script>
部分,该部分将作为联系人页面:
- 打开
src/pages
文件夹中的Contacts.vue
文件。在文件的<script>
部分,从vuex
导入mapActions
和mapGetters
:
import { mapActions, mapGetters } from 'vuex';
- 导出具有以下属性的
default
JavaScript 对象:name
、mixins
、components
、data
、mounted
和methods
。将name
属性定义为ChatContacts
,在mixins
属性中,将数组添加到导入的getAvatar
mixin 中。在components
属性中,在其内部添加两个新属性NewConversation
和AvatarDisplay
,这将接收一个匿名函数,返回导入的组件。最后,在data
属性上,创建一个具有dialogNewConversation
属性且值为false
的对象:
export default {
name: 'ChatContacts',
components: {
AvatarDisplay: () => import('components/AvatarDisplay'),
NewConversation: () => import('components/NewConversation'),
},
data: () => ({
dialogNewConversation: false,
}),
async mounted() {},
computed: {},
methods: {},
};
- 在
computed
属性中,首先调用getUser
从用户模块解构mapGetters
。然后我们将对getConversations
的聊天模块进行同样的操作:
computed: {
...mapGetters('user', ['getUser']),
...mapGetters('chat', ['getConversations']),
},
- 在
methods
属性中,我们将通过调用getMessages
函数从聊天模块解构mapActions
:
methods: {
...mapActions('chat', [
'getMessages',
]),
},
- 最后,在
mounted
生命周期钩子上,我们需要使其异步,并添加对getMessage
函数的调用:
async mounted() {
await this.getMessages();
},
单文件组件
现在,让我们为页面创建<template>
部分:
- 创建一个
QPage
组件,然后添加一个bordered
属性定义为true
的QList
组件作为子元素:
<q-page>
<q-list bordered>
</q-list>
</q-page>
- 在
QList
组件内部,创建一个QItem
组件,其中v-for
指令在getConversations
上迭代。定义组件属性如下:key
为contact.id
、to
为带路由目的地信息的 JavaScript 对象、class
为q-my-sm
、clickable
为true
,然后添加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>
- 在
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>
- 在第一个
QItemSection
之后,创建另一个QItemSection
作为同级元素。在此QItemSection
中,添加两个QItemLabel
组件。第一个添加contact.name
作为内部 HTML,第二个添加caption
属性为true
,lines
属性为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>
- 然后创建另一个
QItemSection
组件作为第三个同级,其side
属性为true
。在其内部添加一个QIcon
组件,其name
属性为chat_bubble
且color
属性为green
:
<q-item-section side>
<q-icon name="chat_bubble" color="green"/>
</q-item-section>
- 最后,作为
QList
组件的同级,创建一个QPageSticky
组件,position
属性定义为bottom-right
,而offset
属性定义为[18, 18]
。在组件内部,创建一个新的子QBtn
组件,其fab
属性定义为true
、icon
为chat
、color
为accent
,并且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 的使用对于使代码更干净、代码重复更少以及使重用相同代码更简单来说至关重要。
另见
- 在找到更多关于类星体
QBtn
成分的信息 https://quasar.dev/vue-components/button 。 - 在找到更多关于类星体
QDialog
成分的信息 https://quasar.dev/vue-components/dialog 。 - 在找到更多关于类星体
QInnerLoading
成分的信息 https://quasar.dev/vue-components/inner-loading 。 - 有关类星体
QSpinners
的更多信息,请访问https://quasar.dev/vue-components/spinners 。 - 在找到更多关于类星体
QPageSticky
成分的信息 https://quasar.dev/layout/page-sticky 。 - 有关类星体
ClosePopup
指令的更多信息,请访问https://quasar.dev/vue-directives/close-popup 。 - 在上查找有关 Vue 混合的更多信息 https://vuejs.org/v2/guide/mixins.html 。
创建应用的消息页面
什么是没有消息的聊天应用?只是一个简单的联系人列表。在这最后的配方中,我们将完成应用的整个周期,为用户直接与其他用户通信创造了可能性。
在此配方中,我们将创建聊天页面、ChatInput
组件和消息布局。
准备
此配方的先决条件如下:
- 上一个配方中的项目
- Node.js 12+
所需的 Node.js 全局对象如下:
@aws-amplify/cli
@quasar/cli
要启动我们的用户消息页面,我们将继续在创建应用的联系人页面中创建的项目。
怎么做。。。
在这个配方中,我们需要将其分为三个部分:创建ChatInput
组件、创建消息布局,最后创建聊天页面。
创建聊天室输入组件
这里我们将创建ChatInput
组件。该组件的职责是接收来自用户的新消息输入并将其发送到服务器。
单文件组件
在本部分中,我们将为页面创建<script>
部分:
- 在
src/components
文件夹中创建一个名为ChatInput.vue
的新文件,并将其打开。 - 从
vuex
包装中导入mapActions
:
import { mapActions } from 'vuex';
- 导出属性为
name
、data
和methods
的default
JavaScript 对象。将name
属性定义为ChatInput
:
export default {
name: 'ChatInput',
data: () => ({}),
methods: {},
};
- 在
data
属性上,添加一个名为text
的新属性,默认值为空字符串:
data: () => ({
text: '',
}),
- 在
methods
属性中,我们将从聊天模块解构mapActions
,调用newMessage
和fetchNewMessages
函数。然后我们需要创建一个名为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>
组件部分了:
- 创建一个
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>
- 在
QInput
组件内部,以after
为名称创建一个具有v-slot
指令的Template
组件。然后创建一个子QBtn
组件,其属性为round
和flat
定义为true
,然后icon
定义为"send"
。最后,在@click
事件上添加一个事件监听器,执行sendMessage
函数:
<template v-slot:after>
<q-btn
round
flat
icon="send"
@click="sendMessage"
/>
</template>
以下是组件的渲染:
创建消息布局
在聊天页面中,我们需要有一个页脚组件供用户输入他们的消息,这需要对我们在前面的食谱中创建的聊天布局进行大量修改。为了使它更简单和更容易维护,我们将创建一个聊天页面专用的新布局,并将其称为 Messages 布局。
单文件组件
现在,让我们创建消息布局的<script>
部分:
- 在
layouts
文件夹中创建一个名为Messages.vue
的新文件。 - 从
driver/auth.js
文件导入signOut
函数,从components/ChatInput
导入ChatInput
组件:
import {
signOut,
} from 'driver/auth';
import ChatInput from '../components/ChatInput';
- 导出一个
default
JavaScript 对象,该对象的name
属性定义为"ChatLayout"
、components
属性和另一个名为methods
的属性:
export default {
name: 'MessagesLayout',
components: {},
methods: {
},
};
- 在
components
属性中,添加导入的ChatInput
组件:
components: { ChatInput },
- 在
methods
属性中,添加一个名为logOff
的新异步函数。在此函数中,我们将执行signOut
函数,并在其之后重新加载浏览器:
methods: {
async logOff() {
await signOut();
window.location.reload();
},
}
单文件组件
在这里,我们将创建聊天版面的<template>
部分:
- 创建一个
QLayout
组件,其view
属性定义为"hHh lpR fFf"
:
<q-layout view="hHh lpR fFf">
</q-layout>
- 在
QLayout
组件中,我们需要添加一个具有elevated
属性的QHeader
组件:
<q-header elevated>
</q-header>
- 在
QHeader
组件上,我们将添加一个QToolbar
组件,其中一个QToolbarTitle
组件作为子元素,一个文本作为插槽占位符:
<q-toolbar>
<q-toolbar-title>
Chat App - {{ $route.params.name }}
</q-toolbar-title>
</q-toolbar>
- 在
QToolbar
组件上,在QToolbarTitle
组件之前,我们将添加一个QBtn
组件,其属性为dense
、flat
、round
定义为true
。icon
属性将显示一个back
图标,v-go-back
指令定义为$route.meta.goBack
,因此目的地在路由文件上定义:
<q-btn
v-go-back="$route.meta.goBack"
dense
flat
round
icon="keyboard_arrow_left"
/>
- 在
QToolbarTitle
组件之后,我们将添加一个QBtn
组件,其属性为dense
、flat
、round
定义为true
。我们将icon
属性定义为exit_to_app
,并在@click
指令上传递logOff
方法:
<q-btn
dense
flat
round
icon="exit_to_app"
@click="logOff"
/>
- 作为
QHeader
组件的同级,创建一个QPageContainer
组件,将RouterView
组件作为直接子组件:
<q-page-container>
<router-view />
</q-page-container>
- 最后,创建一个
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>
更改应用路由
创建消息布局后,我们需要更改聊天页面路由的装载方式,以便它可以使用新创建的消息布局:
- 打开
router
文件夹中的routes.js
文件。 - 找到
/chat
路由,提取Messages
路由对象。在/chat
路由之后,创建一个具有path
、component
和children
属性的新 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>
部分:
- 打开
src/pages
文件夹中的Messages.vue
文件。在文件的<script>
部分,从vuex
导入mapActions
和mapGetters
,从quasar
导入date
:
import { mapActions, mapGetters } from 'vuex';
import { date } from 'quasar';
- 导出属性为
name
、components
、data
、beforeMount
、beforeDestroy
、watch
、computed
、methods
的default
JavaScript 对象。将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: {},
};
- 关于
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'),
}));
},
},
- 在
methods
属性调用fetchNewMessages
从chat
模块解构mapActions
:
methods: {
...mapActions('chat', ['fetchNewMessages']),
},
- 在
watch
属性中,创建一个名为currentMessages
的属性,它是一个 JavaScript 对象,具有三个属性handler
、deep
和immediate
。将handler
属性定义为具有newValue
和oldValue
参数的函数。此功能将检查newValue
是否大于oldValue
。然后创建一个超时,它将滚动屏幕到最后一个可见的元素。deep
属性定义为true
,immediate
属性定义为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,
},
},
- 我们需要使
beforeMount
生命周期挂钩异步。然后我们需要将interval
分配给一个新的setInterval
,它将每 1 秒获取一次新消息:
async beforeMount() {
this.interval = setInterval(async () => {
await this.fetchNewMessages({
conversationId: this.$route.params.id,
});
}, 1000);
},
- 最后,在
beforeDestroy
生命周期钩子上,我们将清除interval
循环,并将interval
定义为null
:
beforeDestroy() {
clearInterval(this.timeout);
this.timeout = null;
},
单文件组件
现在,让我们创建单文件组件的<template>
部分
- 创建一个
QPage
组件,其class
属性定义为q-pa-md row justify-center
,并添加一个QChatMessage
组件作为子组件。 - 在
QChatMessage
子组件中,首先在currentMessages
上迭代v-for
指令。 - 将
ref
和key
组件属性定义为message.id
、stamp
为message.createdAt
、text
为[message.content]
。 - 然后将
sent
属性定义为评估message.authorId
是否与getUser.id
相同,name
是否与message.author.name
相同,avatar
是否与getAvatar
方法相同,并将message.author.avatar
和message.author.name
作为参数传入。 - 然后,将
class
属性定义为col-12
。 - 最后,在
QChatMessage
组件内部,在avatar
插槽上创建一个template
组件,并添加AvatarDisplay
组件。将avatar-object
动态属性定义为message.author.avatar
,name
动态属性定义为message.author.name
,tag
属性定义为'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 页面是一个自动刷新页面,每秒获取新内容,并始终为用户显示新消息。
另见
- 在找到更多关于类星体框架
QChatMessage
组件的信息 https://quasar.dev/vue-components/chat 。 - 有关类星体框架的
date
UTIL 的更多信息,请访问https://quasar.dev/quasar-utils/date-utils 。**
版权属于:月萌API www.moonapi.com,转载请注明出处