Vue 与 GraphQL 应用构建指南(全)
原文:
zh.annas-archive.org/md5/60CC414A1AE322EC97E6A0F8A5BBE3AD
译者:飞龙
前言
自 2012 年 Facebook 发布以来,GraphQL 已经席卷了互联网。像 Airbnb 和 Audi 这样的大公司已经开始采用它,而中小型公司现在也意识到了这种基于查询的 API 的潜力。
GraphQL 起初可能看起来很奇怪,但当你开始阅读和体验更多时,你就不会再想使用 REST API 了。
通过本书中的示例,你将学习如何从头开始构建一个完整的实时聊天应用程序。首先创建一个 AWS Amplify 环境,然后深入开发你的第一个 GraphQL 模式。然后学习如何添加 AppSync GraphQL 客户端并创建你的第一个 GraphQL 变异。本书还将帮助你发现 GraphQL 的简单性和数据获取能力,使前端开发人员能够轻松与服务器通信。最后,你将了解如何使用 Quasar Framework 创建应用程序组件和布局。最后,你将了解如何在应用程序中创建 Vuex 模块来管理应用程序状态,使用 GraphQL 客户端获取数据,并将应用程序部署到 Web 上。
这本书适合谁
这本书适合中级 Vue.js 开发人员,他们想迈出全栈开发的第一步。如果你想了解更多关于使用自定义业务规则开发 Vuex 以及创建入门级企业架构应用程序,那么这本书适合你。在开始阅读本书之前,需要具备 Vue.js 和 JavaScript 的基础知识。
这本书涵盖了什么
第一章,数据绑定、表单验证、事件和计算属性,讨论了基本的 Vue 开发和组件概念,包括v-model
、事件监听器、计算属性和for
循环。读者将介绍如何使用 Vuelidate 插件进行表单验证以及如何在 Vue 组件上使用它,以及如何使用vue-devtools
调试 Vue 组件。
第二章,组件、混入和功能组件,引导读者通过不同的方法构建组件,包括用于内容的自定义插槽、验证的 props、功能组件以及为了代码重用性而创建的混入。然后介绍了一系列不同的方法来访问子组件的数据,创建依赖注入组件和动态注入组件,以及如何延迟加载组件。
第三章,“设置我们的聊天应用程序-AWS Amplify 环境和 GraphQL”,介绍了 AWS Amplify CLI,介绍了如何创建 Amplify 环境。创建他们的身份验证网关与 AWS Cognito,一个 S3 文件托管桶,最后创建 GraphQL API。在这个过程中,读者将创建用于前端和后端通信的驱动程序。
第四章,“创建自定义应用程序组件和布局”,从现在开始,读者将开始开发应用程序。在这一章中,读者将创建用于聊天应用程序页面的组件。读者将创建组件,如PasswordInput
,AvatarInput
,EmailInput
等。
第五章,“创建用户 Vuex、页面和路由”,引导读者构建应用程序的第一个 Vuex 模块,用于管理用户业务规则和存储用户数据。然后读者将创建用户相关的注册、编辑和验证页面。最后,读者将把这些页面添加到 vue-router 模式中。
第六章,“创建聊天和消息 Vuex、页面和路由”,读者将继续创建应用程序的 Vuex 模块。现在是创建聊天模块的时候了。这个模块将包含用户之间通信的业务规则和存储聊天数据。最后,用户将创建与聊天列表和聊天页面相关的页面,然后将其添加到 vue-router 模式中。
第七章,“将您的应用程序转变为 PWA 并部署到 Web”,在这最后一章中,读者将通过将应用程序转变为 PWA 应用程序,为 iOS 设备添加更新通知和安装横幅来完成应用程序。最后,用户将把应用程序部署到 Web 上。
为了充分利用本书
本书从 第二章 组件、混合和功能组件 开始使用 Vue.js 2.7,因为这是写作时 Quasar Framework 的最新支持版本。本书将在 第三章 设置我们的聊天应用 - AWS Amplify 环境和 GraphQL 中使用 Vue.js 3 的代码。所有代码将在 GitHub 存储库的最终版本发布时进行更新:github.com/PacktPublishing/Building-Vue.js-Applications-with-GraphQL
您需要安装 Node.js 12+,将 Vue CLI 更新到最新版本,并且需要一个良好的代码编辑器。其他要求将在每个示例中介绍。所有软件要求都适用于 Windows、macOS 和 Linux。
以下是总结所有要求的表格:
章节编号 | 书中涉及的软件/硬件 | 下载链接 | 操作系统要求 |
---|---|---|---|
1 到 7 | Vue CLI 4.X | cli.vuejs.org/ |
Windows / Linux / macOS |
3 到 7 | Quasar-CLI 1.X | quasar.dev/ |
Windows / Linux / macOS |
3 到 7 | Visual Studio Code 1.4.X 和 IntelliJ WebStorm 2020.2 | code.visualstudio.com/ |
Windows / Linux / macOS |
3 到 7 | AWS Amplify CLI 3.3.X | aws.amazon.com/appsync/resources/ |
Windows / Linux / macOS |
1 到 7 | Node.js 12+- | nodejs.org/en/download/ |
Windows / Linux / macOS |
如果您使用的是本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库访问代码(链接在下一部分中提供)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从您在 www.packt.com 的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
在 www.packt.com 上登录或注册。
-
选择 Support 选项卡。
-
点击 Code Downloads。
-
在 Search 框中输入书名,并按照屏幕上的说明操作。
一旦文件下载完成,请确保使用最新版本的解压软件解压文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Building-Vue.js-Applications-with-GraphQL
。如果代码有更新,将在现有的 GitHub 存储库上更新。
我们还有其他代码包,来自我们丰富的图书和视频目录,可以在github.com/PacktPublishing/
上找到。去看看吧!
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 账号。这是一个例子:“为了做到这一点,以管理员身份打开 PowerShell 并执行> npm install -g windows-build-tools
命令。”
一个代码块设置如下:
<template>
<header>
<div id="blue-portal" />
</header>
</header>
任何命令行输入或输出都以以下方式书写:
> npm run serve
粗体:表示一个新术语,一个重要词,或者你在屏幕上看到的词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“点击电子邮件按钮,将被重定向到电子邮件注册表格”
警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。
部分
在本书中,你会经常看到几个标题(准备工作,如何做,它是如何工作的,还有更多,和另请参阅)。
为了清晰地说明如何完成一个食谱,使用以下部分:
准备工作
这一部分告诉你在食谱中可以期待什么,并描述如何设置任何软件或食谱所需的任何初步设置。
如何做…
这一部分包含了遵循食谱所需的步骤。
它是如何工作的…
这一部分通常包括对前一部分发生的事情的详细解释。
还有更多…
这一部分包括了有关食谱的额外信息,以使你对食谱更加了解。
另请参阅
这一部分为食谱提供了其他有用信息的链接。
第一章:数据绑定、事件和计算属性
数据是当今世界上最有价值的资产,知道如何管理它是必须的。在 Vue 中,我们有权利选择如何收集这些数据,按照我们的意愿进行操作,并将其传递到服务器。
在本章中,我们将更多地了解数据处理和数据处理过程,表单验证,数据过滤,如何向用户显示这些数据,以及如何以与应用程序内部不同的方式呈现它。
我们将学习如何使用各种vue-devtools
,以便我们可以深入了解 Vue 组件并查看我们的数据和应用程序发生了什么。
在本章中,我们将涵盖以下配方:
-
使用 Vue CLI 创建您的第一个项目
-
创建 hello world 组件
-
创建具有双向数据绑定的输入表单
-
在元素上添加事件监听器
-
从输入中删除
v-model
指令 -
创建动态待办事项列表
-
创建计算属性并了解它们的工作原理
-
使用自定义过滤器显示更清洁的数据和文本
-
为列表创建过滤器和排序器
-
创建条件过滤器以对列表数据进行排序
-
添加自定义样式和过渡
-
使用
vue-devtools
调试您的应用程序
让我们开始吧!
技术要求
在本章中,我们将使用Node.js和Vue CLI。
注意,Windows 用户 - 您需要安装一个名为windows-build-tools
的npm
包,以便能够安装以下所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行
> npm install -g windows-build-tools
命令。
要安装Vue CLI,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
使用 Vue CLI 创建您的第一个项目
当 Vue 团队意识到开发人员在创建和管理他们的应用程序时遇到问题时,他们看到了一个机会,可以创建一个工具来帮助世界各地的开发人员。有了这个,Vue CLI 项目诞生了。
Vue CLI 工具是一个在 terminal 命令行中使用的 CLI 工具,如 Windows PowerShell、Linux Bash 或 macOS Terminal。它被创建为 Vue 开发的起点,开发人员可以启动一个项目并顺利地管理和构建它。Vue CLI 团队的重点是为开发人员提供更多时间思考代码,花费更少的时间在工具上,将他们的代码投入生产,添加新的插件或简单的 hot-module-reload
。
Vue CLI 工具已经进行了调整,无需在将其投入生产之前将工具代码弹出 CLI。
当版本 3 发布时,Vue UI 项目被添加到 CLI 中作为主要功能,将 CLI 命令转换为更完整的可视解决方案,并增加了许多新的功能和改进。
准备工作
这个配方的先决条件是 Node.js 12+。
这个配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要创建一个 Vue CLI 项目,请按照以下步骤进行:
- 我们需要在 Terminal (macOS 或 Linux) 或 Command Prompt/PowerShell (Windows) 中执行以下命令:
> vue create my-first-project
- CLI 会询问一些问题,这些问题将帮助你创建项目。你可以使用箭头键进行导航,Enter 键继续,Spacebar 选择选项:
? Please pick a preset: (Use arrow keys) default (babel, eslint) ❯ **Manually select features**
-
有两种方法可以启动一个新项目。默认方法是一个基本的
babel
和eslint
项目,没有任何插件或配置,但也有手动
模式,你可以选择更多模式、插件、linters 和选项。我们将选择手动
模式。 -
在这一点上,我们将被询问关于我们希望为我们的项目选择的功能。这些功能是一些 Vue 插件,如 Vuex 或 Router (Vue-Router)、测试器、linters 等。对于这个项目,我们将选择
CSS 预处理器
并按 Enter 继续:
? Check the features needed for your project: (Press <space> to
select, <a> to toggle all, <i> to invert selection)
❯ Choose Vue version
❯ Babel
TypeScript
Progressive Web App (PWA) Support
Router
Vuex
CSS Pre-processors
❯ Linter / Formatter
Unit Testing
E2E Testing
- CLI 会要求你选择一个 Vue 版本来启动你的应用程序。我们将在这里选择
3.x (Preview)
。按 Enter 继续:
? Choose a version of Vue.js that you want to start the project with
(Use arrow keys)
2.x
❯ 3.x (Preview)
- 可以选择与 Vue 一起使用的主要 层叠样式表 (CSS) 预处理器,即
Sass
、Less
和Stylus
。由你选择哪种最适合你的设计并且最适合你:
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules
are supported by default): (Use arrow keys) Sass/SCSS (with dart-sass) Sass/SCSS (with node-sass) **Less** ❯ Stylus
- 现在是时候格式化您的代码了。您可以在
AirBnB
、Standard
和Prettier
之间进行选择,并使用基本配置。在ESLint
中导入的这些规则总是可以自定义,没有任何问题,并且有一个完美的规则适合您的需求。找出对您来说最好的方法,然后执行以下操作:
? Pick a linter / formatter config: (Use arrow keys) ESLint with error prevention only ❯ **ESLint + Airbnb config** ESLint + Standard config
ESLint + Prettier
- 一旦代码检查规则被设置,我们需要定义它们何时应用于我们的代码。它们可以在保存时应用,或者在提交时进行修复:
? Pick additional lint features:
Lint on save
❯ Lint and fix on commit
- 一旦所有这些插件、代码检查器和处理器都被定义,我们需要选择设置和配置存储的位置。最好的存储位置是在一个专用文件中,但也可以将它们存储在
package.json
文件中:
? Where do you prefer placing config for Babel, ESLint, etc.? (Use
arrow keys) ❯ **In dedicated config files** In package.json
- 现在,您可以选择是否将此选择作为将来项目的预设,这样您就不需要再次重新选择所有内容。
? Save this as a preset for future projects? (y/N) n
- CLI 将自动创建以步骤 1中设置的名称命名的文件夹,安装所有内容并配置项目。
有了这些,现在您可以导航并运行项目了。Vue CLI 项目的基本命令如下:
-
npm run serve
:在本地运行开发服务器 -
npm run build
:用于构建和缩小应用程序以进行部署 -
npm run lint
:对代码执行 lint
您可以通过终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)执行这些命令。
还有更多...
CLI 内部有一个名为 Vue UI 的工具,可帮助您管理 Vue 项目。这个工具将负责项目的依赖关系、插件和配置。
Vue UI 工具中的每个npm
脚本都被称为一个任务,在这些任务中,您可以收集实时统计数据,如资产、模块和依赖项的大小;错误或警告的数量;以及更深入的网络数据,以微调您的应用程序。
要进入 Vue UI 界面,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue ui
另请参阅
-
您可以在
cli.vuejs.org/guide/
找到有关 Vue CLI 项目的更多信息。 -
你可以在
cli.vuejs.org/dev-guide/plugin-dev.html
找到有关 Vue CLI 插件开发的更多信息。
创建 hello world 组件
Vue 应用程序是由各种组件组合在一起,并由 Vue 框架编排的。知道如何制作您的组件是很重要的。每个组件就像墙上的一块砖,需要以一种方式制作,当放置时,不需要其他砖块以不同的方式重新塑造。在这个教程中,我们将学习如何制作一个基础组件,同时遵循一些重要的原则,重点放在组织和清晰的代码上。
准备工作
本教程的先决条件是 Node.js 12+。
本教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要开始我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像我们在使用 Vue CLI 创建你的第一个项目中学到的那样,或者开始一个新的项目。
操作步骤...
要开始一个新的组件,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create my-component
命令行界面(CLI)将询问一些问题,这些问题将帮助您创建项目。您可以使用箭头键导航,使用Enter键继续,使用Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
通过以下步骤创建我们的第一个hello world
组件:
-
让我们在
src/components
文件夹中创建一个名为CurrentTime.vue
的新文件。 -
在这个文件中,我们将从组件的
<template>
部分开始。它将是一个阴影框卡片,显示当前日期,格式化:
<template>
<div class='cardBox'>
<div class='container'>
<h2>Today is:</h2>
<h3>{{ getCurrentDate }}</h3>
</div>
</div>
</template>
- 现在,我们需要创建
<script>
部分。我们将从name
属性开始。这将在使用vue-devtools
调试我们的应用程序时使用,以识别我们的组件,并帮助集成开发环境(IDE)。对于getCurrentDate
计算属性,我们将创建一个computed
属性,它将返回当前日期,由Intl
浏览器函数格式化:
<script>
export default {
name: 'CurrentTime',
computed: {
getCurrentDate() {
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date());
}
}
};
</script>
- 为了为我们的盒子设置样式,我们需要在
src
文件夹中创建一个style.css
文件,然后将cardBox
样式添加到其中:
.cardBox {
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s linear;
max-width: 33%;
border-radius: 3px;
margin: 20px;
}
.cardBox:hover {
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
}
.cardBox>.container {
padding: 4px 18px;
}
[class*='col-'] {
display: inline-block;
}
@media only screen and (max-width: 600px) {
[class*='col-'] {
width: 100%;
}
.cardBox {
margin: 20px 0;
}
}
@media only screen and (min-width: 600px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 768px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 992px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 1200px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
- 在
App.vue
文件中,我们需要导入我们的组件,这样我们才能看到它:
<template>
<div id='app'>
<current-time />
</div>
</template>
<script>
import CurrentTime from './components/CurrentTime.vue';
export default {
name: 'app',
components: {
CurrentTime
}
}
</script>
- 在
main.js
文件中,我们需要导入style.css
文件,以便它包含在 Vue 应用程序中:
import { createApp } from 'vue'; import './style.css'; import App from './App.vue'; createApp(App).mount('#app');
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
Vue 组件几乎与 Node.js 包一样工作。要在代码中使用它,您需要导入组件,然后在要使用的组件的components
属性中声明它。
就像一堵砖墙,Vue 应用程序由调用和使用其他组件的组件组成。
对于我们的组件,我们使用了Intl.DateTimeFormat
函数,这是一个本机函数,可用于将日期格式化和解析为声明的位置。为了获得本地格式,我们使用了 navigator 全局变量。
另请参阅
-
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。 -
您可以在
v3.vuejs.org/guide/single-file-component.html
找到有关 Vue 组件的更多信息。
使用双向数据绑定创建输入表单
在网上收集数据,我们使用 HTML 表单输入。在 Vue 中,可以使用双向数据绑定方法,其中 DOM 上输入的值传递给 JavaScript,反之亦然。
这使得 Web 表单更加动态,使您有可能在保存或将数据发送回服务器之前管理、格式化和验证数据。
准备工作
此配方的先决条件是 Node.js 12+。
此配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要启动我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像我们在使用 Vue CLI 创建您的第一个项目配方中学到的那样,或者使用创建 hello world 组件配方中的项目。
如何做...
按照以下步骤创建具有双向数据绑定的输入表单:
-
让我们在
src/components
文件夹中创建一个名为TaskInput.vue
的新文件。 -
在这个文件中,我们将创建一个组件,它将有一个文本输入和一些显示文本。这个文本将基于文本输入中键入的内容。在组件的
<template>
部分,我们需要创建一个 HTML 输入和一个mustache
变量,用于接收和呈现数据:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is: {{ task }}</strong>
<input
type='text'
v-model='task'
class='taskInput' />
</div>
</div>
</template>
- 现在,在组件的
<script>
部分,我们将对其命名并将任务添加到data
属性中。由于数据始终需要返回一个Object
,我们将使用箭头函数直接返回一个Object
:
<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
};
</script>
- 我们需要为这个组件添加一些样式。在组件的
<style>
部分,我们需要添加scoped
属性,以便样式仅保持在组件中,不会与其他层叠样式表(CSS)规则混合:
<style scoped>
.tasker{
margin: 20px;
}
.tasker .taskInput {
font-size: 14px;
margin: 0 10px;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.75);
}
.tasker button {
border: 1px solid rgba(0, 0, 0, 0.75);
border-radius: 3px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
</style>
- 现在,我们需要将这个组件导入到我们的
App.vue
文件中:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' />
</div> </template> <script> import CurrentTime from './components/CurrentTime.vue'; import TaskInput from './components/TaskInput.vue'; export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput,
}, }; </script>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
当您创建一个 HTMLinput
元素并为其添加v-model
时,您正在传递一个内置到 Vue 中的指令,该指令检查输入类型并为输入提供糖语法。这处理更新变量和 DOM 的值。
这个模型被称为双向数据绑定。如果变量被代码更改,DOM 将重新渲染,如果它被 DOM 通过用户输入更改,比如input-form
,那么 JavaScript 代码可以执行一个函数。
另请参阅
您可以在v3.vuejs.org/guide/forms.html
找到有关表单输入绑定的更多信息。
向元素添加事件侦听器
在 Vue 中,父子通信的最常见方法是通过 props 和 events。在 JavaScript 中,通常会向 DOM 树的元素添加事件侦听器,以在特定事件上执行函数。在 Vue 中,可以添加监听器并根据需要命名,而不是坚持 JavaScript 引擎上存在的名称。
在这个配方中,我们将学习如何创建自定义事件以及如何发出它们。
准备工作
这个配方的先决条件是 Node.js 12+。
这个配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要启动我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像我们在使用 Vue CLI 创建您的第一个项目配方中学到的那样,或者使用使用双向数据绑定创建输入表单配方中的项目。
如何做...
按照以下步骤为 Vue 中的元素添加事件监听器:
-
创建一个新组件或打开
TaskInput.vue
文件。 -
在
<template>
部分,我们将添加一个按钮元素,并使用v-on
指令为按钮点击事件添加一个事件监听器。我们将从组件中删除{{ task }}
变量,因为从现在开始,它将被发出并且不再显示在组件上:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
v-model='task'
class='taskInput' />
<button
v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
- 在组件的
<script>
部分,我们需要添加一个处理点击事件的方法。这个方法将被命名为addTask
。它将发出一个名为add-task
的事件,并将任务发送到数据中。之后,组件上的任务将被重置:
<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
methods: {
addTask(){
this.$emit('add-task', this.task);
this.task = '';
},
}
};
</script>
- 在
App.vue
文件中,我们需要为组件添加一个事件监听器绑定。这个监听器将附加到add-task
事件上。我们将使用v-on
指令的缩写版本@
。当它被触发时,事件将调用addNewTask
方法,该方法将发送一个警报,说明已添加了一个新任务:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input
class='col-6'
@add-task='addNewTask'
/>
</div>
</template>
- 现在,让我们创建
addNewTask
方法。这将接收任务作为参数,并向用户显示一个警报,说明已添加了任务:
<script> import CurrentTime from './components/CurrentTime.vue'; import TaskInput from './components/TaskInput.vue'; export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput,
},
methods: {
addNewTask(task) {
alert(`New task added: ${task}`);
},
}, }; </script>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
以下是渲染和运行的组件:
它是如何工作的...
Vue 使用v-on
事件处理指令来读取 HTML 事件。当我们将v-on:click
指令附加到按钮时,我们为按钮添加了一个监听器,以便在用户单击按钮时执行一个函数。
该函数在组件方法中声明。当调用此函数时,将发出一个事件,表示任何使用此组件作为子组件的组件都可以使用v-on
指令监听它。
另请参阅
您可以在v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息。
从输入中删除 v-model 指令
如果我告诉你,在v-model
的魔术背后,有很多代码使我们的魔术糖语法发生?如果我告诉你,兔子洞可以深入到足以控制输入的事件和值的一切?
在这个配方中,我们将学习如何提取v-model
指令的糖语法,并将其转换为其背后的基本语法。
准备工作
这个配方的先决条件是 Node.js 12+。
本配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要启动我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像我们在使用 Vue CLI 创建您的第一个项目配方中学到的那样,或者使用向元素添加事件监听器配方中的项目。
如何操作...
通过执行以下步骤,我们将从输入中删除v-model
指令的糖语法:
-
打开
TaskInput.vue
文件。 -
在组件的
<template>
块中,找到v-model
指令。我们需要删除v-model
指令。然后,我们需要向输入添加一个新的绑定,称为v-bind:value
或缩写版本:value
,以及一个事件监听器到 HTMLinput
元素。我们需要在input
事件上添加一个事件监听器,使用v-on:input
指令或缩写版本@input
。输入绑定将接收任务值作为参数,事件监听器将接收一个值赋值,其中它将使任务变量等于事件值的值:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
:value='task'
@input='task = $event.target.value'
class='taskInput'
/>
<button v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
记得始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
它是如何工作的...
作为一种语法糖,v-model
指令可以自动声明绑定和事件监听器到元素中。然而,副作用是你无法完全控制可以实现什么。
正如我们所见,绑定的值可以是变量、方法、计算属性或 Vuex getter 等。在事件监听器方面,它可以是一个函数或直接声明一个变量赋值。当事件被触发并传递给 Vue 时,$event
变量用于传递事件。在这种情况下,与普通 JavaScript 一样,要捕获输入的值,我们需要使用event.target.value
值。
另请参阅
您可以在v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息。
创建一个动态的待办事项列表
每个程序员在学习一门新语言时创建的第一个项目之一就是待办事项列表。这样做可以让我们更多地了解在处理状态和数据时遵循的语言流程。
我们将使用 Vue 制作我们的待办事项列表。我们将使用之前教程中学到的知识和创建的内容。
准备工作
这个教程的先决条件是 Node.js 12+。
这个教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要启动我们的组件,我们可以使用 Vue CLI 创建 Vue 项目,就像我们在使用 Vue CLI 创建第一个项目这个教程中学到的那样,或者使用从输入中删除 v-model 指令这个教程中的项目。
如何做...
制作待办事项应用程序涉及一些基本原则-它必须包含一个任务列表,任务可以标记为已完成和未完成,并且列表可以进行过滤和排序。现在,我们将学习如何将任务添加到任务列表中。
按照以下步骤使用 Vue 和从之前教程中获得的信息创建一个动态的待办事项列表:
- 在
App.vue
文件中,我们将创建我们的任务数组。每当TaskInput.vue
组件发出消息时,这个任务将被填充。我们将向这个数组添加一个包含任务以及任务创建的当前日期的对象。目前,任务完成的日期将被留空。为了做到这一点,在组件的<script>
部分,我们需要创建一个接收任务并将任务与当前日期添加到taskList
数组中的方法:
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput.vue';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput,
},
data: () => ({
taskList: [],
}),
methods:{
addNewTask(task){
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined,
})
},
},
}
</script>
- 现在,我们需要在
<template>
部分渲染这个列表。我们将使用 Vue 的v-for
指令来遍历任务列表。当我们将这个指令与数组一起使用时,它会给我们访问两个属性-项目本身和项目的索引。我们将使用项目本身进行渲染,使用索引来创建元素的键以进行渲染过程。我们需要添加一个复选框,当选中时,调用一个改变任务状态的函数,并显示任务完成的时间:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in taskList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
始终重要的是要记住迭代器中的键必须是唯一的。这是因为render
函数需要知道哪些元素已更改。在此示例中,我们添加了Math.random()
函数到索引中以生成唯一键,因为当减少元素数量时,数组的第一个元素的索引始终是相同的数字。
- 我们需要在
App.vue
文件的methods
属性上创建changeStatus
函数。此函数将接收任务的索引作为参数,然后转到任务数组并更改finishedAt
属性,这是我们标记任务完成的标记。
changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
- 现在,我们需要将任务文本添加到屏幕左侧。在组件的
<style>
部分,我们将使其具有作用域并添加自定义类:
<style scoped>
.taskList li{
text-align: left;
}
</style>
- 运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
当我们从组件接收到发射的消息时,我们使用更多数据对消息进行了处理,并将其推送到本地数组变量中。
在模板中,我们迭代此数组,将其转换为任务列表。这显示了我们需要完成的任务、标记任务完成的复选框以及任务完成的时间。
当用户单击复选框时,它会执行一个函数,该函数将当前任务标记为已完成。如果任务已经完成,该函数将将finishedAt
属性设置为undefined
。
另请参阅
-
您可以在
v3.vuejs.org/guide/list.html#mapping-an-array-to-elements-with-v-for
找到有关列表渲染的更多信息。 -
您可以在
v3.vuejs.org/guide/conditional.html#v-if
找到有关条件渲染的更多信息。 -
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
找到有关Math.random
的更多信息。
创建计算属性并了解其工作原理
想象一下,每次您需要获取处理过的数据时,您都需要执行一个函数。想象一下,您需要获取需要经过一些处理的特定数据,并且您需要每次通过函数执行它。这种类型的工作不容易维护。计算属性存在是为了解决这些问题。使用计算属性使得更容易获取需要预处理甚至缓存的数据,而无需执行任何其他外部记忆函数。
准备工作
这个配方的先决条件是 Node.js 12+。
此处所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
您可以继续进行我们的待办事项项目,或者按照我们在使用 Vue CLI 创建您的第一个项目中学到的内容创建一个新的 Vue 项目。
操作步骤
按照以下步骤创建一个计算属性并了解它的工作原理:
- 在
App.vue
文件的<script>
部分,我们将在data
和method
之间添加一个新属性,称为computed
。这是computed
属性将被放置的地方。我们将创建一个名为displayList
的新计算属性,用于在模板上呈现最终列表:
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput.vue';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList(){
return this.taskList;
},
},
methods: {
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
目前,displayList
属性只是返回变量的缓存值,而不是直接的变量本身。
- 现在,对于
<template>
部分,我们需要改变列表的获取位置:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
记得始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
工作原理
当使用computed
属性将一个值传递给模板时,这个值现在被缓存。这意味着只有在值更新时才会触发渲染过程。同时,我们确保模板不使用变量进行渲染,以便它不能在模板上更改,因为它是变量的缓存副本。
使用这个过程,我们可以获得最佳的性能,因为我们不会浪费处理时间重新渲染对数据显示没有影响的更改的 DOM 树。这是因为如果有什么变化,结果是一样的,computed
属性会缓存结果,并且不会更新最终结果。
另请参阅
你可以在v3.vuejs.org/guide/computed.html
找到更多关于计算属性的信息。
使用自定义过滤器显示更清晰的数据和文本
有时,您可能会发现用户,甚至您自己,无法阅读 Unix 时间戳或其他DateTime
格式。我们如何解决这个问题?在 Vue 中呈现数据时,可以使用我们称之为过滤器的东西。
想象一系列数据通过的管道。数据以一种形式进入每个管道,以另一种形式退出。这就是 Vue 中的过滤器的样子。您可以在同一个变量上放置一系列过滤器,以便对其进行格式化、重塑,并最终以不同的数据显示,而代码保持不变。在这些管道中,初始变量的代码是不可变的。
准备工作
本教程的先决条件是 Node.js 12+。
本教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
我们可以继续进行我们的待办事项项目,或者按照我们在使用 Vue CLI 创建您的第一个项目教程中学到的内容创建一个新的 Vue 项目。
如何做...
按照以下步骤创建您的第一个自定义 Vue 过滤器:
- 在
App.vue
文件中,在<script>
部分,在方法中,创建一个formatDate
函数。这个函数将接收value
作为参数并输入过滤器管道。我们可以检查value
是否是一个数字,因为我们知道我们的时间是基于 Unix 时间戳格式的。如果它是一个数字,我们将根据当前浏览器位置进行格式化,并返回该格式化的值。如果值不是一个数字,我们只是返回传递的值。
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput.vue';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList() {
return this.taskList;
}
},
methods: {
formatDate(value) {
if (!value) return '';
if (typeof value !== 'number') return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 对于组件的
<template>
部分,我们需要将变量传递给过滤器方法。为了做到这一点,我们需要找到taskItem.finishedAt
属性,并将其作为formatDate
方法的参数。我们将添加一些文本来表示任务是在日期的开头“完成于:”。
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
过滤器是接收一个值并必须返回一个值以在文件的<template>
部分中显示或在 Vue 属性中使用的方法。
当我们将值传递给formatDate
方法时,我们知道它是一个有效的 Unix 时间戳,因此可以调用一个新的Date
类构造函数,将value
作为参数传递,因为 Unix 时间戳是一个有效的日期构造函数。
我们过滤器背后的代码是Intl.DateTimeFormat
函数,这是一个本地函数,可用于格式化和解析日期到指定的位置。要获取本地格式,我们可以使用全局变量navigator
。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。
为列表创建过滤器和排序器
在处理列表时,通常会遇到原始数据。有时,您需要对这些数据进行过滤,以便用户可以阅读。为此,我们需要一组计算属性来形成最终的过滤器和排序器。
在这个教程中,我们将学习如何创建一个简单的过滤器和排序器,来控制我们最初的待办任务列表。
准备工作
这个教程的先决条件是 Node.js 12+。
本教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
我们可以继续进行待办事项列表项目,或者按照我们在使用 Vue CLI 创建您的第一个项目教程中学到的内容,创建一个新的 Vue 项目。
如何做...
按照以下步骤为您的列表添加一组过滤器和排序器:
- 在
App.vue
文件的<script>
部分,我们将添加新的计算属性;这些将用于排序和过滤。我们将添加三个新的计算属性:baseList
,filteredList
和sortedList
。baseList
属性将是我们的第一个操作。我们将通过Array.map
向任务列表添加一个id
属性。由于 JavaScript 数组从零开始,我们将在数组的索引上添加1
。filteredList
属性将过滤baseList
属性,并返回未完成的任务,而sortedList
属性将对filteredList
属性进行排序,以便最后添加的id
属性将首先显示给用户:
<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return [...this.baseList]
.filter(t => !t.finishedAt);
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => b.id - a.id);
},
displayList() {
return this.sortedList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 在
<template>
部分,我们将添加Task ID
并更改changeStatus
方法发送参数的方式。因为索引现在是可变的,我们不能将其用作变量;它只是数组上的临时索引。我们需要使用任务的id
:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 我们还需要更新
changeStatus
方法中的函数。由于索引现在从1
开始,我们需要将数组的索引减一,以便在更新之前获得元素的真实索引:
changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
- 要运行服务器并查看组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
记得始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
computed
属性一起工作,作为列表的缓存,并确保在操作元素时没有副作用:
-
对于
baseList
属性,我们创建了一个具有相同任务的新数组,但为任务添加了一个新的id
属性。 -
对于
filteredList
属性,我们使用baseList
属性,只返回未完成的任务。 -
对于
sortedList
属性,我们按照它们的 ID,按降序对filteredList
属性上的任务进行排序。
当所有操作完成时,displayList
属性返回了被操作的数据的结果。
另请参阅
-
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。 -
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。 -
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。
创建条件过滤器以对列表数据进行排序
现在您已经完成了上一个食谱,您的数据应该被过滤和排序,但您可能需要检查过滤后的数据或需要更改排序方式。在这个食谱中,您将学习如何创建条件过滤器并对列表上的数据进行排序。
使用一些基本原则,可以收集信息并以许多不同的方式显示它。
准备就绪
此食谱的先决条件是 Node.js 12+。
此食谱所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
我们可以继续进行我们的待办事项列表项目,或者按照我们在 使用 Vue CLI 创建您的第一个项目 食谱中学到的内容创建一个新的 Vue 项目。
如何做...
按照以下步骤添加条件过滤器以对列表数据进行排序:
- 在
App.vue
文件的<script>
部分,我们将更新computed
属性;即filteredList
、sortedList
和displayList
。我们需要向我们的项目添加三个新变量:hideDone
、reverse
和sortById
。所有三个变量都将是布尔变量,并且将以默认值false
开始。filteredList
属性将检查hideDone
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将显示完整的列表而不进行过滤。sortedList
属性将检查sortById
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将按任务的完成日期对列表进行排序。最后,displayList
属性将检查reverse
变量是否为true
。如果是,它将颠倒显示的列表,但如果不是,它将具有相同的行为:
<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
hideDone: false,
reverse: false,
sortById: false,
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return this.hideDone
? [...this.baseList]
.filter(t => !t.finishedAt)
: [...this.baseList];
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => (
this.sortById
? b.id - a.id
: (a.finishedAt || 0) - (b.finishedAt || 0)
));
},
displayList() {
const taskList = [...this.sortedList];
return this.reverse
? taskList.reverse()
: taskList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 对于
<template>
部分,我们需要为这些变量添加控制器。我们将创建三个复选框,直接通过v-model
指令与变量链接:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令 npm run lint --fix
,以自动修复任何代码 lint 错误。
组件已呈现并运行:
工作原理...
computed
属性一起作为列表的缓存,并确保在操作元素时没有任何副作用。通过条件过程,可以通过变量更改过滤和排序过程的规则,并且显示会实时更新:
-
对于
filteredList
属性,我们取baseList
属性并仅返回未完成的任务。当hideDone
变量为false
时,我们返回完整的列表而不进行任何过滤。 -
对于
sortedList
属性,我们对filteredList
属性上的任务进行了排序。当sortById
变量为true
时,列表按 ID 降序排序;当为false
时,按任务的完成时间升序排序。 -
对于
displayList
属性,当reverse
变量为true
时,最终列表被反转。
当所有操作完成时,displayList
属性将返回被操作的数据的结果。
这些computed
属性由用户屏幕上的复选框控制,因此用户可以完全控制他们可以看到什么以及如何看到它。
另请参阅
-
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。 -
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。 -
您可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。
添加自定义样式和过渡效果
向您的组件添加样式是一个很好的做法,因为它可以让您更清楚地向用户展示发生了什么。通过这样做,您可以向用户显示视觉响应,并为他们提供更好的应用体验。
在这个示例中,我们将学习如何添加一种新的条件类绑定。我们将使用混合了 CSS 效果和每个新的 Vue 更新带来的重新渲染。
准备工作
此示例的先决条件是 Node.js 12+。
此处所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
我们可以继续进行待办事项列表项目,或者使用 Vue CLI 创建一个新的 Vue 项目,就像我们在使用 Vue CLI 创建您的第一个项目中学到的那样。
如何做...
按照以下步骤为您的组件添加自定义样式和过渡效果:
- 在
App.vue
文件中,我们将为已完成的任务的列表项添加一个条件类:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
:class="!!taskItem.finishedAt ? 'taskDone' : ''"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 对于组件的
<style>
部分,我们将为taskDone
CSS 类创建 CSS 样式表类。我们需要使列表在项目之间有一个分隔符;然后,我们将使列表具有条纹样式。当它们被标记为已完成时,背景将带有一个效果。为了在行和条纹列表或斑马样式之间添加分隔符,我们需要添加一个 CSS 规则,该规则适用于我们列表的每个even nth-child
:
<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
</style>
- 要在任务完成时将效果添加到背景中,在
<style>
部分的末尾,我们将添加一个 CSS 动画关键帧,指示背景颜色的变化,并将此动画应用于.taskDone
CSS 类:
<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
@keyframes colorChange {
from{
background-color: inherit;
}
to{
background-color: rgba(0, 160, 24, 0.577);
}
}
.taskList li.taskDone{
animation: colorChange 1s ease;
background-color: rgba(0, 160, 24, 0.577);
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
工作原理...
每当我们的应用程序中的新项目被标记为已完成时,displayList
属性都会更新并触发组件的重新渲染。
因此,我们的taskDone
CSS 类附加了一个动画,该动画在渲染时执行,显示绿色背景。
参见
-
您可以在
developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
找到有关 CSS 动画的更多信息。 -
您可以在
v3.vuejs.org/guide/class-and-style.html
找到有关类和样式绑定的更多信息
使用 vue-devtools 调试您的应用程序
vue-devtools
对于每个 Vue 开发人员都是必不可少的。这个工具向我们展示了 Vue 组件、路由、事件和 Vuex 的深度。
借助vue-devtools
扩展,可以调试我们的应用程序,在更改代码之前尝试新数据,执行函数而无需直接在代码中调用它们,等等。
在本教程中,我们将学习如何使用各种开发工具来了解我们的应用程序,并了解它们如何帮助我们的调试过程。
准备工作
本教程的先决条件是 Node.js 12+。
本教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
您需要在浏览器中安装vue-devtools
扩展程序:
-
Chrome 扩展程序:
bit.ly/chrome-vue-devtools
-
Firefox 扩展程序:
bit.ly/firefox-vue-devtools
我们可以继续进行我们的待办事项列表项目,或者使用 Vue CLI 创建一个新的 Vue 项目,就像我们在使用 Vue CLI 创建您的第一个项目中学到的那样。
如何做...
在开发任何 Vue 应用程序时,始终将vue-devtools
作为良好的实践进行开发。
按照以下步骤了解如何使用vue-devtools
以及如何正确调试 Vue 应用程序:
- 要进入
vue-devtools
,您需要在浏览器中安装它,因此请查看本教程的准备就绪部分,获取 Chrome 或 Firefox 扩展程序的链接。在您的 Vue 开发应用程序中,进入浏览器开发者检查器模式。将出现一个名为 Vue 的新标签页:
- 您将看到的第一个标签页是组件标签页。此标签显示应用程序组件树。如果单击组件,您将能够查看所有可用数据,计算属性以及插件(如
vuelidate
,vue-router
或vuex
)注入的额外数据。您可以编辑此数据以实时查看应用程序中的更改:
- 第二个标签页用于 Vuex 开发。此标签将显示变化的历史记录,当前状态和获取器。可以检查每个变化的传递负载,并进行时间旅行变化,以回到过去并查看状态中的 Vuex 更改:
- 第三个标签页专门用于应用程序中的事件发射器。在此处显示应用程序中发射的所有事件。您可以通过单击事件来检查发射的事件。通过这样做,您可以看到事件的名称,类型,事件源(在本例中是组件)以及负载:
- 第四个标签页专门用于 vue-router 插件。在那里,您可以查看其导航历史,以及传递给新路由的所有元数据。这是您可以检查应用程序中所有可用路由的地方:
- 第五个选项卡是性能选项卡。在这里,您可以检查组件的加载时间以及应用程序实时运行的每秒帧数。以下屏幕截图显示了当前应用程序的每秒帧数,以及所选组件的每秒帧数:
以下屏幕截图显示了组件的生命周期钩子性能以及执行每个钩子所需的时间:
- 第六个选项卡是您的设置选项卡。在这里,您可以管理扩展程序并更改其外观,内部行为以及在 Vue 插件中的行为:
- 最后一个选项卡是
vue-devtools
的刷新按钮。有时,当发生热模块重新加载
或当应用程序组件树中发生一些复杂事件时,扩展程序可能会丢失对发生情况的跟踪。此按钮强制扩展程序重新加载并再次读取 Vue 应用程序状态。
另请参阅
您可以在github.com/vuejs/vue-devtools
找到有关vue-devtools
的更多信息。
第二章:组件、混合和功能性组件
构建 Vue 应用就像拼图一样。每个拼图的一部分都是一个组件,每个拼图都有一个槽要填充。
组件在 Vue 开发中扮演着重要角色。在 Vue 中,你的代码的每一部分都将是一个组件 - 它可以是布局、页面、容器或按钮,但最终,它都是一个组件。学习如何与它们交互和重用它们是清理代码和提高 Vue 应用性能的关键。组件是最终会在屏幕上渲染出东西的代码,无论它的大小是多少。
在这一章中,我们将学习如何制作一个可在多个地方重复使用的可视化组件。我们将使用插槽在组件内放置数据,为了快速渲染创建功能性组件,实现父子组件之间的直接通信,并异步加载我们的组件。
然后,我们将把所有这些部分放在一起,创建一个既是美丽的拼图又是 Vue 应用的拼图。
在这一章中,我们将涵盖以下示例:
-
创建一个可视化模板组件
-
使用插槽和命名插槽在组件内放置数据
-
向您的组件传递数据并验证数据
-
创建功能性组件
-
访问子组件的数据
-
创建一个动态注入组件
-
创建一个依赖注入组件
-
创建一个
mixin
组件 -
延迟加载您的组件
让我们开始吧!
技术要求
在这一章中,我们将使用Node.js和Vue-CLI。
注意 Windows 用户:您需要安装一个名为windows-build-tools
的npm
包才能安装所需的包。为此,请以管理员身份打开 PowerShell 并执行> npm install -g windows-build-tools
命令。
要安装Vue CLI,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
创建一个可视化模板组件
组件可以是数据驱动的、无状态的、有状态的或简单的可视化组件。但是什么是可视化组件?可视化组件是一个只有一个目的的组件:视觉操作。
一个可视化组件可以有一个简单的作用域 CSS 和一些div
HTML 元素,或者它可以是一个更复杂的组件,可以实时计算元素在屏幕上的位置。
在这个示例中,我们将创建一个遵循 Material Design 指南的卡片包装组件。
准备工作
此示例的先决条件是 Node.js 12+。
此示例所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们需要使用 Vue CLI 创建一个新的 Vue 项目。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create visual-component
CLI 将询问一些问题,这些问题将帮助您创建项目。您可以使用箭头键导航,使用Enter键继续,并使用空格键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
现在,按照以下步骤创建一个视觉模板组件:
-
在
src/components
文件夹中创建一个名为MaterialCardBox.vue
的新文件。 -
在此文件中,我们将开始处理组件的模板。我们需要为卡片创建一个框。通过使用 Material Design 指南,此框将具有阴影和圆角:
<template>
<div class="cardBox elevation_2">
<div class="section">
This is a Material Card Box
</div>
</div>
</template>
- 在我们组件的
<script>
部分中,我们将只添加我们的基本名称:
<script>
export default {
name: 'MaterialCardBox',
};
</script>
- 我们需要创建我们的高程 CSS 规则。为此,请在
style
文件夹中创建一个名为elevation.css
的文件。在那里,我们将创建从0
到24
的高程,以便我们可以遵循 Material Design 指南提供的所有高程:
.elevation_0 {
border: 1px solid rgba(0, 0, 0, 0.12);
}
.elevation_1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2),
0 1px 1px rgba(0, 0, 0, 0.14),
0 2px 1px -1px rgba(0, 0, 0, 0.12);
}
.elevation_2 {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.elevation_3 {
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2),
0 3px 4px rgba(0, 0, 0, 0.14),
0 3px 3px -2px rgba(0, 0, 0, 0.12);
}
.elevation_4 {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 4px 5px rgba(0, 0, 0, 0.14),
0 1px 10px rgba(0, 0, 0, 0.12);
}
.elevation_5 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
}
.elevation_6 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 6px 10px rgba(0, 0, 0, 0.14),
0 1px 18px rgba(0, 0, 0, 0.12);
}
.elevation_7 {
box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2),
0 7px 10px 1px rgba(0, 0, 0, 0.14),
0 2px 16px 1px rgba(0, 0, 0, 0.12);
}
.elevation_8 {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.elevation_9 {
box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.2),
0 9px 12px 1px rgba(0, 0, 0, 0.14),
0 3px 16px 2px rgba(0, 0, 0, 0.12);
}
.elevation_10 {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12);
}
.elevation_11 {
box-shadow: 0 6px 7px -4px rgba(0, 0, 0, 0.2),
0 11px 15px 1px rgba(0, 0, 0, 0.14),
0 4px 20px 3px rgba(0, 0, 0, 0.12);
}
.elevation_12 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 12px 17px 2px rgba(0, 0, 0, 0.14),
0 5px 22px 4px rgba(0, 0, 0, 0.12);
}
.elevation_13 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
}
.elevation_14 {
box-shadow: 0 7px 9px -4px rgba(0, 0, 0, 0.2),
0 14px 21px 2px rgba(0, 0, 0, 0.14),
0 5px 26px 4px rgba(0, 0, 0, 0.12);
}
.elevation_15 {
box-shadow: 0 8px 9px -5px rgba(0, 0, 0, 0.2),
0 15px 22px 2px rgba(0, 0, 0, 0.14),
0 6px 28px 5px rgba(0, 0, 0, 0.12);
}
.elevation_16 {
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
0 16px 24px 2px rgba(0, 0, 0, 0.14),
0 6px 30px 5px rgba(0, 0, 0, 0.12);
}
.elevation_17 {
box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2),
0 17px 26px 2px rgba(0, 0, 0, 0.14),
0 6px 32px 5px rgba(0, 0, 0, 0.12);
}
.elevation_18 {
box-shadow: 0 9px 11px -5px rgba(0, 0, 0, 0.2),
0 18px 28px 2px rgba(0, 0, 0, 0.14),
0 7px 34px 6px rgba(0, 0, 0, 0.12);
}
.elevation_19 {
box-shadow: 0 9px 12px -6px rgba(0, 0, 0, 0.2),
0 19px 29px 2px rgba(0, 0, 0, 0.14),
0 7px 36px 6px rgba(0, 0, 0, 0.12);
}
.elevation_20 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 20px 31px 3px rgba(0, 0, 0, 0.14),
0 8px 38px 7px rgba(0, 0, 0, 0.12);
}
.elevation_21 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 21px 33px 3px rgba(0, 0, 0, 0.14),
0 8px 40px 7px rgba(0, 0, 0, 0.12);
}
.elevation_22 {
box-shadow: 0 10px 14px -6px rgba(0, 0, 0, 0.2),
0 22px 35px 3px rgba(0, 0, 0, 0.14),
0 8px 42px 7px rgba(0, 0, 0, 0.12);
}
.elevation_23 {
box-shadow: 0 11px 14px -7px rgba(0, 0, 0, 0.2),
0 23px 36px 3px rgba(0, 0, 0, 0.14),
0 9px 44px 8px rgba(0, 0, 0, 0.12);
}
.elevation_24 {
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14),
0 9px 46px 8px rgba(0, 0, 0, 0.12);
}
- 在组件的
<style>
部分中为我们的卡片设置样式,我们需要在<style>
标签内设置scoped
属性。这确保了视觉样式不会干扰应用程序中的任何其他组件。我们将使此卡片遵循 Material Design 指南。我们需要导入Roboto
字体系列并将其应用于此组件内部的所有元素:
<style scoped>
@import url('https://fonts.googleapis.com/css?
family=Roboto:400,500,700&display=swap');
@import '../style/elevation.css';
* {
font-family: 'Roboto', sans-serif;
}
.cardBox {
width: 100%;
max-width: 300px;
background-color: #fff;
position: relative;
display: inline-block;
border-radius: 0.25rem;
}
.cardBox > .section {
padding: 1rem;
position: relative;
}
</style>
- 在
App.vue
文件中,我们需要导入我们的组件以便能够看到它:
<template>
<div id='app'>
<material-card-box />
</div>
</template>
<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
export default {
name: 'app',
components: {
MaterialCardBox
}
}
</script>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
工作原理...
视觉组件是一个将包装任何组件并将包装数据与自定义样式放在一起的组件。由于此组件与其他组件混合,因此它可以形成一个新的组件,而无需您在代码中重新应用或重写任何样式。
另请参阅
-
关于 Scoped CSS 的更多信息,请访问
vue-loader.vuejs.org/guide/scoped-css.html#child-component-root-elements
。 -
关于 Material Design 卡片的更多信息,请访问
material.io/components/cards/
。 -
查看
fonts.google.com/specimen/Roboto
上的 Roboto 字体系列。
使用插槽和命名插槽将数据放入组件中
有时,拼图的一些部分会丢失,你会发现自己有一个空白的地方。想象一下,你可以用自己制作的一块填补那个空白的地方 - 而不是拼图盒子里原来的那块。这大致类似于 Vue 插槽的作用。
Vue 插槽就像是组件中的开放空间,其他组件可以用文本、HTML 元素或其他 Vue 组件填充。你可以在组件中声明插槽的位置和行为方式。
通过这种技术,你可以创建一个组件,并在需要时轻松自定义它。
准备工作
这个食谱的先决条件是 Node.js 12+。
此食谱所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要完成这个食谱,我们将使用我们的 Vue 项目和 Vue CLI,就像在创建可视化模板组件食谱中所做的那样。
操作步骤
按照以下说明在组件中创建插槽和命名插槽:
-
在
components
文件夹中打开MaterialCardBox.vue
文件。 -
在组件的
<template>
部分,我们需要为卡片添加四个主要部分。这些部分基于 Material Design 卡片的结构,分别是header
、media
、main section
和action
区域。我们将使用默认插槽来放置main section
;其余部分都将是命名插槽。对于一些命名插槽,我们将添加一个回退配置,如果用户没有为插槽选择任何设置,则将显示该配置:
<template>
<div class="cardBox elevation_2">
<div class="header">
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
Card Header
</h1>
<h2 class="cardSubHeader cardText">
Card Sub Header
</h2>
</div>
</div>
<div class="media">
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
src="https://via.placeholder.com/350x250"
>
</div>
<div
v-if="$slots.default"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot/>
</div>
<div
v-if="$slots.action"
class="action"
>
<slot name="action"/>
</div>
</div> </template>
- 现在,我们需要为组件创建文本 CSS 规则。在
style
文件夹中,创建一个名为cardStyles.css
的新文件。在这里,我们将添加卡片文本和标题的规则:
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
.cardText {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-decoration: inherit;
text-transform: inherit;
font-size: 0.875rem;
line-height: 1.375rem;
letter-spacing: 0.0071428571em;
}
h1.cardHeader {
font-size: 1.25rem;
line-height: 2rem;
font-weight: 500;
letter-spacing: .0125em;
}
h2.cardSubHeader {
font-size: .875rem;
line-height: 1.25rem;
font-weight: 400;
letter-spacing: .0178571429em;
opacity: .6;
}
- 在组件的
<style>
部分,我们需要创建一些遵循设计指南规则的 CSS:
<style scoped>
@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap");
@import "../style/elevation.css";
@import "../style/cardStyles.css"; * {
font-family: "Roboto", sans-serif;
} .cardBox {
width: 100%;
max-width: 300px;
border-radius: 0.25rem;
background-color: #fff;
position: relative;
display: inline-block;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0,
0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
} .cardBox > .header {
padding: 1rem;
position: relative;
display: block;
} .cardBox > .media {
overflow: hidden;
position: relative;
display: block;
max-width: 100%;
} .cardBox > .section {
padding: 1rem;
position: relative;
margin-bottom: 1.5rem;
display: block;
} .cardBox > .action {
padding: 0.5rem;
position: relative;
display: block;
} .cardBox > .action > *:not(:first-child) {
margin-left: 0.4rem;
} .noBottomPadding {
padding-bottom: 0 !important;
} .halfPaddingTop {
padding-top: 0.5rem !important;
} </style>
- 在
src
文件夹中的App.vue
文件中,我们需要向这些插槽添加元素。这些元素将被添加到每个命名插槽以及默认插槽。我们将更改文件的<template>
部分内的组件。要添加命名插槽,我们需要使用一个名为v-slot:
的指令,然后添加我们想要使用的插槽的名称:
<template>
<div id="app">
<MaterialCardBox>
<template v-slot:header>
<strong>Card Title</strong><br>
<span>Card Sub-Title</span>
</template>
<template v-slot:media>
<img src="https://via.placeholder.com/350x150">
</template>
<p>Main Section</p>
<template v-slot:action>
<button>Action Button</button>
<button>Action Button</button>
</template>
</MaterialCardBox>
</div> </template>
对于默认插槽,我们不需要使用指令;它只需要包裹在组件内部,以便可以放置在组件的<slot />
部分内。
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
插槽是可以放置任何可以呈现到 DOM 中的东西的地方。我们选择插槽的位置,并告诉组件在接收到任何信息时在哪里呈现。
在这个示例中,我们使用了命名插槽,这些插槽旨在与需要多个插槽的组件一起使用。要在 Vue 单文件(.vue
)的<template>
部分中放置组件内的任何信息,您需要添加v-slot:
指令,以便 Vue 知道在哪里放置传递下来的信息。
另请参阅
-
您可以在
v3.vuejs.org/guide/component-slots.html
找到有关 Vue 插槽的更多信息。 -
您可以在
material.io/components/cards/#anatomy
找到有关 Material Design 卡片解剖的更多信息。
向组件传递数据并验证数据
到目前为止,您知道如何通过插槽将数据放入组件中,但这些插槽是为 HTML DOM 元素或 Vue 组件而设计的。有时,您需要传递诸如字符串、数组、布尔值甚至对象之类的数据。
整个应用程序就像一个拼图,其中每个部分都是一个组件。组件之间的通信是其中的重要部分。向组件传递数据是连接拼图的第一步,而验证数据是连接部件的最后一步。
在这个示例中,我们将学习如何向组件传递数据并验证传递给它的数据。
准备工作
这个食谱的先决条件是 Node.js 12+。
这个食谱所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要完成这个食谱,我们将继续使用来自 使用插槽和命名插槽在组件内放置数据 食谱的项目。
如何做...
按照以下说明传递数据给组件并验证它:
-
打开
src/components
文件夹中的MaterialCardBox.vue
文件。 -
在组件的
<script>
部分,我们将创建一个名为props
的新属性。这个属性接收组件的数据,可以用于视觉操作、代码内的变量,或者需要执行的函数。在这个属性中,我们需要声明属性的名称、类型、是否必需,以及验证函数。这个函数将在运行时执行,以验证传递的属性是否有效:
<script> export default {
name: 'MaterialCardBox',
inheritAttrs: false,
props: {
header: {
type: String,
required: false,
default: '',
validator: (v) => typeof v === 'string',
},
subHeader: {
type: String,
required: false,
default: '',
validator: (v) => typeof v === 'string',
},
mainText: {
type: String,
required: false,
default: '',
validator: (v) => typeof v === 'string',
},
showMedia: {
type: Boolean,
required: false,
default: false,
validator: (v) => typeof v === 'boolean',
},
imgSrc: {
type: String,
required: false,
default: '',
validator: (v) => typeof v === 'string',
},
showActions: {
type: Boolean,
required: false,
default: false,
validator: (v) => typeof v === 'boolean',
},
elevation: {
type: Number,
required: false,
default: 2,
validator: (v) => typeof v === 'number',
},
},
computed: {}, }; </script>
- 在组件的
<script>
部分的computed
属性中,我们需要创建一组用于渲染卡片的视觉操作规则。这些规则被称为showMediaContent
、showActionsButtons
、showHeader
和cardElevation
。每个规则将检查接收到的props
和$slots
对象,以检查是否需要渲染相关的卡片部分:
computed: {
showMediaContent() {
return (this.$slots.media || this.imgSrc) && this.showMedia;
},
showActionsButtons() {
return this.showActions && this.$slots.action;
},
showHeader() {
return this.$slots.header || (this.header || this.subHeader);
},
showMainContent() {
return this.$slots.default || this.mainText;
},
cardElevation() {
return `elevation_${parseInt(this.elevation, 10)}`;
}, },
- 在添加了视觉操作规则之后,我们需要将创建的规则添加到组件的
<template>
部分。它们将影响我们卡片的外观和行为。例如,如果没有定义头部插槽,但定义了头部属性,我们将显示备用头部。这个头部包含通过props
传递下来的数据:
<template>
<div
class="cardBox"
:class="cardElevation"
>
<div
v-if="showHeader"
class="header"
>
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
{{ header }}
</h1>
<h2 class="cardSubHeader cardText">
{{ subHeader }}
</h2>
</div>
</div>
<div
v-if="showMediaContent"
class="media"
>
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
:src="imgSrc"
>
</div>
<div
v-if="showMainContent"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot v-if="$slots.default" />
<p
v-else
class="cardText"
>
{{ mainText }}
</p>
</div>
<div
v-if="showActionsButtons"
class="action"
>
<slot
v-if="$slots.action"
name="action"
/>
</div>
</div> </template>
- 要运行服务器并查看你的组件,你需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
记得总是执行命令 npm run lint --fix
,自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
每个 Vue 组件都是一个 JavaScript 对象,有一个渲染函数。当需要在 HTML DOM 中渲染它时,会调用这个渲染函数。单文件组件是这个对象的一个抽象。
当我们声明我们的组件具有可以传递的唯一 props 时,它为其他组件或 JavaScript 打开了一个小门,以在我们的组件内放置信息。然后,我们可以在组件内使用这些值来渲染数据,进行一些计算,或者制定视觉规则。
在我们的情况下,使用单文件组件,我们将这些规则作为 HTML 属性传递,因为vue-template-compiler
将获取这些属性并将其转换为 JavaScript 对象。
当这些值传递给我们的组件时,Vue 会检查传递的属性是否与正确的类型匹配,然后我们对每个值执行验证函数,以查看它是否与我们期望的匹配。
完成所有这些后,组件的生命周期继续,我们可以渲染我们的组件。
另请参阅
-
您可以在
v3.vuejs.org/guide/component-props.html
找到有关props
的更多信息。 -
您可以在
vue-loader.vuejs.org/guide/
找到有关vue-template-compiler
的更多信息。
创建功能组件
功能组件的美丽之处在于它们的简单性。它们是无状态组件,没有任何数据、计算属性,甚至生命周期。它们只是在传递的数据发生变化时调用的渲染函数。
您可能想知道这有什么用。嗯,功能组件是 UI 组件的完美伴侣,它们不需要在内部保留任何数据,或者只是渲染组件而不需要任何数据操作的可视组件。
顾名思义,它们类似于函数组件,除了渲染函数外没有其他内容。它们是组件的精简版本,专门用于性能渲染和可视元素。
准备工作
此配方的先决条件是 Node.js 12+。
此配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要完成此配方,我们将使用我们的 Vue 项目和 Vue CLI,就像在将数据传递给您的组件并验证数据配方中所做的那样。
如何做...
按照以下说明创建一个 Vue 功能组件:
-
在
src/components
文件夹中创建一个名为MaterialButton.vue
的新文件。 -
在这个组件中,我们需要验证我们将接收的 prop 是否是有效的颜色。为此,在项目中安装
is-color
模块。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save is-color
- 在我们组件的
<script>
部分,我们需要创建props
对象,函数组件将接收该对象。由于函数组件只是一个没有状态的渲染函数,因此它是无状态的 - 组件的<script>
部分被简化为props
,injections
和slots
。将有四个props
对象:backgroundColor
,textColor
,isRound
和isFlat
。在安装组件时,这些将不是必需的,因为我们在props
中定义了默认值:
<script> import isColor from 'is-color'; export default {
name: 'MaterialButton',
props: {
backgroundColor: {
type: String,
required: false,
default: '#fff',
validator: (v) => typeof v === 'string' && isColor(v),
},
textColor: {
type: String,
required: false,
default: '#000',
validator: (v) => typeof v === 'string' && isColor(v),
},
isRound: {
type: Boolean,
required: false,
default: false,
},
isFlat: {
type: Boolean,
required: false,
default: false,
},
}, }; </script>
- 我们需要创建一个带有基本
class
属性按钮的 HTML 元素,并且一个基于接收到的props
对象的动态class
属性。与普通组件相比,我们需要指定props
属性以使用函数组件。对于按钮的样式,我们需要创建一个基于$props
的动态style
属性。为了直接将所有事件监听器传递给父级,我们可以调用v-bind
指令并传递$attrs
属性。这将绑定所有事件监听器,而无需我们声明每一个。在按钮内部,我们将添加一个用于视觉增强的div
HTML 元素,并添加<slot>
,文本将放置在其中:
<template>
<button
tabindex="0"
class="button"
:class="{
round: $props.isRound,
isFlat: $props.isFlat,
}"
:style="{
background: $props.backgroundColor,
color: $props.textColor
}"
v-bind="$attrs"
>
<div
tabindex="-1"
class="button_focus_helper"
/>
<slot/>
</button> </template>
- 现在,让我们把它弄得漂亮一点。在组件的
<style>
部分,我们需要为这个按钮创建所有的 CSS 规则。我们需要在<style>
中添加scoped
属性,以便 CSS 规则不会影响我们应用程序中的任何其他元素:
<style scoped>
.button {
user-select: none;
position: relative;
outline: 0;
border: 0;
border-radius: 0.25rem;
vertical-align: middle;
cursor: pointer;
padding: 4px 16px;
font-size: 14px;
line-height: 1.718em;
text-decoration: none;
color: inherit;
background: transparent;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
min-height: 2.572em;
font-weight: 500;
text-transform: uppercase;
}
.button:not(.isFlat){
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
} .button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
} .button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
} .button_focus_helper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
outline: 0;
opacity: 0;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.4s cubic-bezier(0.25, 0.8, 0.5, 1);
} .button_focus_helper:after, .button_focus_helper:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
border-radius: inherit;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);
} .button_focus_helper:before {
background: #000;
} .button_focus_helper:after {
background: #fff;
} .button:focus .button_focus_helper:before,
.button:hover .button_focus_helper:before {
opacity: .1;
} .button:focus .button_focus_helper:after,
.button:hover .button_focus_helper:after {
opacity: .6;
} .button:focus .button_focus_helper,
.button:hover .button_focus_helper {
opacity: 0.2;
} .round {
border-radius: 50%;
} </style>
- 在
App.vue
文件中,我们需要导入我们的组件才能看到它:
<template>
<div id="app">
<MaterialCardBox
header="Material Card Header"
sub-header="Card Sub Header"
show-media
show-actions
img-src="https://picsum.photos/300/200"
:main-text="`
The path of the righteous man is beset on all sides by the
iniquities of the selfish and the tyranny of evil men.`"
>
<template v-slot:action>
<MaterialButton
background-color="#027be3"
text-color="#fff"
>
Action 1
</MaterialButton>
<MaterialButton
background-color="#26a69a"
text-color="#fff"
is-flat
>
Action 2
</MaterialButton>
</template>
</MaterialCardBox>
</div>
</template>
<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
export default {
name: 'App',
components: {
MaterialButton,
MaterialCardBox,
},
};
</script>
<style>
body {
font-size: 14px;
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
npm run serve
记得总是执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
它是如何工作的...
函数组件就像渲染函数一样简单。它们没有任何类型的数据、函数或对外部世界的访问。
它们最初是作为 JavaScript 对象render()
函数在 Vue 中引入的;后来,它们被添加到vue-template-compiler
中,用于 Vue 单文件应用程序。
功能性组件通过接收两个参数createElement
和context
来工作。正如我们在单文件中看到的,我们只能访问元素,因为它们不在 JavaScript 对象的this
属性中。这是因为当上下文传递给渲染函数时,没有this
属性。
功能性组件在 Vue 上提供了最快的渲染速度,因为它不依赖于组件的生命周期来检查渲染;它只在数据改变时渲染。
另请参阅
- 您可以在
www.npmjs.com/package/is-color
找到有关is-color
模块的更多信息。
访问您的子组件的数据
通常,父子通信是通过事件或 props 来完成的。但有时,您需要访问存在于子函数或父函数中的数据、函数或计算属性。
Vue 为我们提供了双向交互的方式,从而打开了使用 props 和事件监听器等通信和事件的大门。
还有另一种访问组件之间数据的方法:直接访问。这可以通过在单文件组件中使用模板中的特殊属性来完成,或者通过直接调用 JavaScript 中的对象来完成。有些人认为这种方法有点懒惰,但有时确实没有其他方法可以做到这一点。
准备工作
这个食谱的先决条件是 Node.js 12+。
这个食谱所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
为了完成这个食谱,我们将使用我们的 Vue 项目和 Vue CLI,就像在创建功能性组件的食谱中一样。
如何做...
我们将把这个食谱分成四个部分。前三部分将涵盖新组件的创建-StarRatingInput
、StarRatingDisplay
和StarRating
,而最后一部分将涵盖数据和函数访问的直接父子操作。
创建星级评分输入
在这个食谱中,我们将创建一个基于五星评级系统的星级评分输入。
按照以下步骤创建自定义星级评分输入:
-
在
src/components
文件夹中创建一个名为StarRatingInput.vue
的新文件。 -
在组件的
<script>
部分中,在props
属性中创建一个maxRating
属性,它是一个数字,非必需,并具有默认值5
。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
。在methods
属性中,我们需要创建三种方法:updateRating
、emitFinalVoting
和getStarName
。updateRating
方法将评分保存到数据中,emitFinalVoting
将调用updateRating
并通过final-vote
事件将评分传递给父组件,getStarName
将接收一个值并返回星星的图标名称:
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
}; </script>
- 在组件的
<template>
部分中,我们需要创建一个<slot>
组件,以便我们可以在星级评分之前放置文本。我们将根据通过props
属性接收到的maxRating
值创建一个动态星星列表。创建的每个星星都将在mouseenter
、focus
和click
事件中附加一个监听器。当触发mouseenter
和focus
时,将调用updateRating
方法,而click
将调用emitFinalVote
:
<template>
<div class="starRating">
<span class="rateThis">
<slot/>
</span>
<ul>
<li
v-for="rate in maxRating"
:key="rate"
@mouseenter="updateRating(rate)"
@click="emitFinalVote(rate)"
@focus="updateRating(rate)"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
</div> </template>
- 我们需要将 Material Design 图标导入到我们的应用程序中。在
styles
文件夹中创建一个名为materialIcons.css
的新样式文件,并添加font-family
的 CSS 规则:
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ- Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons' !important;
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
- 打开
main.js
文件并将创建的样式表导入其中。css-loader
webpack 将处理 JavaScript 文件中导入的.css
文件。这将有助于开发,因为您无需在其他地方重新导入文件:
import { createApp } from 'vue'; import App from './App.vue'; import './style/materialIcons.css'; createApp(App).mount('#app');
- 为了给我们的组件设置样式,我们将在
src/style
文件夹中创建一个名为starRating.css
的通用样式文件。在那里,我们将添加在StarRatingDisplay
和StarRatingInput
组件之间共享的通用样式:
.starRating {
user-select: none;
display: flex;
flex-direction: row; } .starRating * {
line-height: 0.9rem; } .starRating .material-icons {
font-size: .9rem !important;
color: orange; } ul {
display: inline-block;
padding: 0;
margin: 0; } ul > li {
list-style: none;
float: left; }
- 在组件的
<style>
部分中,我们需要创建所有的 CSS 规则。然后,在位于src/components
文件夹中的StarRatingInput.vue
组件文件中,我们需要向<style>
添加scoped
属性,以便不影响应用程序中的任何其他元素的 CSS 规则。在这里,我们将导入我们创建的通用样式并添加新的输入样式:
<style scoped>
@import '../style/starRating.css'; .starRating {
justify-content: space-between;
} .starRating * {
line-height: 1.7rem;
} .starRating .material-icons {
font-size: 1.6rem !important;
} .rateThis {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: 1rem;
} </style>
- 运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
记住始终执行命令 npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
创建 StarRatingDisplay 组件
现在我们有了输入,我们需要一种方法来向用户显示所选的选择。按照以下步骤创建StarRatingDisplay
组件:
-
在
src/components
文件夹中创建一个名为StarRatingDisplay.vue
的新组件。 -
在组件的
<script>
部分,在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。它们三个都将是数字,非必需的,并具有默认值。在methods
属性中,我们需要创建一个名为getStarName
的新方法,该方法将接收一个值并返回星星的图标名称:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
}; </script>
- 在
<template>
中,我们需要根据通过props
属性接收到的maxRating
值创建一个动态星星列表。在列表之后,我们需要显示我们收到的投票,如果我们收到更多的投票,我们也会显示它们:
<template>
<div class="starRating">
<ul>
<li
v-for="rate in maxRating"
:key="rate"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
<span class="rating">
{{ rating }}
</span>
<span
v-if="votes"
class="votes"
>
({{ votes }})
</span>
</div> </template>
- 在组件的
<style>
部分,我们需要创建所有的 CSS 规则。我们需要向<style>
添加scoped
属性,以便没有任何 CSS 规则影响我们应用程序中的任何其他元素。在这里,我们将导入我们创建的通用样式,并添加新的样式以供显示:
<style scoped>
@import '../style/starRating.css'; .rating, .votes {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: .75rem;
margin-left: .4rem;
} </style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是渲染和运行的组件:
创建 StarRating 组件
现在我们已经创建了输入和显示,我们需要将它们合并到一个单独的组件中。这个组件将是我们在应用程序中使用的最终组件。
按照以下步骤创建最终的StarRating
组件:
-
在
src/components
文件夹中创建一个名为StarRating.vue
的新文件。 -
在组件的
<script>
部分,我们需要导入StarRatingDisplay
和StarRatingInput
组件。在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。所有这三个属性都将是数字,非必需的,并具有默认值。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
,以及一个名为voted
的属性,其默认值为false
。在methods
属性中,我们需要添加一个名为vote
的新方法,它将接收rank
作为参数。它将把rating
定义为接收到的值,并将voted
组件的内部变量定义为true
:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue'; export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
}; </script>
- 对于
<template>
部分,我们将在这里放置两个组件,从而显示评分的输入:
<template>
<div>
<StarRatingInput
v-if="!voted"
:max-rating="maxRating"
@final-vote="vote"
>
Rate this Place
</StarRatingInput>
<StarRatingDisplay
v-else
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
/>
</div> </template>
子组件中的数据操作
现在我们所有的组件都准备好了,我们需要将它们添加到我们的应用程序中。基础应用程序将访问子组件,并将评分设置为 5 星。
按照以下步骤来理解和操作子组件中的数据:
-
在
App.vue
文件中,在组件的<template>
部分,删除MaterialCardBox
组件的main-text
属性,并将其设置为组件的默认插槽。 -
在放置的文本之前,我们将添加
StarRating
组件。我们将为其添加一个ref
属性。这个属性将告诉 Vue 将这个组件直接链接到组件的this
对象中的一个特殊属性。在操作按钮中,我们将为点击事件添加监听器 - 一个用于resetVote
,另一个用于forceVote
:
<template>
<div id="app">
<MaterialCardBox
header="Material Card Header"
sub-header="Card Sub Header"
show-media
show-actions
img-src="https://picsum.photos/300/200"
>
<p>
<StarRating
ref="starRating"
/>
</p>
<p>
The path of the righteous man is beset on all sides by the
iniquities of the selfish and the tyranny of evil men.
</p>
<template v-slot:action>
<MaterialButton
background-color="#027be3"
text-color="#fff"
@click="resetVote"
>
Reset
</MaterialButton>
<MaterialButton
background-color="#26a69a"
text-color="#fff"
is-flat
@click="forceVote"
>
Rate 5 Stars
</MaterialButton>
</template>
</MaterialCardBox>
</div> </template>
- 在组件的
<script>
部分,我们将创建一个methods
属性,并添加两个新方法:resetVote
和forceVote
。这些方法将访问StarRating
组件并重置数据或将数据设置为 5 星评分:
<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
import StarRating from './components/StarRating.vue'; export default {
name: 'App',
components: {
StarRating,
MaterialButton,
MaterialCardBox,
},
methods: {
resetVote() {
this.$refs.starRating.vote(0);
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.vote(5);
},
},
工作原理...
当ref
属性添加到组件时,Vue 会将对被引用元素的链接添加到 JavaScript 的this
属性对象内的$refs
属性中。从那里,您可以完全访问组件。
这种方法通常用于操作 HTML DOM 元素,而无需调用文档查询选择器函数。
然而,该属性的主要功能是直接访问 Vue 组件,使您能够执行函数并查看组件的计算属性、变量和已更改的变量 - 这就像从外部完全访问组件一样。
还有更多...
与父组件可以访问子组件的方式相同,子组件可以通过在this
对象上调用$parent
来访问父组件。事件可以通过调用$root
属性来访问 Vue 应用程序的根元素。
另请参阅
您可以在v3.vuejs.org/guide/migration/custom-directives.html#edge-case-accessing-the-component-instance
找到有关父子通信的更多信息。
创建一个动态注入的组件
有些情况下,您的组件可以根据您收到的变量的类型或您拥有的数据类型来定义;然后,您需要在不需要设置大量 Vue v-if
、v-else-if
和v-else
指令的情况下即时更改组件。
在这些情况下,最好的做法是使用动态组件,当计算属性或函数可以定义要呈现的组件时,并且决定是实时进行的。
如果有两种响应,这些决定有时可能很容易做出,但如果有一个长的开关情况,那么它们可能会更复杂,其中您可能有一个需要使用的长列表可能组件。
准备就绪
此配方的先决条件是 Node.js 12+。
此配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
为了完成这个配方,我们将使用我们的 Vue 项目和 Vue CLI,就像我们在访问你的子组件数据配方中所做的那样。
如何做...
按照以下步骤创建一个动态注入的组件:
-
打开
StarRating.vue
组件。 -
在组件的
<script>
部分,我们需要创建一个带有名为starComponent
的新计算值的computed
属性。此值将检查用户是否已投票。如果他们没有,它将返回StarRatingInput
组件;否则,它将返回StarRatingDisplay
组件:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue'; export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
}; </script>
- 在组件的
<template>
部分,我们将删除现有组件,并用一个名为<component>
的特殊组件替换它们。这个特殊组件有一个命名属性,您可以指向任何返回有效的 Vue 组件的地方。在我们的例子中,我们将指向计算属性starComponent
。我们将把由这两个其他组件定义的所有绑定属性放在这个新组件中,包括放在<slot>
中的文本:
<template>
<component
:is="starComponent"
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
@final-vote="vote"
>
Rate this Place
</component>
</template>
它是如何工作的...
使用 Vue 特殊的<component>
组件,我们声明了根据计算属性设置的规则应该渲染什么组件。
作为一个通用组件,您总是需要保证每个可以渲染的组件都会有一切。这样做的最佳方式是使用v-bind
指令与需要定义的 props 和规则,但也可以直接在组件上定义,因为它将作为一个 prop 传递下来。
另请参阅
您可以在v3.vuejs.org/guide/component-dynamic-async.html#dynamic-async-components
找到有关动态组件的更多信息。
创建一个依赖注入组件
直接从子组件或父组件访问数据而不知道它们是否存在可能非常危险。
在 Vue 中,可以使您的组件行为像一个接口,并且具有一个在开发过程中不会改变的常见和抽象函数。依赖注入的过程是开发世界中的一个常见范例,并且在 Vue 中也已经实现。
使用 Vue 的内部依赖注入有一些优缺点,但这总是一种确保您的子组件在开发时知道从父组件可以期望什么的好方法。
准备工作
这个配方的先决条件是 Node.js 12+。
这个配方所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要完成这个配方,我们将使用我们的 Vue 项目和 Vue CLI,就像在创建一个动态注入组件配方中所做的那样。
如何做到...
按照以下步骤创建一个依赖注入组件:
-
打开
StarRating.vue
组件。 -
在组件的
<script>
部分,添加一个名为provide
的新属性。在我们的情况下,我们将只是添加一个键值来检查组件是否是特定组件的子组件。在属性中创建一个包含starRating
键和true
值的对象:
<script> import StarRatingInput from './StarRatingInput.vue'; import StarRatingDisplay from './StarRatingDisplay.vue'; export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
provide: {
starRating: true,
},
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
}, }; </script>
-
打开
StarRatingDisplay.vue
文件。 -
在组件的
<script>
部分,我们将添加一个名为inject
的新属性。这个属性将接收一个名为starRating
的键的对象,值将是一个包含default()
函数的对象。
如果这个组件不是StarRating
组件的子组件,这个函数将记录一个错误:
<script> export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
}, }; </script>
-
打开
StarRatingInput.vue
文件。 -
在组件的
<script>
部分,我们将添加一个名为inject
的新属性。这个属性将接收一个名为starRating
的键的对象,值将是一个包含default()
函数的对象。如果这个组件不是StarRating
组件的子组件,这个函数将记录一个错误:
<script> export default {
name: 'StartRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingInput need to be a child of
StartRating');
},
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
}, }; </script>
它是如何工作的...
在运行时,Vue 将检查StarRatingDisplay
和StarRatingInput
组件中的starRating
的注入属性,如果父组件没有提供这个值,它将在控制台中记录一个错误。
使用组件注入通常用于提供和维护绑定组件之间的公共接口,比如菜单和项目。项目可能需要一些存储在菜单中的函数或数据,或者我们可能需要检查它是否是菜单的子组件。
依赖注入的主要缺点是共享元素上不再具有响应性。因此,它主要用于共享函数或检查组件链接。
另请参阅
您可以在v3.vuejs.org/guide/component-provide-inject.html#provide-inject
找到有关组件依赖注入的更多信息。
创建一个组件 mixin
有时你会发现自己一遍又一遍地重写相同的代码。然而,有一种方法可以防止这种情况,并让自己更加高效。
为此,您可以使用所谓的mixin
,这是 Vue 中的一个特殊代码导入,它将外部代码部分连接到当前组件。
准备工作
这个食谱的先决条件是 Node.js 12+。
这个食谱所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
为了完成这个示例,我们将使用我们的 Vue 项目和 Vue CLI,就像我们在创建一个依赖注入组件示例中所做的那样。
如何做...
按照以下步骤创建一个组件混合:
-
打开
StarRating.vue
组件。 -
在
<script>
部分,我们需要将props
属性提取到一个名为starRatingDisplay.js
的新文件中,我们需要在mixins
文件夹中创建这个新文件。这个新文件将是我们的第一个mixin
,看起来会像这样:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
}, };
- 回到
StarRating.vue
组件,我们需要导入这个新创建的文件,并将其添加到一个名为mixin
的新属性中。
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
import StarRatingDisplayMixin from '../mixins/starRatingDisplay'; export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
mixins: [StarRatingDisplayMixin],
provide: {
starRating: true,
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
}; </script>
-
现在,我们将打开
StarRatingDisplay.vue
文件。 -
在
<script>
部分,我们将inject
属性提取到一个名为starRatingChild.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们的inject
属性的mixin
:
export default {
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
};
- 回到
StarRatingDisplay.vue
文件,在<script>
部分,我们将methods
属性提取到一个名为starRatingName.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们的getStarName
方法的mixin
:
export default {
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
- 回到
StarRatingDisplay.vue
文件,我们需要导入这些新创建的文件,并将它们添加到一个名为mixin
的新属性中:
<script> import StarRatingDisplayMixin from '../mixins/starRatingDisplay'; import StarRatingNameMixin from '../mixins/starRatingName'; import StarRatingChildMixin from '../mixins/starRatingChild'; export default {
name: 'StarRatingDisplay',
mixins: [
StarRatingDisplayMixin,
StarRatingNameMixin,
StarRatingChildMixin,
], }; </script>
-
打开
StarRatingInput.vue
文件。 -
在
<script>
部分,删除inject
属性并将props
属性提取到一个名为starRatingBase.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们的props
属性的mixin
:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
},
};
- 回到
StarRatingInput.vue
文件,我们需要将rating
数据属性重命名为rank
,并且在getStarName
方法中,我们需要添加一个新的常量,它将接收rating
属性或rank
数据。最后,我们需要导入starRatingChildMixin
和starRatingBaseMixin
:
<script>
import StarRatingBaseMixin from '../mixins/starRatingBase';
import StarRatingChildMixin from '../mixins/starRatingChild'; export default {
name: 'StarRatingInput',
mixins: [
StarRatingBaseMixin,
StarRatingChildMixin,
],
data: () => ({
rank: 0,
}),
methods: {
updateRating(value) {
this.rank = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rank);
},
getStarName(rate) {
const rating = (this.rating || this.rank);
if (rate <= rating) {
return 'star';
}
if (Math.fround((rate - rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
}; </script>
工作原理...
混合将对象合并在一起,但请确保不要用导入的对象替换组件中已经存在的属性。
mixins
属性的顺序也很重要,因为它们将被作为for
循环进行检查和导入,因此最后一个mixin
不会改变任何祖先的属性。
在这里,我们将代码中的许多重复部分拆分成了四个不同的小的 JavaScript 文件,这样更容易维护并提高了生产力,而无需重写代码。
另请参阅
您可以在v3.vuejs.org/guide/mixins.html#mixins
找到有关混合的更多信息。
延迟加载您的组件
webpack
和 Vue 天生就是一对。当将webpack
作为 Vue 项目的打包工具时,可以使组件在需要时异步加载。这通常被称为延迟加载。
准备工作
此教程的先决条件是 Node.js 12+。
此教程所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
要完成此教程,我们将使用我们的 Vue 项目和 Vue CLI,就像在创建组件混合教程中所做的那样。
如何做...
按照以下步骤使用延迟加载技术导入您的组件:
-
打开
App.vue
文件。 -
在组件的
<script>
部分,从 Vue 中导入defineAsyncComponent
API,并将lazyLoad
组件函数作为defineAsyncComponent
函数的参数传递:
<script>
import { defineAsyncComponent } from 'vue';
import StarRating from './components/StarRating.vue';
export default {
name: 'App',
components: {
StarRating,
MaterialButton: defineAsyncComponent(() => import('./components/MaterialButton.vue')),
MaterialCardBox: defineAsyncComponent(() => import('./components/MaterialCardBox.vue')),
},
methods: {
resetVote() {
this.$refs.starRating.vote(0);
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.vote(5);
},
},
};
</script>
<style>
body {
font-size: 14px;
}
</style>
它是如何工作的...
Vue 现在使用一个名为defineAsyncComponent
的新 API 来将组件标识为异步组件,并将另一个返回import()
方法的函数作为参数传递。
当我们为每个组件声明一个返回import()
函数的函数时,webpack
知道这个导入函数将进行代码拆分,并将使组件成为捆绑包中的一个新文件。
另请参阅
-
您可以在
v3.vuejs.org/guide/component-dynamic-async.html#dynamic-async-components
找到有关异步组件的更多信息。 -
您可以在
github.com/tc39/proposal-dynamic-import
找到有关 TC39 动态导入的更多信息。
第三章:设置我们的聊天应用程序 - AWS Amplify 环境和 GraphQL
自从 Facebook 在 2012 年推出 GraphQL 以来,它就像飓风一样席卷了网络。大公司开始采用它,而中小型公司也看到了这种基于查询的 API 的潜力。
一开始看起来很奇怪,但随着您开始阅读和体验更多,您就不想再使用 REST API 了。简单性和数据获取能力使前端开发人员的生活变得更轻松,因为他们可以只获取他们想要的内容,而不必受限于只提供单个信息片段的端点。
这是一个漫长的配方的开始,所有的配方都将形成一个完整的聊天应用程序,但您可以在不需要编写整个章节的情况下,在配方中学习有关 GraphQL 和 AWS Amplify 的知识。
在本章中,我们将学习更多关于 AWS Amplify 环境和 GraphQL 的知识,以及如何将其添加到我们的应用程序并使其可用作通信驱动程序。
在本章中,我们将涵盖以下配方:
-
创建您的 AWS Amplify 环境
-
创建您的第一个 GraphQL API
-
将 GraphQL 客户端添加到您的应用程序
-
为您的应用程序创建 AWS Amplify 驱动程序
技术要求
在本章中,我们将使用 Node.js、AWS Amplify 和 Quasar Framework。
注意,Windows 用户!您需要安装一个名为windows-build-tools
的 NPM 包,以便能够安装所需的软件包。要执行此操作,请以管理员身份打开 PowerShell 并执行以下命令:
> 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
创建您的 AWS Amplify 环境
借助 AWS Amplify 的帮助,我们可以在几分钟内创建一个后端环境,其中包括 NoSQL 数据库、GraphQL 解析器和一个在线存储桶,供我们在开发后部署我们的应用程序。
为了创建 Vue 应用程序,我们将使用 Quasar Framework。这是一个基于 Vue 的框架,提供了开发应用程序所需的所有工具、结构和组件。
在这个配方中,我们将学习如何创建我们的 AWS 账户,在本地配置 AWS Amplify 环境,并使用 Quasar Framework 创建我们的初始项目。
准备就绪
这个教程的先决条件是 Node.js 12+。
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
如何做...
我们将把这个教程的任务分成四个部分:创建 AWS 账户,配置 AWS Amplify,创建您的 Quasar 项目,以及初始化 AWS Amplify 项目。
创建 AWS 账户
在这里,我们将学习如何在 AWS 门户上创建一个账户,以便我们可以访问 AWS 控制台:
-
在网站上,点击“创建 AWS 账户”按钮。
-
选择创建一个“专业”账户或一个“个人”账户(因为我们将要探索平台并为自己开发示例应用程序,最好选择“个人”账户)。
-
现在亚马逊将要求您提供付款信息,以防您的使用超出了免费套餐限制。
-
现在是确认您的身份的时候 - 您需要提供一个有效的电话号码,亚马逊将用它来发送您需要输入的 PIN 码。
-
在收到 PIN 码后,您将看到一个成功的屏幕和一个“继续”按钮。
-
现在您需要为您的账户选择一个计划;您可以选择此教程的“基本计划”选项。
-
现在您已经完成,可以登录到您的 Amazon AWS 账户控制台。
配置 AWS Amplify
让我们配置本地 AWS Amplify 环境,以准备开始开发我们的聊天应用程序:
- 要设置 AWS Amplify,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> amplify configure
-
浏览器将打开,您需要登录到您的 AWS 控制台账户。
-
登录后,返回终端并按Enter。CLI 将要求您选择您希望应用程序执行的服务器区域。建议在
us-east-1
上运行。 -
选择区域后,CLI 将要求您为身份和访问管理(IAM)定义用户名。您可以按Enter使用默认值,也可以输入您想要的值(但必须是唯一的)。
-
现在浏览器将打开以定义您指定的用户的用户详细信息。点击“下一步:权限”按钮转到下一个屏幕。
-
点击“下一步:标签”按钮转到 AWS 标签屏幕。在这个屏幕上,点击“下一步:审核”按钮来审查您定义的设置。
-
现在你可以点击“创建用户”按钮来创建用户并转到访问密钥屏幕。
-
最后,在此屏幕上,等待访问密钥 ID 和秘密访问密钥可用。在浏览器中复制访问密钥 ID,粘贴到终端中,然后按“Enter”键。
-
粘贴访问密钥 ID 后,您必须返回浏览器,点击秘密访问密钥上的“显示”链接,复制该值,粘贴到终端中,然后按“Enter”键。
-
最后,您需要定义 AWS 配置文件名称(您可以通过按“Enter”键使用默认值)。
您现在已在计算机上设置了 AWS Amplify 环境。
创建您的 Quasar 项目
现在我们将创建 Quasar Framework 项目,这将是我们的聊天应用程序:
- 要创建您的 Quasar Framework 应用程序,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar create chat-app
- Quasar CLI 将要求输入项目名称;它需要是有效的 npm 软件包名称:
> ? Project name (internal usage for dev) chat-app
- CLI 将要求输入产品名称(通常用于渐进式 Web 应用程序(PWA),混合移动应用程序和 Electron 应用程序):
? Project product name (must start with letter if building mobile
apps) Chat App
- 之后,CLI 将要求输入项目描述,这将用于混合应用程序和 PWA:
? Project description A Chat Application
- 现在 CLI 将要求输入项目的作者。通常,这是您的 npm 或 Git 配置的作者:
? Author Heitor Ramon Ribeiro <[email protected]>
- 现在您可以选择 CSS 预处理器。我们将选择
Stylus
(您可以选择最适合您的预处理器):
? Pick your favorite CSS preprocessor: (can be changed later)
Sass with indented syntax (recommended)
Sass with SCSS syntax (recommended)
❯ Stylus
None (the others will still be available)
- Quasar 有两种将组件、指令和插件导入构建系统的方法。您可以通过在
quasar.conf.js
中声明来手动执行,也可以通过自动导入您在代码中使用的组件、指令和插件来自动执行。我们将使用自动导入方法:
? Pick a Quasar components & directives import strategy: (can be changed later) (Use arrow key s)
❯ * Auto-import in-use Quasar components & directives - slightly
higher compile time; next to minimum bundle size; most
convenient
* Manually specify what to import - fastest compile time; minimum
bundle size; most tedious
* Import everything from Quasar - not treeshaking Quasar; biggest
bundle size; convenient
- 现在我们必须选择要添加到项目中的默认功能;我们将选择
ESLint
、Vuex
、Axios
和Vue-i18n
:
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯ ESLint
Vuex
TypeScript
Axios
Vue-i18n
IE11 support
- 现在您可以选择要在项目中使用的
ESLint
预设;在这种情况下,我们将选择AirBnB
:
? Pick an ESLint preset: (Use arrow keys)
Standard (https://github.com/standard/standard)
❯ Airbnb (https://github.com/airbnb/javascript)
Prettier (https://github.com/prettier/prettier)
- 您需要定义一个 Cordova/Capacitor ID(即使您不构建混合应用程序,也可以使用默认值):
? Cordova/Capacitor id (disregard if not building mobile apps)
org.cordova.quasar.app
- 最后,您可以选择要运行的软件包管理器,并安装您需要运行代码的软件包:
? Should we run `npm install` for you after the project has been
created? (recommended) (Use arrow keys)
Yes, use Yarn (recommended)
❯ Yes, use NPM
No, I will handle that myself
初始化 AWS Amplify 项目
要初始化您的 AWS Amplify 项目,请执行以下步骤:
- 打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify init
- Amplify CLI 将要求输入项目名称:
? Enter a name for the project: chatapp
- 然后,您需要为您的机器上正在运行的当前项目定义一个环境:
? Enter a name for the environment: dev
- 现在您可以选择您将在项目中使用的默认编辑器:
? Choose your default editor: (Use arrow keys)
❯ Visual Studio Code
Atom Editor
Sublime Text
InteliJ IDEA
Vim (via Terminal, Mac OS only)
Emac (via Terminal, Mac OS only)
None
- 您需要决定由 AWS Amplify 托管的项目类型。在我们的情况下,这将是一个 JavaScript 应用程序:
? Choose the type of app that you're building? (recommended) (Use
arrow keys)
android
ios
❯ javascript
- 对于框架,因为我们将使用 Quasar Framework 作为基础,我们需要从所呈现的框架列表中选择“无”:
? What javascript framework are you using? (recommended) (Use arrow
keys)
angular
ember
ionic
react
react-native
vue
❯ none
- 您将需要定义应用程序的源路径;您可以将源目录路径保留为默认值
src
。然后按Enter继续:
? Source Directory Path: (src)
- 对于分发目录,由于 Quasar 使用不同类型的路径组织,我们需要将其定义为
dist/spa
:
? Distribution Directory Path: dist/spa
- AWS Amplify 将在部署之前使用的构建命令,我们将将其定义为
quasar build
:
? Build Command: quasar build
- 对于启动命令,我们需要使用 Quasar 内置的
quasar dev
命令:
? Start Command: quasar dev
对于 Windows 用户,由于 Amplify 和 WSL 不兼容,您可能需要将启动命令定义如下:
? Start Command: quasar.cmd dev
- 现在 CLI 会询问我们是否要为此配置使用本地 AWS 配置文件:
? Do you want to use an AWS profile: y
- 我们将选择之前创建的默认配置文件:
? Please choose the profile you want to use: (Use arrow keys)
❯ default
- CLI 完成初始化过程后,我们需要向项目添加托管。为此,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify add hosting
- CLI 会询问您的应用程序的托管过程。选择“使用 Amplify Console 进行托管”,然后按Enter继续:
? Select the plugin module to execute
❯ Hosting with Amplify Console (Managed hosting with custom domains,
Continuous deployment)
Amazon CloudFront and S3
- 然后 CLI 会询问您部署过程将如何进行;选择“手动部署”,然后按Enter继续:
? Choose a type (Use arrow keys)
Continuous deployment (Git-based deployments)
❯ Manual deployment
Learn more
- 当您完成所有操作后,要完成此过程,您需要发布它。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify publish
- 您将被问及是否要继续发布,您可以接受。完成所有操作后,浏览器将打开默认的 Quasar Framework 首页:
它是如何工作的...
AWS Amplify 是 Web 开发人员的一体化解决方案,提供了一整套工具,从托管应用程序到后端开发。
我们能够快速轻松地构建应用程序并将其上线,完全没有遇到基础设施方面的问题。
在这个步骤中,我们设法创建了我们的 AWS 账户,并为本地开发和网页部署准备好了我们的第一个 AWS Amplify 环境。此外,我们还能够创建了将用作聊天应用程序的 Quasar Framework 项目,并将其部署到 AWS 基础设施中,以准备应用程序的未来发布。
另请参阅
-
您可以在
aws.amazon.com/amplify/
找到有关 AWS Amplify 的更多信息。 -
您可以在
docs.amplify.aws/
找到有关 AWS Amplify 框架的更多信息。 -
您可以在
quasar.dev/
找到有关 Quasar Framework 的更多信息。
创建您的第一个 GraphQL API
AWS Amplify 提供了在简单步骤和许多附加选项(包括身份验证、部署和环境)的情况下,开箱即用地拥有 GraphQL API 的可能性。这使我们能够仅使用 GraphQL SDL 模式快速开发 API,并且 AWS Amplify 将为连接构建 API、DynamoDB 实例和代理服务器。
在这个步骤中,我们将学习如何使用 AWS Amplify 创建 GraphQL API,并为身份验证添加 AWS Cognito 功能。
准备工作
此步骤的先决条件如下:
-
上一个步骤的项目
-
Node.js 12+
所需的 Node.js 全局对象是@aws-amplify/cli
。
要安装 AWS Amplify,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install -g @aws-amplify/cli
在这个步骤中,我们将使用创建您的 AWS Amplify 环境步骤中的项目。请先完成该步骤中的说明。
如何做...
要启动我们的 GraphQL API,我们将继续使用在创建您的 AWS Amplify 环境步骤中创建的项目。
这个步骤将分为两部分:创建 AWS Cognito 和创建 GraphQL API。
创建 AWS Cognito 身份验证
为了给我们的 API 和应用程序增加一层安全性,我们将使用 AWS Cognito 服务。这将提供对用户和身份验证的控制作为服务:
- 要初始化您的 AWS Cognito 配置,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify auth add
- 现在 CLI 会要求您选择用于创建 Cognito 服务的配置类型。这些是预先制定的规则和配置的选择。我们将选择
默认配置
:
Do you want to use default authentication and security configuration: (Use arrow keys)
❯ Default configuration Default configuration with Social Provider (Federation)
Manual configuration
I want to learn more.
- 之后,您需要选择用户将如何登录;因为我们正在构建一个聊天应用程序,我们将选择
电子邮件
:
Warning: you will not be able to edit these selections.
How do you want users to be able to sign in: (Use arrow keys)
Username
❯ Email Phone Number
Email and Phone Number
I want to learn more.
- 对于 AWS Cognito,不需要选择更高级的设置。我们可以通过选择
不,我完成了。
来跳过这一步。
Do you want to configure advanced settings: (Use arrow keys)
❯ No, I am done. Yes, I want to make some additional changes.
- 最后,我们需要将这个配置推送到云端。为此,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify auth push
- 您将被问及是否要继续 - 输入
y
,CLI 将发布配置到 AWS Cognito 云:
? Are you sure you want to continue: y
创建 GraphQL API
在这部分,我们将把说明分为两部分,首先创建 GraphQL SDL 模式,然后创建 GraphQL API。
创建 GraphQL SDL 模式
要使用 AWS Amplify 创建 GraphQL API,首先需要创建一个 GraphQL SDL 模式。AWS Amplify 将使用该模式生成 API 的数据库和解析器:
-
在
src
文件夹中创建一个名为chatApi.graphql
的新文件,并打开它。 -
创建我们基本的
S3Object
模式类型,这是一个简单的模型,用于管理放置在 AWS S3 存储桶中的文件的存储:
type S3Object {
bucket: String!
region: String!
key: String! }
- 然后我们将创建我们的
用户类型
。这就像一个带有更多规则附加的数据库模型。这个类型
将有一个@auth
规则,只允许所有者,在这种情况下是用户
,执行创建
、更新
和删除
操作。之后,我们将声明用户
字段:
type User
@model(subscriptions: null) @auth(rules: [
{ allow: owner, ownerField: "id", queries: null },
{ allow: owner, ownerField: "owner", queries: null },
]) {
id: ID!
email: String!
username: String!
avatar: S3Object
name: String
conversations: [ConversationLink] @connection(name: "UserLinks")
messages: [Message] @connection(name: "UserMessages", keyField: "authorId")
createdAt: String
updatedAt: String }
- 我们的
用户
将与另一个用户进行对话。我们将创建一个对话类型
,为了保护这个对话,我们将添加一个@auth
规则,以确保只有这个对话的成员可以看到用户之间交换的消息。在messages
字段中,我们将创建一个与消息类型
的@connection
,并在关联字段中创建一个与对话链接类型
的@connection
:
type Conversation
@model(
mutations: { create: "createConversation" }
queries: { get: "getConversation" }
subscriptions: null ) @auth(rules: [{ allow: owner, ownerField: "members" }]) {
id: ID!
messages: [Message] @connection(name: "ConversationMessages",
sortField: "createdAt")
associated: [ConversationLink] @connection(name:
"AssociatedLinks")
name: String!
members: [String!]!
createdAt: String
updatedAt: String }
- 对于
消息类型
,我们需要添加一个@auth
装饰器规则,只允许所有者对其进行操作。我们需要创建一个@connection
装饰器,将author
字段连接到用户类型
,并创建一个@connection
装饰器,将conversation
字段连接到对话类型
:
type Message
@model(subscriptions: null, queries: null) @auth(rules: [{ allow: owner, ownerField: "authorId", operations: [create, update, delete]}]) {
id: ID!
author: User @connection(name: "UserMessages", keyField:
"authorId")
authorId: String
content: String!
conversation: Conversation! @connection(name: "ConversationMessages")
messageConversationId: ID!
createdAt: String
updatedAt: String }
- 现在我们正在使用
type ConversationLink
将对话链接在一起。这个type
需要user
字段具有@connection
装饰器到User
和@connection
对话到type Conversation
:
type ConversationLink
@model(
mutations: { create: "createConversationLink", update:
"updateConversationLink" }
queries: null
subscriptions: null ) {
id: ID!
user: User! @connection(name: "UserLinks")
conversationLinkUserId: ID
conversation: Conversation! @connection(name: "AssociatedLinks")
conversationLinkConversationId: ID!
createdAt: String
updatedAt: String }
- 最后,我们需要创建一个
type Subscription
来在 GraphQL API 内部具有事件处理程序。Subscription
类型会监听并处理特定变化的特定变化,createConversationLink
和createMessage
,两者都会在数据库内触发事件:
type Subscription {
onCreateConversationLink(conversationLinkUserId: ID!):
ConversationLink
@aws_subscribe(mutations: ["createConversationLink"])
onCreateMessage(messageConversationId: ID!): Message
@aws_subscribe(mutations: ["createMessage"])
onCreateUser: User
@aws_subscribe(mutations: ["createUser"])
onDeleteUser: User
@aws_subscribe(mutations: ["deleteUser"])
onUpdateUser: User
@aws_subscribe(mutations: ["updateUser"]) }
使用 AWS Amplify 创建 GraphQL API
在这里,我们将使用 AWS Amplify API 使用先前创建的 GraphQL 模式来创建我们的 GraphQL API:
- 要初始化您的 AWS Amplify API 配置,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify add api
- 在这里,CLI 将询问您要创建什么类型的 API。我们将选择
GraphQL
:
? Please select from one of the below mentioned services: (Use arrow
keys)
❯ GraphQL REST
- 现在 CLI 将要求输入 API 名称(您可以选择):
? Provide API name: chatapp
- 在这里,我们将选择 API 将使用的身份验证方法。由于我们将使用 AWS Cognito,我们需要选择
Amazon Cognito User Pool
选项:
? Choose the default authorization type for the API: (Use arrow
keys)
API key
❯ Amazon Cognito User Pool IAM
OpenID Connect
- 然后 CLI 将询问您是否要在 API 上配置更多设置;我们将选择
No, I am done.
选项:
? Do you want to configure advanced settings for the GraphQL API:
(Use arrow keys)
❯ No, I am done. Yes, I want to make some additional changes.
- 现在我们将被问及是否有注释的 GraphQL 模式;由于我们之前已经编写了一个,我们需要输入
y
:
? Do you have an annotated GraphQL schema?: y
- 在这里,我们需要输入刚刚创建的文件的路径
./src/chatApi.graphql
:
? Provide your schema file path: ./src/chatApi.graphql
- 完成后,我们需要将配置推送到 AWS Amplify。要执行此操作,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify push
- 当询问是否要继续时,输入
y
:
? Are you sure you want to continue?: y
- CLI 将询问您是否要为新创建的 GraphQL API 生成代码;再次输入
y
:
? Do you want to generate code for your newly created GraphQL API: y
- 在这里,您可以选择 CLI 要使用的语言来创建项目中使用的通信文件。我们将选择
javascript
,但您可以选择最符合您需求的语言:
? Choose the code generation language target: (Use arrow keys)
❯ javascript
typescript
flow
- CLI 将询问要放置将生成的文件的位置,我们将使用默认值:
? Enter the file name pattern of graphql queries, mutation and
subscriptions: (src/graphql/***/**.js)
- 现在 CLI 将询问有关 GraphQL 操作的生成。由于我们正在创建我们的第一个 GraphQL API,我们将选择
y
,因此 CLI 将为我们创建所有文件:
? Do you want to generate/update all possible GraphQL operations -
queries, mutations and subscriptions: y
- 最后,我们可以定义文件中模式的最大深度,我们将使用默认值
2
:
? Enter maximum statement depth [increase from default if your
schema is deeply nested]: (2)
- 当你完成所有的事情后,我们需要将配置发布到 AWS Amplify。要做到这一点,你需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify publish
工作原理...
在创建一个带有 AWS Amplify 的 GraphQL API 的过程中,我们需要一个预先构建的模式,用于生成数据库和端点。这个模式是基于 GraphQL SDL 语言的。Amplify 已经在 SDL 中添加了更多的装饰符,这样我们就可以在 API 的开发中拥有更广泛的可能性。
与此同时,我们需要创建一个 AWS Cognito 用户池,用于保存将在应用程序上注册的用户。这是为了在应用程序外部管理和维护身份验证层,并作为一个服务使用,可以提供更多功能,包括双因素身份验证、必填字段和恢复模式。
最后,在一切都完成之后,我们的 API 已经在 AWS Amplify 上发布,并准备好进行开发,具有可以用作开发环境的 URL。
另请参阅
-
你可以在
graphql.org/learn/schema/
找到更多关于 GraphQL SDL 的信息。 -
你可以在
docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js
找到更多关于 AWS Amplify API 的信息。 -
你可以在
docs.amplify.aws/lib/auth/getting-started/q/platform/js
找到更多关于 AWS Amplify 身份验证的信息。
将 GraphQL 客户端添加到你的应用程序
Apollo Client 目前是 JavaScript 生态系统中最好的 GraphQL 客户端实现。它有一个庞大的社区支持,并得到了大公司的支持。
我们的 AWS Amplify GraphQL API 的实现在后端使用了 Apollo Server,因此 Apollo Client 的使用将是一个完美的匹配。AWS AppSync 也使用他们自己的 Apollo 实现作为客户端,所以我们仍然会使用 Apollo 作为客户端,但不是直接使用。
在这个配方中,我们将学习如何将 GraphQL 客户端添加到我们的应用程序中,以及如何连接到 AWS Amplify GraphQL 服务器来执行查询。
准备工作
这个配方的先决条件如下:
-
上一个配方的项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
在这个示例中,我们将使用创建您的第一个 GraphQL API示例中的项目。在遵循本示例之前,请按照上一个示例中的步骤进行操作。
如何做...
我们将使用 Amplify 客户端将 GraphQL 客户端添加到我们的应用程序中。按照以下步骤创建 GraphQL 驱动程序:
- 要安装使用 GraphQL 客户端所需的软件包,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save graphql aws-amplify graphql-tag aws-appsync
-
在
boot
文件夹中创建一个名为amplify.js
的新文件,并打开它。 -
在这个文件中,我们将导入
aws-amplify
包和 AWS Amplify CLI 在配置过程中为我们创建的aws-exports.js
文件。我们将使用我们拥有的配置来配置 Amplify。为了使 Quasar 引导文件起作用,我们需要导出一个default
空函数:
import Amplify from 'aws-amplify'; import AwsExports from '../aws-exports'; Amplify.configure(AwsExports); export default () => {};
- 在
root
文件夹中的quasar.conf.js
文件中,我们需要向webpack
捆绑器添加新规则。要做到这一点,找到extendWebpack
函数。在函数的第一行之后,创建两个新规则给捆绑器,第一个规则将添加graphql-loader
webpack 加载程序,第二个规则将允许捆绑器理解.mjs
文件:
// The rest of the quasar.conf.js... extendWebpack (cfg) {
//New rules that need to be added
cfg.module.rules.push({
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: 'graphql-tag/loader',
}); cfg.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
});
// Maintain these rules cfg.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /node_modules/,
options: {
formatter:
require('eslint').CLIEngine.getFormatter('stylish'),
},
}); cfg.resolve.alias = {
...cfg.resolve.alias,
driver: path.resolve(__dirname, './src/driver'),
}; }, // The rest of the quasar.conf.js...
-
现在,在
src/driver
文件夹中创建一个名为graphql.js
的新文件,并打开它。 -
在这个文件中,我们需要从
aws-appsync
包中导入AWSAppSyncClient
,从aws-amplify
包中导入Auth
,并从src
文件夹中的aws-exports.js
文件中导入AwsExports
。然后,我们需要使用aws-exports
的配置实例化AWSAppSyncClient
,并导出客户端的这个实例化:
import AWSAppSyncClient from 'aws-appsync'; import { Auth } from 'aws-amplify'; import AwsExports from '../aws-exports'; export default new AWSAppSyncClient({
url: AwsExports.aws_appsync_graphqlEndpoint,
region: AwsExports.aws_appsync_region,
auth: {
type: AwsExports.aws_appsync_authenticationType,
jwtToken: async () => (await
Auth.currentSession()).idToken.jwtToken,
}, });
- 在
quasar.conf.js
文件中的root
文件夹中,我们需要将新创建的amplify.js
文件添加到引导序列中,该文件位于boot
文件夹中。要做到这一点,找到boot
数组,并在末尾添加文件在boot
文件夹中的路径作为字符串,不包括扩展名。在我们的情况下,这将是'amplify'
:
// The rest of the quasar.conf.js...
boot: [ 'axios',
'amplify' ], // The rest of the quasar.conf.js...
它是如何工作的...
我们在全局范围内将aws-amplify
包添加到我们的应用程序中,并通过新的graphql.js
文件中的导出条目使其可用于使用。这使得在应用程序中可以使用AWSAmplifyAppSync
。
使用 Quasar Framework 的引导过程,我们能够在 Vue 应用程序开始在屏幕上呈现之前实例化 Amplify。
另请参阅
-
您可以在
docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js
找到有关 AWS Amplify AppSync 的更多信息。 -
您可以在
quasar.dev/quasar-cli/developing-ssr/writing-universal-code#Boot-Files
找到有关 Quasar Framework 引导文件的更多信息。
为您的应用程序创建 AWS Amplify 驱动程序
为了与 AWS Amplify 服务进行通信,我们需要使用他们的 SDK。这个过程是重复的,可以合并到我们将要使用的每个 Amplify 服务的驱动程序中。
在这个示例中,我们将学习如何创建通信驱动程序,以及如何使用 AWS Amplify 进行操作。
准备工作
这个示例的先决条件如下:
-
上一个示例的项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
在这个示例中,我们将使用将 GraphQL 客户端添加到您的应用程序示例中的项目。请先完成该示例中的说明。
如何做...
在这个示例中,我们将其分为三个部分:第一部分将用于 AWS 存储驱动程序,第二部分将用于 Amplify Auth 驱动程序,最后,我们将看到 Amplify AppSync 实例的创建。
创建 AWS Amplify 存储驱动程序
要创建 AWS Amplify 存储驱动程序,我们首先需要创建 AWS Amplify 存储基础设施,并在我们的环境中设置好,之后我们需要创建 AWS Amplify 存储 SDK 与我们的应用程序之间的通信驱动程序。
添加 AWS Amplify 存储
在这部分,我们将向我们的 Amplify 服务列表中添加 AWS S3 功能。这是必需的,这样我们就可以在 AWS S3 云基础设施上保存文件:
- 首先,我们需要向项目添加 AWS 存储。为此,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹并执行以下命令:
> amplify add storage
- 现在我们需要选择将上传什么内容。我们需要选择
内容(图片、音频、视频等)
:
? Please select from one of the below mentioned services: (Use arrow
keys)
❯ Content (Images, audio, video, etc.) NoSQL Database
- 我们需要为资源添加一个名称。我们将其称为
bucket
:
? Please provide a friendly name for your resource that will be used
to label this category in the project: bucket
- 现在我们需要提供一个 AWS S3 存储桶名称。我们将其称为
chatappbucket
:
? Please provide bucket name: chatappbucket
- 然后我们需要选择谁可以操作存储桶文件。由于应用程序将仅基于授权,我们需要选择
仅授权用户
:
? Who should have access: (Use arrow keys)
❯ Auth users only Auth and guest users
- 现在您需要选择用户在存储桶中的访问级别:
? What kind of access do you want for Authenticated users?
create/update
read
❯ delete
- 当被问及创建自定义 Lambda 触发器时,选择
n
:
? Do you want to add a Lambda Trigger for you S3 Bucket: n
- 最后,我们需要将更改推送到云端。为此,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify push
- 当您完成所有操作后,我们需要将配置发布到 AWS Amplify。为此,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify publish
创建 Amplify Storage 驱动程序
在这部分,我们将创建与 Amplify Storage 通信的驱动程序。该驱动程序将处理我们应用程序中的文件上传:
-
在
src/driver
文件夹中创建一个名为bucket.js
的新文件并打开它。 -
从
aws-amplify
包中导入Storage
类,从quasar
中导入uid
函数,以及AwsExports
:
import { Storage } from 'aws-amplify'; import { uid } from 'quasar'; import AwsExports from '../aws-exports';
- 创建一个名为
uploadFile
的异步函数,它接收三个参数:file
,name
和type
。name
参数的默认值为uid()
,type
参数的默认值为'image/png'
。在这个函数中,我们将调用Storage.put
函数,传递name
和file
作为参数,作为第三个参数,我们将传递一个 JavaScript 对象,其中contentType
属性定义为接收到的type
,并且accept
属性定义为'**/**'
。上传完成后,我们将返回一个具有bucket
,region
和uploadedFile
属性的 JavaScript 对象:
export async function uploadFile(file, name = uid(), type = 'image/png') {
try {
const uploadedFile = await Storage.put(name, file, {
contentType: type,
accept: '*/*',
}); return {
...uploadedFile,
bucket: AwsConfig.aws_user_files_s3_bucket,
region: AwsConfig.aws_user_files_s3_bucket_region,
};
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
getFile
的异步函数,它接收name
参数,默认值为空字符串。在函数内部,我们将返回Storage.get
,传递name
参数和设置为public
级别的选项:
export async function getFile(name = '') {
try {
return await Storage.get(name, { level: 'public' });
} catch (err) {
return Promise.reject(err);
} }
- 最后,导出一个默认的 JavaScript 对象,并将创建的函数
uploadFile
和getFile
作为属性添加进去:
export default {
uploadFile,
getFile, };
创建 Amplify Auth 驱动程序
现在我们将创建认证驱动程序。该驱动程序负责处理应用程序中的所有认证请求并获取用户信息:
-
在
src/driver
文件夹中创建一个名为auth.js
的新文件并打开它。 -
在新创建的文件中,从
aws-amplify
包中导入Auth
类:
import { Auth } from 'aws-amplify';
- 创建一个名为
signIn
的新异步函数。它将接收email
和password
作为参数,并且该函数将返回Auth.signIn
函数,传递email
和password
作为参数:
export async function signIn(email = '', password = '') {
try {
return Auth.signIn({
username: email,
password,
});
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
signUp
的新异步函数,该函数将接收email
和password
作为参数。该函数将返回Auth.signUp
函数,传递一个带有这些属性的 JavaScript 对象作为参数:username
、password
、attributes
和validationData
。
username
属性将是作为参数接收的email
值。
password
属性将是作为参数接收的password
值。
attributes
属性将是一个带有email
属性的 JavaScript 对象,该属性将作为参数接收:
export async function signUp(email = '', password = '') {
try {
return Auth.signUp({
username: email,
password: `${password}`,
attributes: {
email,
},
validationData: [],
});
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
validateUser
的新异步函数,该函数将接收username
和code
作为参数。该函数等待Auth.confirmSignUp
函数的响应,将username
和code
作为参数传递给该函数,并在完成时返回true
:
export async function validateUser(username = '', code = '') {
try {
await Auth.confirmSignUp(username, `${code}`); return Promise.resolve(true);
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
resendValidationCode
的新异步函数,该函数将接收username
作为参数。该函数返回Auth.resendSignUp
函数,将username
作为参数:
export async function resendValidationCode(username = '') {
try {
return Auth.resendSignUp(username);
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
signOut
的新异步函数,该函数返回Auth.signOut
函数:
export async function signOut() {
try {
return Auth.signOut();
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
changePassword
的新异步函数,该函数将接收oldPassword
和newPassword
作为参数。该函数等待获取当前经过身份验证的用户,并返回Auth.changePassword
函数,将获取的user
、oldPassword
和newPassword
作为参数:
export async function changePassword(oldPassword = '', newPassword = '') {
try {
const user = await Auth.currentAuthenticatedUser();
return Auth.changePassword(user, `${oldPassword}`, `${newPassword}`);
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
getCurrentAuthUser
的新异步函数;该函数将获取当前经过身份验证的用户,并返回一个带有id
、email
和username
属性的 JavaScript 对象:
export async function getCurrentAuthUser() {
try {
const user = await Auth.currentAuthenticatedUser(); return Promise.resolve({
id: user.username,
email: user.signInUserSession.idToken.payload.email,
username: user.username,
});
} catch (err) {
return Promise.reject(err);
} }
创建 Amplify AppSync 实例
在经过身份验证的情况下与 AWS Amplify API 通信,我们需要创建一个新的 AWS Amplify AppSync API 实例,其中包含用户身份验证信息:
-
在
src/driver
文件夹中创建一个名为appsync.js
的新文件并打开它。 -
在新创建的文件中,从
aws-amplify
包中导入Auth
和API
,从@aws-amplify/api
包中导入GRAPHQL_AUTH_MODE
枚举,以及 AWS 配置:
import { Auth, API } from 'aws-amplify'; import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api'; import AwsExports from '../aws-exports';
- 通过执行
API.configure
函数从aws-amplify
包中配置 API,传递一个 JavaScript 对象作为参数,其中包含url
、region
和auth
的属性。
在url
属性中,传递 GraphQL 端点 URL 的配置。
在region
属性中,传递当前正在使用的 AWS 区域的配置。
在auth
属性中,我们需要传递一个具有两个属性type
和jwtToken
的 JavaScript 对象。
我们需要将type
属性设置为GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS
。
在jwtToken
中,我们将传递一个异步函数,该函数将返回当前登录用户的令牌:
API.configure({
url: awsconfig.aws_appsync_graphqlEndpoint,
region: awsconfig.aws_appsync_region,
auth: {
type: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(),
}, });
- 最后,我们将
API
导出为名为AuthAPI
的常量:
export const AuthAPI = API;
工作原理...
在这个示例中,我们学习了如何将应用程序的责任分离为可以在多个领域重复使用而无需重写整个代码的驱动程序。通过这个过程,我们能够创建一个用于 Amplify 存储的驱动程序,可以异步发送文件,并且这些文件被保存在 AWS S3 服务器上的存储桶中。
在我们对 Auth 驱动程序的工作中,我们能够创建一个可以管理 Amplify 身份验证 SDK 并在需要时提供信息并封装特殊功能以使在我们的应用程序中执行任务更容易的驱动程序。
最后,在 Amplify AppSync API 中,我们成功实例化了 API 连接器,并使用了所有需要的身份验证标头,以便应用程序可以在没有任何问题的情况下执行,并且用户可以在请求时访问所有信息。
另请参阅
-
在
docs.amplify.aws/lib/storage/getting-started/q/platform/js
上查找有关 AWS Amplify Storage 的更多信息。 -
在
docs.amplify.aws/lib/auth/getting-started/q/platform/js
上查找有关 AWS Amplify Auth 的更多信息。 -
在
docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js
上查找有关 AWS Amplify AppSync 的更多信息。
第四章:创建自定义应用程序组件和布局
要开始我们应用程序的开发,我们需要创建整个应用程序将使用的自定义组件和输入。这些组件将采用无状态的方法创建。
我们将开发UsernameInput
组件,PasswordInput
组件,EmailInput
组件和AvatarInput
组件。我们还将开发应用程序页面和聊天布局的基本布局,它将包装聊天页面。
在本章中,我们将涵盖以下示例:
-
为应用程序创建自定义输入
-
创建应用程序布局
技术要求
在本章中,我们将使用Node.js和Quasar Framework。
注意,Windows 用户!您需要安装一个名为windows-build-tools
的npm
包,以便能够安装所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行以下命令:
> npm install -g windows-build-tools
要安装 Quasar Framework,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @quasar/cli
为应用程序创建自定义输入
创建应用程序需要创建大量的表单。所有这些表单都需要输入,这些输入很可能在应用程序中重复出现。
在这个示例中,我们将创建自定义输入表单,我们将在几乎每个表单中使用它们。
创建自定义输入表单的过程有助于开发人员节省调试时间,代码的可重用性和未来的改进。
准备工作
这个示例的先决条件如下:
-
最后的示例项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始我们的自定义组件,我们将继续使用在第三章设置我们的聊天应用程序 - AWS Amplify 环境和 GraphQL中创建的项目。
如何做...
为了更好地重用代码,我们将创建单独的组件来处理应用程序上的自定义表单。在这种情况下,我们将创建六个组件:
-
UsernameInput
-
PasswordInput
-
NameInput
-
EmailInput
-
AvatarInput
-
AvatarDisplay
所以,让我们开始吧。
创建 UsernameInput 组件
UsernameInput
将负责处理用户名的检查和验证,这样我们就不需要在每个需要使用它的页面上重新编写所有规则。
单文件组件<script>
部分
在这里,我们将创建UsernameInput
组件的<script>
部分:
-
在
src/components
文件夹中创建一个名为UsernameInput.vue
的新文件,并打开它。 -
创建一个带有
name
和props
属性的默认导出的 JavaScript 对象:
export default {
name: '',
props: {}, };
- 对于
name
属性,将其定义为"UsernameInput"
:
name: 'UsernameInput',
- 对于
props
属性,将其定义为一个 JavaScript 对象,并添加一个名为value
的新属性,它也将是一个具有type
,default
和required
属性的 JavaScript 对象。type
属性需要定义为String
,default
为''
,required
为false
:
props: {
value: {
type: String,
default: '',
required: false,
},
},
单文件组件<template>
部分
在这里,我们将创建UsernameInput
组件的<template>
部分:
-
在
<template>
部分,创建一个QInput
组件。创建两个动态属性,value
和rules
。现在,value
将绑定到value
属性,rules
属性将接收一个数组。数组的第一项是一个函数,用于验证输入,第二项是出现错误时的消息。 -
将
outlined
和lazy-rules
属性设置为true
,并将label
属性定义为"Your Username"
。 -
最后,通过创建一个
v-on
指令,使用$listeners
Vue API 作为值来为事件创建事件侦听器。
完成所有步骤后,您的最终代码应该像这样:
<template>
<q-input
:value="value"
:rules="[ val => (val && val.length > 5 || 'Please type a valid
Username')]"
outlined
label="Your Username"
lazy-rules
v-on="$listeners"
/> </template>
这是您的组件呈现出来的样子:
创建一个 PasswordInput 组件
PasswordInput
将是一个组件,具有特殊逻辑,通过单击按钮切换密码的可见性。我们将在这个组件中包装这个逻辑,这样每次使用这个组件时就不需要重新编写它。
单文件组件<script>
部分
在这部分,我们将创建PasswordInput
组件的<script>
部分:
-
在
components
文件夹中创建一个名为PasswordInput.vue
的新文件,并打开它。 -
创建一个默认导出的 JavaScript 对象,具有三个属性,
name
,props
和data
:
export default {
name: '',
props: {},
data: () => (), };
- 对于
name
属性,将值定义为"PasswordInput"
:
name: 'PasswordInput',
- 对于
props
属性,添加两个属性,value
和label
,都是 JavaScript 对象。每个对象内部应该有三个属性:type
,default
和required
。将value.type
设置为String
,value.default
设置为''
,value.required
设置为false
。然后,将label.type
设置为String
,label.default
设置为'Your Password'
,label.required
设置为false
:
props: {
value: {
type: String,
default: '',
required: false,
},
label: {
type: String,
default: 'Your password',
required: false,
}, },
- 最后,在
data
属性中,添加一个 JavaScript 对象作为返回值,其中isPwd
值设置为true
:
data: () => ({
isPwd: true, }),
单文件组件部分
现在我们将创建PasswordInput
的<template>
部分。按照以下说明来实现正确的输入组件:
-
在<template>
部分,创建一个QInput
组件,并将value
,label
和rules
属性添加为变量。value
将绑定到value
属性,label
将绑定到label
属性,rules
将接收一个函数数组,用于执行对表单输入的基本验证。
-
对于type
属性,将其定义为一个变量,并将其设置为对isPwd
的三元验证,在"password"
和"text"
之间切换。
-
将outlined
和lazy-rules
属性设置为true
。
-
创建一个hint
变量属性,并将其定义为三元运算符,它将检查当前值的长度是否匹配最小值大小;否则,它将向用户显示一条消息。
-
然后,通过创建一个v-on
指令并使用$listeners
Vue API 作为值来为事件创建事件侦听器。
-
在QInput
模板内部,我们将添加一个子组件,该组件将占据一个命名插槽v-slot:append
,该插槽将容纳一个QIcon
组件。
-
对于QIcon
组件,定义name
属性以对isPwd
变量进行响应,因此当isPwd
设置为true
时,它将是'visibility_off'
,或者当isPwd
设置为false
时,它将是'visibility'
。将class
属性定义为"cursor-pointer"
,以便鼠标具有实际鼠标的外观和"hover hand icon"
,并在@click
事件侦听器上,我们将设置isPwd
为当前isPwd
的相反值。
完成所有步骤后,您的最终代码应该像这样:
<template>
<q-input
:value="value"
:type="isPwd ? 'password' : 'text'"
:rules="[ val => val.length >= 8 || 'Your password need to have 8
or more characters', val => val !== null && val !== '' ||
'Please type your password']"
:hint=" value.length < 8 ? 'Your password has a minimum of 8
characters' : ''"
:label="label"
outlined
lazy-rules
v-on="$listeners"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input> </template>
这是您的组件呈现的方式:
创建 NameInput 组件
在我们创建的所有组件中,NameInput
组件是最简单的,几乎没有改变QInput
组件的行为,只是添加了验证规则和一些个性化。
单文件组件<script>部分
在这部分,我们将创建NameInput
组件的<script>
部分:
- 创建一个默认导出的 JavaScript 对象,有两个属性:
name
和props
:
export default {
name: '',
props: {}, };
- 在
name
属性中,将值定义为'NameInput'
:
name: 'NameInput',
- 在
props
属性中,添加一个属性value
,作为一个 JavaScript 对象,里面有三个属性:type
,default
和required
。将value.type
设置为String
,value.default
设置为**''**
,value.required
设置为false
:
props: {
value: {
type: String,
default: '',
required: false,
}, },
单文件组件部分
在这部分,我们将创建NameInput
组件的<template>
部分:
-
在<template>
部分,创建一个QInput
组件,并添加value
和rules
属性作为变量。value
将绑定到value
属性,rules
将接收一个函数数组,用于检查表单输入的基本验证。
-
将outlined
和lazy-rules
属性设置为true
,并将label
属性定义为"Your Name"
。
-
最后,通过创建一个v-on
指令并将"$listeners"
Vue API 作为值来为事件创建事件监听器。
完成所有步骤后,你的最终代码应该像这样:
<template>
<q-input
:value="value"
:rules="[ val => (val && val.length > 0
|| 'Please type a valid Name')]"
outlined
label="Your Name"
lazy-rules
v-on="$listeners"
/> </template>
这是你的组件渲染结果:
创建 EmailInput 组件
在EmailInput
组件中,我们需要特别注意规则验证的处理,因为我们需要检查正在输入的电子邮件是否是有效的电子邮件地址。
单文件组件<script>部分
在这部分,我们将创建EmailInput
组件的<script>
部分:
- 创建一个默认导出的 JavaScript 对象,有三个属性:
name
,props
和methods
:
export default {
name: '',
props: {},
methods: {}, };
- 在
name
属性中,将值定义为'EmailInput'
:
name: 'EmailInput',
- 在
props
属性中,添加一个属性value
,作为一个 JavaScript 对象,里面有三个属性:type
,default
和required
。将value.type
设置为String
,value.default
设置为**'**
,value.required
设置为false
:
props: {
value: {
type: String,
default: '',
required: false,
}, },
- 在
methods
属性中,我们需要添加一个名为validateEmail
的新方法,该方法接收一个名为email
的参数。此方法将通过正则表达式测试接收到的参数,以检查它是否是有效的表达式,并返回结果:
methods: {
validateEmail(email) {
const regex = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;
<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-
z\-]+\.)+[A-Za-z]{2,}))$/;
return regex.test(email);
}, },
单文件组件部分
在这里,我们将创建EmailInput
组件的<template>
部分:
-
在<template>
部分,创建一个QInput
组件,并将value
和rules
属性作为变量添加。value
将绑定到value
属性,rules
将接收一个函数数组,用于执行基本验证表单输入的检查。
-
将outlined
和lazy-rules
属性添加为true
,将label
属性定义为"Your E-Mail"
,将type
属性定义为"email"
.
-
最后,通过创建一个v-on
指令并将"$listeners"
作为值,为事件创建事件侦听器。
完成所有步骤后,您的最终代码应该像这样:
<template>
<q-input
:value="value"
:rules="[ val => (val && val.length > 0 && validateEmail(val)
|| 'Please type a valid E-mail')]"
outlined
type="email"
label="Your E-mail"
lazy-rules
v-on="$listeners"
/> </template>
这是您的组件呈现:
创建 AvatarInput 组件
对于AvatarInput
组件,我们需要添加使用AWS-Amplify Storage
API 驱动程序的逻辑。通过这样做,我们可以直接通过组件上传文件,并使逻辑和组件在整个应用程序中更具可重用性。
单文件组件<script>部分
在这部分,我们将创建AvatarInput
组件的<script>
部分:
- 从
quasar
包中导入uid
和从'src/driver/bucket'
中导入uploadFile
:
import { uid } from 'quasar';
import { uploadFile } from 'src/driver/bucket';
- 创建一个默认导出的 JavaScript 对象,具有四个属性,
name
,props
,data
和methods
:
export default {
name: '',
props: {},
data: () => ({})
methods: {}, };
- 在
name
属性中,将值定义为"AvatarInput"
:
name: 'AvatarInput',
- 在
props
属性中,添加一个属性value
,作为 JavaScript 对象,内部有三个属性 - type
,default
和required
。将value.type
设置为Object
,将value.default
设置为返回 JavaScript 对象的工厂函数,将value.required
设置为false
:
props: {
value: {
type: Object,
required: false,
default: () => ({}),
}, },
- 在
data
属性中,我们需要添加六个新属性:file
,type
,name
,s3file
,photoUrl
和canUpload
:
-
file
属性将是一个数组。
-
type
,name
和photoUrl
将是字符串。
-
canUpload
属性将是一个布尔值,定义为false
。
-
s3file
将是一个具有三个属性的 JavaScript 对象,key
,bucket
和region
,它们都是字符串:
data: () => ({
file: [],
type: '',
name: '',
s3file: {
key: '',
bucket: '',
region: '',
},
photoUrl: '',
canUpload: false, }),
- 在
methods
属性上,我们需要添加一个名为uploadFile
的新方法。这个方法将检查是否可以开始上传过程,然后调用uploadFile
函数,传递this.file
、this.name
和this.type
作为参数。在我们收到上传函数的响应后,我们将使用结果来定义this.s3File
和$emit
以及事件'input'
。最后,我们将this.canUpload
定义为false
:
async uploadFile() {
try {
if (this.canUpload) {
const file = await uploadFile(this.file, this.name,
this.type);
this.s3file = file;
this.$emit('input', file);
this.canUpload = false;
} } catch (err) {
console.error(err);
} },
- 最后,创建一个名为
getFile
的方法,它接收$event
作为参数。在函数中,我们将把this.type
定义为$event.type
,将this.name
定义为uid
生成函数和文件名的连接。然后,我们将为FileReader
实例创建一个监听器,它将把that.photoURL
设置为读取的结果,并将that.canUpload
设置为true
:
getFile($event) {
this.type = $event.type;
this.name = `${uid()}-${$event.name}`;
const that = this;
const reader = new FileReader();
reader.onload = ({ target }) => {
that.photoUrl = target.result;
that.canUpload = true;
};
reader.readAsDataURL(this.file); },
单文件组件部分
现在是创建AvatarInput
组件的<template>
部分的时候了:
- 创建一个
QFile
组件,将v-model
指令绑定到file
数据属性。将outlined
和bottom-slots
属性定义为true
,并将label
属性设置为"Your Avatar"
。对于class
属性,将其设置为"q-pr-md"
,最后将@input
事件监听器设置为目标getFile
方法:
<q-file
v-model="file"
outlined
bottom-slots label="Your Avatar"
class="q-pr-md"
@input="getFile" >
</q-file>
- 在
QFile
组件内部,我们将添加一个直接子组件,它将放置在一个命名为v-slot:before
的插槽中,并且只有在数据属性中存在任何photoUrl
时才会显示。在这个插槽中,我们将添加一个QAvatar
组件,其子组件是一个HTML img
标签,其中src
属性绑定到photoUrl
数据属性:
<template
v-if="photoUrl"
v-slot:before >
<q-avatar>
<img :src="photoUrl">
</q-avatar> </template>
- 在我们创建的插槽之后,我们需要创建另一个插槽,现在放置在名为
v-slot:after
的插槽下面,里面有一个QBtn
组件。QBtn
将具有以下属性:round
、dense
、flat
、icon
定义为"cloud_upload"
,并且@click
事件监听器绑定到uploadFile
方法:
<template v-slot:after>
<q-btn
round
dense flat icon="cloud_upload"
@click="uploadFile"
/> </template>
这是您的组件渲染结果:
创建 avatar mixin
在这里,我们将创建一个简单的 mixin,它将用于在新的对话组件和联系人页面中显示用户头像,或者如果没有定义头像,则显示用户名的首字母:
-
在src
文件夹下创建一个名为mixins
的新文件夹,然后创建一个名为getAvatar.js
的文件,并打开它。
-
从driver/bucket
文件中导入getFile
函数。
-
导出一个带有methods
属性的default
JavaScript 对象。在methods
属性内部,创建一个名为getAvatar
的新函数。此函数将接收两个参数,object
和name
。对于此函数,我们将检查对象是否为null
,以及是否有一个名字来显示初始字母。如果 JavaScript 对象中有属性,我们将返回getFile
函数的结果,将key
属性作为参数传递:
import { uploadFile } from 'src/driver/bucket'; export default {
methods: {
async getAvatar(object, name) {
const baseUrl = 'http://placehold.jp/350/9c27b0/FFFFFF/600x600.png?text='; if (object === null && !name) return `${baseUrl}%20`; if (!object && name) return `${baseUrl}${name.split('').shift()}`; return getFile(object.key);
}, }, };
创建 AvatarDisplay 组件
AvatarDisplay
将负责处理用户名的检查和验证,因此我们不需要在每个需要使用它的页面上重新编写所有规则。
单文件组件<script>部分
在这里,我们将创建AvatarDisplay
组件的<script>
部分:
-
在components
文件夹中创建一个名为AvatarDisplay.vue
的新文件,并打开它。
-
创建一个带有以下属性的export default
JavaScript 对象:name
,props
,mixins
,beforeMount
,data
,watch
,computed
和methods
:
import { QImg } from 'quasar'; import getAvatar from 'src/mixins/getAvatar'; export default {
name: '',
props: {},
mixins: [],
async beforeMount() {},
data: () => ({}),
watch: {},
computed: {},
methods: {}, };
- 对于
name
属性,将其定义为"AvatarDisplay"
:
name: 'UsernameInput',
- 对于
props
属性,将其定义为 JavaScript 对象,并添加三个新属性,分别称为avatarObject
,name
和tag
。avatarObject
属性将是一个具有type
,default
和required
属性的 JavaScript 对象。name
和tag
属性需要定义为String
,default
为''
,required
为false
。对于tag
属性,我们将将默认属性设置为'q-img'
:
props: {
avatarObject: {
type: Object,
required: false,
default: () => ({}),
},
name: {
type: String,
required: false,
default: '',
},
tag: {
type: String,
required: false,
default: 'q-img',
}, },
- 对于
mixins
属性,我们将在数组中添加导入的getAvatar
mixin:
mixins: [getAvatar],
- 现在,在
data
中返回 JavaScript 对象,我们将创建一个名为src
的属性,其默认值为''
:
data: () => ({
src: '', }),
- 然后对于
computed
属性,创建一个名为 components 的新属性,返回一个三元运算符,检查tag
属性是否等于'q-img'
,并返回 Quasar 中导入的QImg
组件;如果不是,则返回'img'
标签:
computed: {
componentIs() {
return this.tag === 'q-img' ? QImg : 'img';
}, },
- 在
methods
属性中,创建一个名为updateSrc
的新方法。在这个方法中,我们将src
定义为getAvatar
方法的结果。我们将函数的参数传递给avatarObject
和name
属性:
methods: {
async updateSrc() {
this.src = await this.getAvatar(this.avatarObject, this.name);
}, },
- 在
beforeMount
生命周期钩子中,我们将调用updateSrc
方法:
async beforeMount() {
await this.updateSrc(); },
- 最后,对于
watch
属性,创建两个属性,avatarObject
和name
。对于avatarObject
属性,将其定义为一个具有两个属性handler
和deep
的 Javascript 对象。在deep
属性中,将其定义为true
,在handler
属性上,将其定义为调用updateSrc
方法的函数。然后在name
属性上,创建一个handler
属性,定义为调用updateSrc
方法的函数:
watch: {
avatarObject: {
async handler() {
await this.updateSrc();
},
deep: true,
},
name: {
async handler() {
await this.updateSrc();
},
}, },
单文件组件部分
在这里,我们将创建AvatarDisplay
组件的<template>
部分:
- 在
<template>
部分,创建一个component
元素。创建两个动态属性,src
和is
。现在,src
将绑定到数据src
,而is
属性将绑定到componentIs
计算属性。最后,创建一个spinner-color
属性,并将其定义为'primary'
。
完成所有步骤后,您的最终代码应该像这样:
<template>
<component
:src="src"
:is="componentIs"
spinner-color="primary"
/> </template>
它是如何工作的...
在这个示例中,我们学习了如何通过包装 Quasar Framework 的组件并在其上添加自定义逻辑来为我们的应用程序创建自定义组件。
这种技术允许开发独特的组件,可以在应用程序中重复使用,而无需重写逻辑使其正常工作。
对于Usernameinput
和Nameinput
,我们在QInput
组件周围创建了一个包装器,添加了验证规则和文本,以便更轻松地开发和重用组件,而无需添加更多逻辑。
在PasswordInput
组件中,我们添加了控制密码可见性的逻辑,该逻辑会更改输入的类型,并自定义了QInput
组件,以便有一个特殊按钮来触发可见性控制。
对于EmailInput
,我们需要基于正则表达式创建自定义验证规则,检查输入的电子邮件是否是有效的电子邮件,并防止用户意外输入无效的电子邮件。
最后,在AvatarInput
中,使用QFile
组件,我们创建了一个自定义输入,当浏览器读取文件并将文件上传到 AWS Amplify Storage 时,自动上传文件,并在文件上传后将文件 URL 返回给应用程序。
另请参阅
-
在quasar.dev/vue-components/input
找到有关 Quasar 输入组件的更多信息。
-
在quasar.dev/vue-components/file-picker
找到有关 Quasar 文件选择器组件的更多信息。
创建应用程序布局
在我们的应用程序中,我们将使用一个基于布局组件的父路由的vue-router
结构,以及我们正在尝试访问的页面的最终路由。
这种模式改进了我们应用程序的开发,因为我们可以在vue-router
上创建父子责任划分。
在本教程中,我们将学习如何创建自定义布局,将我们的页面包装在vue-router
的父子结构中。
准备工作
本教程的先决条件如下:
-
最后的教程项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始我们的应用程序自定义布局,我们将继续使用在为应用程序创建自定义输入中创建的项目。
如何做...
准备好我们的组件后,我们可以开始创建用于用户登录或注册到聊天应用程序或编辑其信息的布局,以及用于聊天消息页面的聊天布局。
创建基本布局
在我们的应用程序中,我们将使用一种基本布局的技术。它将成为应用程序所有内容的包装器。此布局将应用在布局执行中没有自定义更改的地方。
单文件组件<script>部分
在这部分,我们将创建基本布局的<script>部分:
-
在layouts
文件夹中创建一个名为Base.vue
的新文件。
-
使用 JavaScript 对象创建一个export default
实例,其中name
属性定义为'BaseLayout'
:
<script> export default {
name: 'BaseLayout', }; </script>
单文件组件部分
在这里,我们将创建基本布局的部分:
- 创建一个
QLayout
组件,其中view
属性定义为"hHh Lpr lff"
:
<q-layout view="hHh Lpr lff"> </q-layout>
- 在
QLayout
组件内部,我们需要添加一个带有elevated
属性的QHeader
组件:
<q-header elevated> </q-header>
- 在
QHeader
组件中,我们将添加一个QToolbar
组件,其中包含一个QToolbarTitle
组件作为子元素,以文本作为插槽占位符:
<q-toolbar>
<q-toolbar-title>
Chat App
</q-toolbar-title> </q-toolbar>
- 在
QHeader
组件之后,创建一个带有RouterView
组件的QPageContainer
组件作为直接子元素:
<q-page-container>
<router-view /> </q-page-container>
创建聊天布局
对于我们应用程序的经过身份验证的页面,我们将使用不同的页面布局,其中将有按钮供用户注销、管理其用户并浏览应用程序。
单文件组件<script>部分
让我们创建聊天布局的<script>部分:
-
在layouts
文件夹中创建一个名为Chat.vue
的新文件。
-
从src/driver/auth.js
中导入signOut
函数:
import {signOut,} from 'src/driver/auth';
- 创建一个
export default
实例,包括一个 JavaScript 对象,其中包括两个属性:一个名为name
的属性,定义为'ChatLayout'
,另一个名为methods
的属性:
export default {
name: 'ChatLayout',
methods: { }, };
- 在
methods
属性中,添加一个名为logOff
的新异步函数;在这个函数中,我们将执行signOut
函数,并在其后重新加载浏览器:
async logOff() {
await signOut();
window.location.reload(); }
单文件组件部分
在这里,我们将创建聊天布局的<template>
部分:
- 创建一个带有
view
属性定义为"hHh Lpr lff"
的QLayout
组件:
<q-layout view="hHh Lpr lff"> </q-layout>
- 在
QLayout
组件内部,我们需要添加一个带有elevated
属性的QHeader
组件:
<q-header elevated> </q-header>
- 对于
QHeader
组件,我们将添加一个QToolbar
组件,其中包含一个QToolbarTitle
组件作为子元素,文本作为插槽占位符:
<q-toolbar>
<q-toolbar-title>
Chat App
</q-toolbar-title> </q-toolbar>
- 对于
QToolbar
组件,在QToolbarTitle
组件之前,我们将添加一个带有dense
、flat
和round
属性定义为true
的QBtn
组件。在icon
属性中,我们将添加一个三元表达式,验证$route.meta.goBack
是否存在,以显示back图标或person图标。最后,对于to
属性,我们将做同样的操作,但值将是$route.meta.goBack
或一个具有name
属性为Edit
的 JavaScript 对象。
<q-btn
dense
flat
round
replace
:icon="$route.meta.goBack ? 'keyboard_arrow_left' : 'person'"
:to="$route.meta.goBack ? $route.meta.goBack : {name: 'Edit'}" />
- 在
QToolbarTitle
组件之后,我们将添加一个带有dense
、flat
和round
属性的QBtn
组件,这些属性被定义为true
。对于icon
属性,我们将定义为exit_to_app
,对于@click
指令,我们将传递logOff
方法:
<q-btn
dense
flat round icon="exit_to_app"
@click="logOff" />
- 在
QHeader
组件之后,创建一个带有RouterView
组件作为直接子元素的QPageContainer
组件:
<q-page-container>
<router-view /> </q-page-container>
工作原理...
在这个示例中,我们学习了如何创建我们将在应用程序中使用的布局。这些布局是我们应用程序页面的包装器,使得在需要时可以轻松添加常见项目,如菜单、头部项目和页脚项目,而无需编辑每个页面文件。
对于创建的两种布局,我们使用了常见的QLayout
、QHeader
和QToolbarTitle
组件。这些组件创建了页面的结构,包括布局容器、头部容器和自定义头部工具栏。
最后,对于聊天布局,我们在页眉菜单中添加了两个按钮:一个按钮可以是返回按钮或菜单,具体取决于路由中是否存在该参数;另一个是注销按钮,用户可以用它来从应用程序中注销。
另请参阅
-
关于 Quasar Framework QLayout
组件的更多信息,请访问quasar.dev/layout/layout
。
-
关于 Quasar Framework QHeader
组件的更多信息,请访问quasar.dev/layout/header-and-footer
。
-
关于 Quasar Framework QPage
组件的更多信息,请访问quasar.dev/layout/page
。
-
关于 Quasar Framework QBtn
组件的更多信息,请访问quasar.dev/vue-components/button
。
第五章:创建用户 Vuex 模块、页面和路由
现在,是时候给应用程序一个可识别的面孔了。在本章中,我们将开始开发用户与应用程序之间的交互。
我们将利用我们从前面章节中收集的知识,通过使用自定义业务规则、Vuex 数据存储、特殊应用程序布局和用户可以交互的页面,将这个应用程序变得生动起来。
在本章中,我们将学习如何创建用户 Vuex 模块,以便我们可以存储和管理与用户、用户注册、登录、验证和编辑页面相关的一切。
在本章中,我们将涵盖以下配方:
-
在您的应用程序中创建用户 Vuex 模块
-
为您的应用程序创建用户页面和路由
让我们开始吧!
技术要求
在本章中,我们将使用Node.js、AWS Amplify和Quasar Framework。
注意,Windows 用户! 您需要安装一个名为windows-build-tools
的npm
包,以便能够安装所需的软件包。要做到这一点,以管理员身份打开 PowerShell 并执行> 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
在您的应用程序中创建用户 Vuex 模块
现在,是时候开始在我们的应用状态管理器或 Vuex 中存储数据了。在应用上下文中,存储的所有数据都保存在命名空间中。
在这个配方中,我们将学习如何创建用户 Vuex 模块。利用我们从上一章中获得的知识,然后创建动作来创建新用户、更新他们的数据、验证用户、在 Amplify 上登录用户,并列出应用程序中的所有用户。
准备工作
本配方的先决条件是 Node.js 12+。
本章所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始我们的用户 Vuex 存储模块,我们将继续使用我们在第四章中创建的项目,即创建自定义应用程序组件和布局。
这个食谱将使用 GraphQL 查询和突变完成,以及它们的驱动程序,这些驱动程序是在第三章的创建您的第一个 GraphQL API和为您的应用程序创建 AWS Amplify 驱动程序食谱中编写的。
如何做...
我们将把用户的 Vuex 模块的创建分为五个部分:创建state,mutations,getters和actions,然后将模块添加到 Vuex 中。
创建用户的 Vuex 状态
要在 Vuex 模块上存储数据,我们需要一个将为我们存储数据的状态。按照以下步骤创建用户状态:
-
在store
文件夹中,创建一个名为user
的新文件夹。在内部,创建一个名为state.js
的新文件并打开它。
-
创建一个名为createState
的新函数,它返回一个 JavaScript 对象,提供id
,username
,email
,name
,avatar
,password
,loading
,validated
和error
属性。id
,username
,email
,name
和password
属性将被定义为空字符串,而loading
和validated
属性将被定义为false
。error
将被定义为undefined
,avatar
是一个具有三个属性的 JavaScript 对象-key
,bucket
和region
:
export function createState() {
return {
id: '',
username: '',
email: '',
name: '',
avatar: {
key: '',
bucket: '',
region: '',
},
password: '',
loading: false,
validated: false,
error: undefined,
}; }
- 最后,为了将状态导出为单例并将其作为 JavaScript 对象可用,我们需要
export default
执行createState
函数:
export default createState();
创建用户的 Vuex mutations
要在状态上保存任何数据,Vuex 需要一个 mutation。按照以下步骤创建将管理此模块的 mutations 的用户 mutation:
-
在store/user
文件夹内创建一个名为types.js
的新文件并打开它。
-
在文件中,导出一个默认的 JavaScript 对象,提供CREATE_USER
,SET_USER_DATA
,CLEAR_USER
,USER_VALIDATED
,LOADING
和ERROR
属性。值与属性相同,但格式为字符串。
export default {
CREATE_USER: 'CREATE_USER', SET_USER_DATA: 'SET_USER_DATA',
CLEAR_USER: 'CLEAR_USER',
USER_VALIDATED: 'USER_VALIDATED',
LOADING: 'LOADING',
ERROR: 'ERROR', };
-
在store/user
文件夹内创建一个名为mutations.js
的新文件并打开它。
-
导入新创建的types.js
文件和state.js
中的createState
JavaScript 对象:
import MT from './types'; import { createState } from './state';
- 创建一个名为
setLoading
的新函数,状态作为第一个参数。在内部,我们将设置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; }
- 创建一个名为
createUser
的新函数,以state
作为第一个参数,并以 JavaScript 对象作为第二个参数。这个 JavaScript 对象将提供id
、email
、password
、name
和username
属性。所有属性都将是空字符串。在函数内部,我们将定义state
属性为函数参数中收到的属性:
function createUser(state, {
id = '',
email = '',
password = '',
name = '',
username = '', }) {
state.username = username;
state.email = email;
state.name = name;
state.id = id;
state.password = window.btoa(password);
state.loading = false; }
- 创建一个名为
validateUser
的新函数,以state
作为第一个参数。在其中,我们将将state.validated
属性设置为true
,删除state.password
属性,并将state.loading
属性设置为false
:
function validateUser(state) {
state.validated = true;
delete state.password;
state.loading = false; }
- 创建一个名为
setUserData
的新函数,以state
作为第一个参数,并以 JavaScript 对象作为第二个参数。这个对象将提供id
、email
、password
、name
和username
属性。它们都将是空字符串。avatar
是一个具有三个属性的 JavaScript 对象:key
、bucket
和region
。在函数内部,我们将定义state
属性为函数参数中收到的属性:
function setUserData(state, {
id = '',
email = '',
name = '',
username = '',
avatar = {
key: '',
bucket: '',
region: '',
}, }) {
state.id = id;
state.email = email;
state.name = name;
state.username = username;
state.avatar = avatar || {
key: '',
bucket: '',
region: '',
}; delete state.password; state.validated = true;
state.loading = false; }
- 创建一个名为
clearUser
的新函数,以state
作为第一个参数。然后,在其中的函数中,我们将从createState
函数获取一个新的干净的state
,并迭代当前的state
,将state
属性的值重新定义为默认值:
function clearUser(state) {
const newState = createState(); Object.keys(state).forEach((key) => {
state[key] = newState[key];
}); }
- 最后,导出一个默认的 JavaScript 对象,其中键是导入的变异类型,值是对应于每种类型的函数:
-
将MT.LOADING
设置为setLoading
-
将MT.ERROR
设置为setError
-
将MT.CREATE_USER
设置为createUser
-
将MT.USER_VALIDATED
设置为validateUser
-
将MT.SET_USER_DATA
设置为setUserData
-
将MT.CLEAR_USER
设置为clearUser
export default {
[MT.LOADING]: setLoading,
[MT.ERROR]: setError,
[MT.CREATE_USER]: createUser,
[MT.USER_VALIDATED]: validateUser,
[MT.SET_USER_DATA]: setUserData,
[MT.CLEAR_USER]: clearUser, };
创建用户 Vuex getter
要访问存储在状态中的数据,我们需要创建一些“getter”。按照以下步骤为用户模块创建“getter”:
在getter
函数中,该函数将始终接收到 Vuexstore
的当前state
作为第一个参数。
-
在store/user
文件夹内创建一个名为getters.js
的新文件。
-
创建一个名为getUserId
的新函数,返回state.id
:
const getUserId = (state) => state.id;
- 创建一个名为
getUserEmail
的新函数,返回state.email
:
const getUserEmail = (state) => state.email;
- 创建一个名为
getUserUsername
的新函数,返回state.username
:
const getUserUsername = (state) => state.username;
- 创建一个名为
getUserAvatar
的新函数,返回state.avatar
:
const getUserAvatar = (state) => state.avatar;
- 创建一个名为
getUser
的新函数,返回一个提供id
、name
、username
、avatar
和email
属性的 JavaScript 对象。这些属性的值将对应于state
:
const getUser = (state) => ({
id: state.id,
name: state.name,
username: state.username,
avatar: state.avatar,
email: state.email, });
- 创建一个名为
isLoading
的新函数,返回state.loading
:
const isLoading = (state) => state.loading;
- 创建一个名为
hasError
的新函数,返回state.error
:
const hasError = (state) => state.error;
- 最后,导出一个带有创建的函数(
getUserId
、getUserEmail
、getUserUsername
、getUserAvatar
、getUser
、isLoading
和hasError
)作为属性的default
JavaScript 对象:
export default {
getUserId,
getUserEmail,
getUserUsername,
getUserAvatar,
getUser,
isLoading,
hasError, };
创建用户 Vuex 操作
按照以下步骤创建用户 Vuex 操作:
-
在store/user
文件夹内创建一个名为actions.js
的文件并打开它。
-
首先,我们需要导入这里将要使用的函数、枚举和类。
-
从aws-amplify
npm 包中导入graphqlOperation
。
-
从 GraphQL 查询中导入getUser
和listUsers
。
-
从 GraphQL 变异中导入createUser
和updateUser
。
-
从driver/auth.js
中导入signUp
、validateUser
、signIn
、getCurrentAuthUser
和changePassword
函数。
-
从driver/appsync
导入AuthAPI
。
-
从./types.js
导入 Vuex 变异类型:
import { graphqlOperation } from 'aws-amplify';
import { getUser, listUsers } from 'src/graphql/queries';
import { createUser, updateUser } from 'src/graphql/mutations';
import { AuthAPI } from 'src/driver/appsync';
import {
signUp,
validateUser,
signIn,
getCurrentAuthUser,
changePassword,
} from 'src/driver/auth';
import MT from './types';
- 创建一个名为
initialLogin
的新异步函数。此函数将接收一个 JavaScript 对象作为第一个参数。这将提供一个commit
属性。在这个函数中,我们将获取当前认证的用户,从 GraphQL API 获取他们的数据,并将用户数据提交到 Vuex 存储中:
async function initialLogin({ commit }) {
try {
commit(MT.LOADING); const AuthUser = await getCurrentAuthUser(); const { data } = await AuthAPI.graphql(graphqlOperation(getUser, {
id: AuthUser.username,
})); commit(MT.SET_USER_DATA, data.getUser); return Promise.resolve(AuthUser);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
- 创建一个名为
signUpNewUser
的新异步函数。此函数将接收一个带有commit
属性的 JavaScript 对象作为第一个参数。第二个参数也是一个 JavaScript 对象,但具有email
、name
和password
属性。在这个函数中,我们将执行auth.js
驱动器中的signUp
函数来注册并在 AWS Cognito 用户池中创建用户,然后将用户数据提交到 Vuex 存储中:
async function signUpNewUser({ commit }, {
email = '',
name = '',
username = '',
password = '', }) {
try {
commit(MT.LOADING); const userData = await signUp(email, password); commit(MT.CREATE_USER, {
id: userData.userSub,
email,
password,
name,
username,
}); return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
- 创建一个名为
createNewUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和state
属性。对于第二个参数,函数将接收一个code
字符串。在这个函数中,我们将从state
中获取用户数据,并执行auth.js
驱动器中的validateUser
函数,以检查用户是否是 AWS Cognito 用户池中的有效用户。然后,我们将执行auth.js
中的signIn
函数,将email
和password
作为参数传递,需要将password
转换为加密的 base64 字符串,然后发送到函数。之后,我们将获取经过身份验证的用户数据,并将其发送到 GraphQL API 以创建一个新用户:
async function createNewUser({ commit, state }, code) {
try {
commit(MT.LOADING);
const {
email,
name,
username,
password,
} = state;
const userData = await validateUser(email, code); await signIn(`${email}`, `${window.atob(password)}`); const { id } = await getCurrentAuthUser(); await AuthAPI.graphql(graphqlOperation(
createUser,
{
input: {
id,
username,
email,
name,
},
},
)); commit(MT.USER_VALIDATED); return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
- 创建一个名为
signInUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和dispatch
属性。第二个参数也是一个 JavaScript 对象,包含email
和password
属性。在这个函数内部,我们将执行auth.js
驱动器中的signIn
函数,将email
和password
作为参数传递,然后触发initialLogin
Vuex 动作:
async function signInUser({ commit, dispatch }, { email = '', password = '' }) {
try {
commit(MT.LOADING); await signIn(`${email}`, `${password}`); await dispatch('initialLogin'); return Promise.resolve(true);
} catch (err) {
commit(MT.ERROR);
return Promise.reject(err);
} }
- 创建一个名为
editUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和state
属性。第二个参数也是一个 JavaScript 对象,包含username
、name
、avatar
、password
和newPassword
属性。在这个函数内部,我们将合并state
的值和作为参数接收到的新值。然后将它们发送到 GraphQL API 以更新用户信息。然后,我们将检查是否password
和newPasssword
属性都填写了。如果是,我们将执行auth.js
驱动器中的changePassword
函数,以在 AWS Cognito 用户池中更改用户的密码:
async function editUser({ commit, state }, {
username = '',
name = '',
avatar = {
key: '',
bucket: '',
region: '',
},
password = '',
newPassword = '', }) {
try {
commit(MT.LOADING); const updateObject = {
...{
name: state.name,
username: state.username,
avatar: state.avatar,
},
...{
name,
username,
avatar,
},
}; const { data } = await AuthAPI.graphql(graphqlOperation(updateUser,
{ input: { id: state.id, ...updateObject } })); if (password && newPassword) {
await changePassword(password, newPassword);
} commit(MT.SET_USER_DATA, data.updateUser); return Promise.resolve(data.updateUser);
} catch (err) {
return Promise.reject(err);
} }
- 创建一个名为
listAllUsers
的新异步函数。这个函数将获取数据库中的所有用户并返回一个列表:
async function listAllUsers() {
try {
const {
data: {
listUsers: {
items: usersList,
},
},
} = await AuthAPI.graphql(graphqlOperation(
listUsers,
)); return Promise.resolve(usersList);
} catch (e) {
return Promise.reject(e);
} }
- 最后,我们将导出所有默认创建的函数:
export default {
initialLogin,
signUpNewUser,
createNewUser,
signInUser,
editUser,
listAllUsers, };
将用户模块添加到 Vuex
按照以下步骤将创建的用户模块导入到 Vuex 状态中:
-
在store/user
文件夹内创建一个名为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
文件。
-
在store/user
文件夹中导入新创建的index.js
:
import Vue from 'vue'; import Vuex from 'vuex'; import user from './user';
- 在新的 Vuex 类实例化中,我们需要添加一个名为
modules
的新属性,并将其定义为 JavaScript 对象。然后,我们需要添加一个新的user
属性-这将自动用作值,因为它与上一步中导入的 User 模块具有相同的名称:
export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
user,
},
strict: process.env.DEV,
}); return Store; }
工作原理...
当声明你的 Vuex 存储时,你需要创建三个主要属性:state
、mutations
和actions
。这些属性作为一个单一的结构,通过注入的$store
原型或导出的store
变量绑定到 Vue 应用程序。
state
是一个集中的对象,保存着你的信息,并使其可以被mutations
、actions
或components
使用。改变state
总是需要通过mutation
执行同步函数。
mutation
是一个同步函数,可以改变state
并被追踪。这意味着在开发时,你可以在 Vuex 存储中时间旅行通过所有执行的mutations
。
action
是一个异步函数,可以用来保存业务逻辑、API 调用、分发其他actions
和执行mutations
。这些函数是当你需要对 Vuex 存储进行更改时的常见入口点。
Vuex 存储的简单表示可以在以下图表中看到:
在这个示例中,我们创建了 User Vuex 模块。该模块包括了所有的业务逻辑,将帮助我们在应用程序中管理用户,从创建新用户到更新用户。
当我们查看 Vuex actions 时,我们使用 AppSync API 客户端来获取数据并将其发送到我们的 GraphQL API。我们使用了由 Amplify CLI 创建的查询和 mutations。为了能够与 GraphQL API 通信,以便我们可以更新用户,我们从为应用程序创建 AWS Amplify driver配方中获取了我们在 Auth Driver 中使用的数据,第三章,设置我们的聊天应用程序 - AWS Amplify 环境和 GraphQL。
这些 API 请求由 Vuex mutations 操纵,并存储在 Vuex 状态中,我们可以通过 Vuex getter 访问。
另请参阅
-
您可以在aws-amplify.github.io/docs/js/api#amplify-graphql-client
找到有关 Amplify 的 AppSync GraphQL 客户端的更多信息。
-
您可以在https://vuex.vuejs.org/找到有关 Vuex 的更多信息。
-
您可以在vuex.vuejs.org/guide/modules.html
找到有关 Vuex 模块的更多信息
为您的应用程序创建用户页面和路由
在使用 Vue 应用程序时,您需要一种管理用户位置的方法。您可以使用动态组件来处理这个问题,但最好的方法是通过路由管理。
在这个食谱中,我们将学习如何创建我们的应用程序页面,其中包含每个路由所需的业务规则。然后,我们将使用路由管理来处理一切。
准备工作
此食谱的先决条件如下:
-
我们在上一个食谱中创建的项目
-
Node.js 12+
此食谱所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始我们的用户页面和路由,我们将继续使用在在应用程序上创建用户 Vuex 模块食谱中创建的项目。
如何做...
在这个食谱中,我们将为我们的应用程序创建所有我们需要的用户页面:登录页面、注册页面和用户编辑页面。
将对话框插件添加到 Quasar
使用 Quasar 对话框插件,我们需要将其添加到配置文件中。
打开项目根文件夹内的quasar.conf.js
文件,并找到framework
属性。然后,在plugins
属性中,将'Dialog'
字符串添加到数组中,以便 Quasar 在启动应用程序时加载Dialog
插件:
framework: {
...
plugins: [
'Dialog',
],
...
},
创建用户登录页面
对于用户登录页面,我们将使用之前创建的两个组件:PasswordInput
和EmailInput
。
单文件组件<script>
部分
现在是创建用户登录页面的<script>
部分的时候了:
-
在src/pages
文件夹中,打开Index.vue
文件。
-
从vuex
包中导入mapActions
和mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
- 创建一个具有五个属性的
export default
JavaScript 对象;即name
(定义为'Index'
),components
,data
,computed
和methods
:
export default {
name: 'Index',
components: {
},
data: () => ({ }),
computed: { },
methods: { }, };
- 在
components
属性中,添加两个名为PasswordInput
和EmailInput
的新属性。将PasswordInput
定义为一个匿名函数,其返回值为import('components/PasswordInput')
,并将EmailInput
定义为一个匿名函数,其返回值为import('components/EmailInput')
:
components: {
PasswordInput: () => import('components/PasswordInput'),
EmailInput: () => import('components/EmailInput'), },
- 在
data
属性中,我们将返回一个提供两个属性email
和password
的 JavaScript 对象,它们都将是空字符串:
data: () => ({
email: '',
password: '', }),
- 在
computed
属性中,我们将解构mapGetters
函数,将我们想要的模块的命名空间作为第一个参数(在本例中为'user'
)。我们将把我们想要导入的getters
数组(在本例中为isLoading
)作为第二个参数传递进去:
computed: {
...mapGetters('user', [
'isLoading',
'getUserId',
]), },
- 在
beforeMount
生命周期钩子上,我们将添加一个if
语句,检查getUserId
是否为真,并将用户重定向到Contacts
路由。
async beforeMount() {
if (this.getUserId) {
await this.$router.replace({ name: 'Contacts' });
} },
- 最后,在
methods
属性中,我们将解构mapActions
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递进去。对于第二个参数,我们将使用一个包含我们想要导入的actions
的数组(在这种情况下,这是signInUser
)。接下来,我们需要添加异步的onSubmit
方法,该方法将调度signInUser
并将用户发送到Contacts
路由,以及createAccount
方法,该方法将用户发送到SignUp
路由:
methods: {
...mapActions('user', [
'signInUser',
]),
async onSubmit() {
try {
await this.signInUser({
email: this.email,
password: this.password,
});
await this.$router.push({ name: 'Contacts' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
createAccount() {
this.$router.push({ name: 'SignUp' });
}, },
单文件组件<template>
部分
现在,我们需要添加<template>
部分来完成我们的页面:
- 创建一个名为
QPage
的组件,其class
属性定义为"bg-grey-1 flex flex-center"
:
<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
- 在
QPage
组件内部,创建一个QCard
组件,其style
属性定义为"width: 350px"
:
<q-card style="width: 350px"> </q-card>
- 在
QCard
组件内部,创建一个带有class
属性定义为no-margin
的QCardSection
和一个h6
子组件:
<q-card-section>
<h6 class="no-margin">Chat Application</h6> </q-card-section>
- 现在,创建一个
QCardSection
,其中包含一个QForm
子组件,其 class 属性定义为q-gutter-md
。在QForm
组件内部,创建一个EmailInput
组件,其v-model
指令绑定到data.email
,以及一个PasswordInput
组件,其v-model
指令绑定到data.password
属性:
<q-card-section>
<q-form
class="q-gutter-md"
>
<email-input
v-model.trim="email"
/>
<password-input
v-model.trim="password"
/>
</q-form> </q-card-section>
- 然后,创建一个
QCardActions
组件,其中定义了一个 align
属性为 right
。在内部,添加一个 QBtn
,label
属性设置为 "Create new Account"
,color
设置为 primary
,class
设置为 q-ml-sm
,flat
设置为 true
,并且 @click
事件监听器绑定到 createAccount
方法。接下来,创建另一个 QBtn
组件,label
属性设置为 "Login"
,type
设置为 "submit"
,color
设置为 primary
,并且 @click
事件监听器绑定到 onSubmit
方法:
<q-card-actions align="right">
<q-btn
label="Create new account"
color="primary"
flat
class="q-ml-sm"
@click="createAccount"
/>
<q-btn
label="Login"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
- 最后,创建一个
QInnerLoading
组件,将 :showing
属性绑定到 computed.isLoading
。这将需要有一个 QSpinner
子组件,提供 size
属性。将其设置为 50px
,color
设置为 primary
:
<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
要运行服务器并查看您的进度,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar dev
请记住始终执行命令 npm run lint --fix
,以自动修复任何代码 lint 错误。
这是页面预览的样子:
创建用户注册页面
对于用户注册页面,我们将使用已经创建的四个组件:NameInput
,UsernameInput
,PasswordInput
和 EmailInput
。
单文件组件 <script> 部分
在这里,我们将创建用户注册页面的 <script>
部分:
-
在 **src/pages
** 文件夹中,创建一个名为 SignUp.vue
的新文件并打开它。
-
从 vuex
包中导入 mapActions
和 mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
- 创建一个
export default
JavaScript 对象,提供五个属性:name
(定义为 'SignUp'
),components
,data
,computed
和 methods
:
export default {
name: 'SignUp',
components: {}, data: () => ({ }),
computed: { },
methods: { }, };
- 在
components
属性中,添加四个新属性:NameInput
,UsernameInput
,PasswordInput
和 EmailInput
。像这样定义它们:
-
NameInput
作为一个匿名函数,返回值为 import('components/NameInput')
-
UsernameInput
作为一个匿名函数,返回值为 import('components/UsernameInput')
-
PasswordInput
作为一个匿名函数,返回值为 import('components/PasswordInput')
-
EmailInput
作为一个匿名函数,返回值为 import('components/EmailInput')
这可以在以下代码中看到:
components: {
PasswordInput: () => import('components/PasswordInput'),
EmailInput: () => import('components/EmailInput'),
UsernameInput: () => import('components/UsernameInput'),
NameInput: () => import('components/NameInput'), },
- 在
data
属性中,我们将返回一个提供四个属性的 JavaScript 对象 - name
,username
,email
和 password
- 其中所有属性都将是空字符串:
data: () => ({
name: '',
username: '',
email: '',
password: '', }),
- 在
computed
属性中,我们将解构mapGetters
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递。对于第二个参数,我们将使用一个要导入的getters
数组,这种情况下是isLoading
:
computed: {
...mapGetters('user', [
'isLoading',
]), },
- 最后,对于
methods
属性,首先,我们将解构mapActions
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递。对于第二个参数,我们将传递一个要导入的actions
数组,这种情况下是signUpNewUser
。接下来,我们需要添加异步的onSubmit
方法,它将分发signUpNewUser
,然后将用户发送到Validate
路由,以及onReset
方法,它将清除数据:
methods: {
...mapActions('user', [
'signUpNewUser',
]),
async onSubmit() {
try {
await this.signUpNewUser({
name: this.name,
username: this.username,
email: this.email,
password: this.password,
});
await this.$router.replace({ name: 'Validate' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
onReset() {
this.email = '';
this.password = '';
}, },
单文件组件<template>
部分
要完成页面,我们需要添加<template>
部分:
- 创建一个
QPage
组件,class
属性定义为"bg-grey-1 flex flex-center"
:
<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
- 在
QPage
组件内部,创建一个QCard
组件,style
属性定义为"width: 350px"
:
<q-card style="width: 350px"> </q-card>
- 在
QCard
组件内部,创建一个QCardSection
,其中包含一个h6
子组件,其中class
属性定义为no-margin
:
<q-card-section>
<h6 class="no-margin">Create a new Account</h6> </q-card-section>
- 之后,创建一个
QCardSection
,其中包含一个QForm
子组件,其中class
属性定义为q-gutter-md
。在QForm
组件内部,创建一个NameInput
组件,v-model
指令绑定到data.name
,一个UsernameInput
组件,v-model
指令绑定到data.username
,一个EmailInput
组件,v-model
指令绑定到data.email
,以及一个PasswordInput
组件,v-model
指令绑定到data.password
属性:
<q-card-section>
<q-form class="q-gutter-md"
>
<name-input
v-model.trim="name"
/>
<username-input
v-model.trim="username"
/>
<email-input
v-model.trim="email"
/>
<password-input
v-model.trim="password"
/>
</q-form> </q-card-section>
- 现在,创建一个
QCardActions
组件,align
属性设置为right
。在内部,添加一个QBtn
,label
属性设置为"Reset"
,color
设置为primary
,class
设置为q-ml-sm
,flat
设置为true
,@click
事件监听器绑定到onReset
方法。然后,创建另一个QBtn
组件,label
属性设置为"Create"
,type
设置为"submit"
,color
设置为primary
,@click
事件监听器绑定到onSubmit
方法:
<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
@click="onReset"
/>
<q-btn
label="Create"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
- 最后,创建一个
QInnerLoading
组件,:showing
属性绑定到computed.isLoading
。这将需要一个QSpinner
子组件。size
属性需要设置为50px
,color
需要设置为primary
:
<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
要运行服务器并查看您的进度,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar dev
记得始终执行命令npm run lint --fix
,自动修复任何代码 lint 错误。
这是页面的预览:
创建用户验证页面。
用户创建了一个账户后,AWS Amplify 将发送一封带有验证 pin 码的电子邮件,我们需要将其发送回来进行验证。这个页面将是验证页面。
单文件组件<script>
部分
按照以下步骤创建用户验证页面的<script>
部分:
-
在src/pages
文件夹内,创建一个名为Validate.vue
的新文件并打开它。
-
从vuex
包中导入mapActions
和mapGetters
函数,以及从src/driver/auth
导入resendValidationCode
:
import { mapActions, mapGetters } from 'vuex';
import { resendValidationCode } from 'src/driver/auth';
- 创建一个
export default
的 JavaScript 对象,提供四个属性:name
(定义为'Validate'
)、data
、computed
和methods
:
export default {
name: 'Validate', data: () => ({ }),
computed: { },
methods: { }, };
- 在
data
属性内,我们将返回一个具有空字符串code
属性的 JavaScript 对象:
data: () => ({
code: '', }),
- 在
computed
属性内,我们将解构mapGetters
函数,传递我们想要的模块的命名空间作为第一个参数,例如'user'
。对于第二个参数,我们将传递一个要导入的getters
数组,例如isLoading
和getUserEmail
:
computed: {
...mapGetters('user', [
'isLoading',
'getUserEmail',
]), },
- 最后,在
methods
属性中,我们将解构mapActions
函数,传递我们想要的模块的命名空间作为第一个参数,例如'user'
。对于第二个参数,我们将传递一个要导入的actions
数组,例如createNewUser
。接下来,我们需要添加异步的onSubmit
方法,它将分发createNewUser
并将用户发送到Index
路由;resendCode
方法,它将重新发送用户另一个验证代码;以及onReset
方法,它将重置数据:
methods: {
...mapActions('user', [
'createNewUser',
]),
async onSubmit() {
try {
await this.createNewUser(this.code);
await this.$router.replace({ name: 'Index' });
} catch (e) {
console.error(e);
this.$q.dialog({
message: e.message,
});
}
},
async resendCode() {
await resendValidationCode(this.getUserEmail);
},
onReset() {
this.code = '';
}, },
单文件组件<template>
部分
按照以下步骤创建用户验证页面的<template>
部分:
- 创建一个
QPage
组件,定义class
属性为"bg-grey-1 flex flex-center"
:
<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
- 在
QPage
组件内部,创建一个QCard
组件,定义style
属性为"width: 350px"
:
<q-card style="width: 350px"> </q-card>
- 在
QCard
组件内,创建一个带有 h6
子组件和定义为 no-margin
的 class
属性的 QCardSection
。然后,创建一个兄弟元素,其 class
属性定义为 text-subtitle2
:
<q-card-section>
<h6 class="no-margin">Validate new account</h6>
<div class="text-subtitle2">{{ getUserEmail }}</div> </q-card-section>
- 创建一个带有两个子组件的
QCardSection
。这些将是 HTML 元素 P
:
<q-card-section>
<p>A validation code were sent to you E-mail.</p>
<p>Please enter it to validate your new account.</p> </q-card-section>
- 之后,创建一个带有
QForm
子组件和定义为 q-gutter-md
的 class
属性的 QCardSection
。在 QForm
组件内,添加 QInput
组件作为子元素。然后,在 QInput
组件内,将 v-model
指令绑定到 data.code
。在 QInput
的 rules
属性内,将 rules
值定义为一个验证数组,用于检查是否已输入任何代码。启用 lazy-rules
,以便它只在一段时间后进行验证:
<q-card-section>
<q-form
class="q-gutter-md"
>
<q-input
v-model.trim="code"
:rules="[ val => val && val.length > 0
|| 'Please type the validation code']"
outlined
label="Validation Code"
lazy-rules
/>
</q-form> </q-card-section>
- 现在,创建一个带有
align
属性设置为 right
的 QCardActions
组件。在内部,添加一个 label
属性设置为 "Reset"
、color
设置为 primary
、class
设置为 q-ml-sm
、flat
设置为 true
,并且 @click
事件监听器绑定到 onReset
方法的 QBtn
。创建另一个 label
属性设置为 "Re-send code"
、color
设置为 secondary
、class
设置为 q-ml-sm
、flat
设置为 true
,并且 @click
事件监听器绑定到 resendCode
方法的 QBtn
。最后,创建一个带有 label
属性设置为 "Validate"
、type
设置为 "submit"
、color
设置为 primary
,并且 @click
事件监听器绑定到 onSubmit
方法的 QBtn
组件:
<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
/>
<q-btn
flat
label="Re-send code"
color="secondary"
class="q-ml-sm"
@click="resendCode"
/>
<q-btn
label="Validate"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
- 最后,创建一个带有
:showing
属性绑定到 computed.isLoading
的 QInnerLoading
组件。它应该有一个 size
设置为 50px
和 color
设置为 primary
的 QSpinner
子组件:
<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
要运行服务器并查看您的进度,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar dev
记得始终执行命令 npm run lint --fix
,自动修复任何代码 lint 错误。
这是页面的预览:
创建用户编辑页面
对于用户编辑页面,我们将使用我们已经创建的四个组件:NameInput
、UsernameInput
、AvatarInput
和 PasswordInput
。
单文件组件 <script> 部分
按照以下步骤开始开发用户编辑页面的 <script>
部分:
-
在 src/pages
文件夹内,创建一个名为 Edit.vue
的新文件并打开它。
-
从vuex
包中导入mapActions
和mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
- 创建一个
export default
的 JavaScript 对象,提供四个属性:name
(定义为'SignUp'
)、data
、computed
和methods
:
export default {
name: 'EditUser',
components: {}, data: () => ({ }),
created() {},
computed: { },
methods: { }, };
- 在
components
属性中,添加四个名为NameInput
、UsernameInput
、PasswordInput
、AvatarInput
的新属性。设置它们如下:
NameInput
作为一个匿名函数,返回值为import('components/NameInput')
UsernameInput
作为一个匿名函数,返回值为import('components/UsernameInput')
PasswordInput
作为一个匿名函数,返回值为import('components/PasswordInput')
AvatarInput
作为一个匿名函数,返回值为import('components/AvatarInput')
:
components: {
AvatarInput: () => import('/components/AvatarInput'),
PasswordInput: () => import('components/PasswordInput'),
UsernameInput: () => import('components/UsernameInput'),
NameInput: () => import('components/NameInput'), },
- 在
data
属性中,我们将返回一个提供五个属性的 JavaScript 对象:name
、username
、avatar
、email
和password
。所有这些都将是空字符串:
data: () => ({
name: '',
username: '',
avatar: '',
password: '',
newPassword: '', }),
- 在
created
生命周期钩子中,将data.name
定义为getUser.name
,data.username
定义为getUser.username
,以及data.avatar
定义为getUser.avatar
:
created() {
this.name = this.getUser.name;
this.username = this.getUser.username;
this.avatar = this.getUser.avatar; },
- 在
computed
属性中,我们将解构mapGetters
函数,传递我们想要的模块的命名空间作为第一个参数,这里是'user'
。对于第二个参数,我们将传递一个要导入的getters
数组,这种情况下是isLoading
:
computed: {
...mapGetters('user', [
'getUser',
'isLoading',
]), },
- 最后,在
methods
属性中,我们将解构mapActions
函数,传递我们想要的模块的命名空间作为第一个参数,这里是'user'
。对于第二个参数,我们将传递一个要导入的actions
数组,这种情况下是editUser
。接下来,我们需要添加异步的onSubmit
方法,它将调度$refs.avatar.uploadFile()
,然后调度editUser
发送用户到Chat
路由,以及onReset
方法,它将清除数据:
methods: {
...mapActions('user', [
'editUser',
]),
async onSubmit() {
try {
await this.$refs.avatar.uploadFile(); await this.editUser({
name: this.name,
avatar: this.$refs.avatar.s3file,
username: this.username,
password: this.password,
newPassword: this.newPassword,
}); await this.$router.replace({ name: 'Contacts' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
onReset() {
this.name = this.getUser.name;
this.username = this.getUser.username; this.password = '';
this.newPassword = '';
}, },
单文件组件部分
按照以下步骤创建用户编辑页面的<template>
部分:
- 创建一个带有
class
属性定义为"bg-grey-1 flex flex-center"
的QPage
组件:
<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
- 在
QPage
组件内部,创建一个带有style
属性定义为"width: 350px"
的QCard
组件:
<q-card style="width: 350px"> </q-card>
- 在
QCard
组件内部,创建一个带有h6
子组件的QCardSection
,并且class
属性定义为no-margin
:
<q-card-section>
<h6 class="no-margin">Edit user account</h6> </q-card-section>
- 之后,创建一个带有
class
属性定义为q-gutter-md
的QCardSection
,其中包含一个QForm
子组件。在QForm
组件内部,创建一个AvatarInput
组件,其中reference
指令定义为avatar
,v-model
指令绑定到data.avatar
,一个NameInput
组件,其中v-model
指令绑定到data.name
,一个UsernameInput
组件,其中v-model
指令绑定到data.username
,一个EmailInput
组件,其中v-model
指令绑定到data.email
,以及一个PasswordInput
组件,其中v-model
指令绑定到data.password
属性:
<q-card-section>
<q-form
class="q-gutter-md"
>
<avatar-input
v-model="avatar"
ref="avatar"
/>
<name-input
v-model.trim="name"
/>
<username-input
v-model.trim="username"
/>
<q-separator/>
<password-input
v-model.trim="password"
label="Your old password"
/>
<password-input
v-model.trim="newPassword"
label="Your new password"
/>
</q-form> </q-card-section>
- 现在,创建一个带有
align
属性设置为right
的QCardActions
组件。在内部,添加一个label
属性设置为"Reset"
,color
设置为primary
,class
设置为q-ml-sm
,flat
设置为true
,并且@click
事件监听器绑定到onReset
方法的QBtn
。然后,创建另一个QBtn
组件,其中label
属性设置为"Create"
,type
设置为"submit"
,color
设置为primary
,并且@click
事件监听器绑定到onSubmit
方法:
<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
@click="onReset"
/>
<q-btn
label="Update"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
- 最后,创建一个
QInnerLoading
组件,其中:showing
属性绑定到computed.isLoading
。它应该有一个QSpinner
子组件,size
设置为50px
,color
设置为primary
:
<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
要运行服务器并查看您的进度,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar dev
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
这是页面的预览:
创建应用程序路由
现在我们已经创建了用户页面、组件和布局,我们需要将它们绑定在一起,以便用户可以访问。为此,我们需要创建路由并使其可用,以便用户可以在页面之间导航。按照以下步骤进行操作:
-
打开router
文件夹内的routes.js
文件。
-
将routes
常量设置为空数组:
const routes = [];
- 向该数组添加一个具有三个属性
path
、component
和children
的 JavaScript 对象。path
属性是一个字符串,将是一个静态 URL,component
属性是一个匿名函数,将返回一个带有将要渲染的组件的 WebPackimport
函数,children
属性是一个将在path
内渲染的组件数组。每个子组件都是一个具有相同属性的 JavaScript 对象,另外还有一个叫做name
的新属性。
{
path: '/',
component: () => import('layouts/Base.vue'),
children: [
{
path: '',
name: 'Index',
meta: {
authenticated: false,
},
component: () => import('pages/Index.vue'),
},
], },
- 现在,对于
/chat
URL,我们需要在pages
文件夹内创建两个新的占位符页面:Contacts.vue
和Messages.vue
。在这些文件内,创建一个带有以下模板的空组件:
<template>
<div />
</template>
<script>
export default {
name: 'PlaceholderPage',
};
</script>
- 在
message
路由内,我们需要添加两个特殊参数::id
和path
。这些参数将用于在用户之间获取特定的消息。
{
path: '/chat',
component: () => import('layouts/Chat.vue'),
children: [
{
path: 'contacts',
name: 'Contacts',
component: () => import('pages/Contacts.vue'),
},
{
path: 'messages/:id/:name',
name: 'Messages',
meta: {
authenticated: true,
goBack: {
name: 'Contacts',
},
},
component: () => import('pages/Messages.vue'),
},
], },
- 对于
/user
URL,我们将只创建一个子路由,即edit
路由。在这个路由内,我们使用alias
属性,因为vue-router
需要有一个path
为空的子路由进行首次子路由渲染。我们还将在我们的应用程序内有一个/user/edit
路由可用。
{
path: '/user',
component: () => import('layouts/Chat.vue'),
children: [
{
path: '',
alias: 'edit',
name: 'Edit',
meta: {
authenticated: true,
goBack: {
name: 'Contacts',
},
},
component: () => import('pages/Edit.vue'),
},
], },
- 最后,对于创建新用户,我们需要添加
/register
URL,其中包括两个子路由:SignUp
和Validate
。SignUp
路由将是注册 URL 上的主要路由,并且当用户进入此 URL 时将直接调用。Validate
路由只有在用户被重定向到/register/validate
URL 时才会被调用。
{
path: '/register',
component: () => import('layouts/Base.vue'),
children: [
{
path: '',
alias: 'sign-up',
name: 'SignUp',
meta: {
authenticated: false,
},
component: () => import('pages/SignUp.vue'),
},
{
path: 'validate',
name: 'Validate',
meta: {
authenticated: false,
},
component: () => import('pages/Validate.vue'),
},
], },
添加身份验证守卫
为了在用户进入应用程序时验证用户身份令牌,如果令牌有效,或者用户试图访问无权限的路由,我们需要为我们的应用程序创建一个身份验证守卫。
-
在src/boot
文件夹内创建一个名为routeGuard.js
的新文件。
-
创建一个默认的导出异步函数。在这个参数内,添加一个名为app
的 JavaScript 对象属性。在函数内部,创建一个常量,使用app
的对象重构获取store
属性。然后,创建一个try/catch
块。在try
部分,检查'user/getUserId'
是否存在,如果不存在则调度'user/initialLogin'
。最后,在catch
块内,将用户重定向到Index
路由。
export default async ({ app }) => {
const { store } = app; try {
if (!store.getters['user/getUserId']) {
await store.dispatch('user/initialLogin');
}
} catch {
await app.router.replace({ name: 'Index' });
} };
- 最后,打开项目根文件夹内的
quasar.conf.js
文件,并找到boot
属性。将'routerGuard'
项添加到数组中。
boot: [
'amplify',
'axios',
'routeGuard', ],
它是如何工作的...
在本章中,我们开发了微组件,如NameInput
,EmailInput
等,以简化开发宏组件或容器(如页面)的过程。
在这个配方中,我们使用了在上一个配方中开发的组件来创建完整的页面,例如用户登录、用户编辑和用户注册页面。
使用vue-router
来管理使用自定义布局包装页面的父子过程,我们使用了本书先前配方中创建的布局来为我们的应用程序创建路由。我们使它们可用,以便我们可以像正常的 Web 应用程序一样访问应用程序,具有自定义 URL 和路由。
最后,我们在我们的主初始化 Vue 文件中添加了一些身份验证中间件,以便我们可以重定向已经经过身份验证的用户。这意味着当他们第二次进入应用程序时,他们不需要再次进行身份验证。
还有...
现在,您的应用程序已准备好进行用户注册和登录。可以浏览用户注册页面,并从亚马逊收到一封带有验证代码的电子邮件,以便您可以在服务器上验证用户。
要检查您的进程并在本地环境中运行它,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> quasar dev
请记住始终执行命令npm run lint --fix
,以自动修复任何代码 lint 错误。
另请参阅
-
您可以在router.vuejs.org/guide/essentials/nested-routes.html
找到有关vue-router
嵌套路由的更多信息。
-
您可以在router.vuejs.org/guide/advanced/lazy-loading.html
找到有关vue-router
懒加载的更多信息。
-
你可以在quasar.dev/vue-components/inner-loading
找到有关 Quasar 框架的QInnerLoading
组件的更多信息。
第六章:创建 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_MESSAGES
mutation 添加到状态中,并返回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>部分
在这里,我们将创建组件的<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
属性中,首先,我们将从用户模块中解构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();
}
}, },
单文件组件的部分
现在让我们为NewConversation
组件创建<template>
部分:
- 创建一个带有
value
属性定义为value
的QDialog
组件。还创建一个事件监听器input
,定义为$emit
函数,发送带有$event
作为数据的'input'
事件:
<q-dialog
:value="value"
@input="$emit('input', $event)" ></q-dialog>
- 在
QDialog
组件内部,创建一个带有style
属性定义为min-width: 400px; min-height: 100px;
的QCard
组件。在QCard
组件内部,创建两个QCardSection
子组件。在第一个组件中,添加一个class
属性定义为row items-center q-pb-none
:
<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
组件上,添加一个带有class
属性定义为text-h6
的div
,并将内部 HTML 定义为New Conversation
。然后添加一个QSpace
组件。最后,添加一个带有icon
属性定义为close
的QBtn
,并将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
组件内部,创建一个带有avatar
属性定义为true
的QItemSection
组件。然后创建一个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
。在其中添加一个name
属性设置为add_comment
,color
设置为green
的QIcon
组件:
<q-item-section side>
<q-icon name="add_comment" color="green"/> </q-item-section>
- 最后,作为
QList
组件的同级元素,创建一个带有showing
属性定义为pending
的QInnerLoading
组件。在其中添加一个size
属性设置为50px
,color
属性定义为primary
的QSpinner
组件:
<q-inner-loading
:showing="pending">
<q-spinner
size="50px"
color="primary"/> </q-inner-loading>
这是您组件的渲染版本:
创建联系人页面
现在是创建联系人页面的时候了。这个页面将是已认证用户应用程序的初始页面。在这里,用户可以转到用户更新页面,进入并恢复旧对话,或创建新对话。
单文件组件 <script> 部分
在这里,我们将创建单文件组件的<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
混合的数组。在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
组件内部,创建一个带有v-for
指令的QItem
组件,迭代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>
部分之间的相似之处是有意为之,这样用户在创建新对话和查看当前联系人列表时有相同的体验。
混合使用对于使代码更清晰,减少代码重复并使代码更简单重用至关重要。
另请参阅
-
在quasar.dev/vue-components/button
找到有关 Quasar QBtn
组件的更多信息。
-
在quasar.dev/vue-components/dialog
找到有关 Quasar QDialog
组件的更多信息。
-
在quasar.dev/vue-components/inner-loading
找到有关 Quasar QInnerLoading
组件的更多信息。
-
在quasar.dev/vue-components/spinners
找到有关 Quasar QSpinners
的更多信息。
-
在quasar.dev/layout/page-sticky
找到有关 Quasar QPageSticky
组件的更多信息。
-
在quasar.dev/vue-directives/close-popup
找到有关 Quasar ClosePopup
指令的更多信息。
-
在vuejs.org/v2/guide/mixins.html
找到有关 Vue mixins 的更多信息。
创建应用程序的消息页面
没有消息的聊天应用程序只是一个简单的联系人列表。在这个最终的配方中,我们将完成应用程序的整个周期,为用户创建与其他用户直接交流的可能性。
在这个配方中,我们将创建聊天页面,ChatInput
组件和消息布局。
准备工作
此处的先决条件如下:
-
来自上一个配方的项目
-
Node.js 12+
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始我们的用户消息页面,我们将继续使用在创建应用程序的联系人页面中创建的项目。
如何做...
在这个配方中,我们需要将其分为三个部分:创建ChatInput
组件,创建消息布局,最后创建聊天页面。
创建 ChatInput 组件
在这里,我们将创建ChatInput
组件。该组件的责任是接收用户的新消息输入并将其发送到服务器。
单文件组件<script>部分
在这部分,我们将为页面创建<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
组件内部,创建一个带有v-slot
指令的Template
组件,名称为after
。然后创建一个子QBtn
组件,属性为round
和flat
定义为true
,然后icon
定义为"send"
。最后,在@click
事件上添加事件监听器,执行sendMessage
函数:
<template v-slot:after>
<q-btn
round
flat icon="send"
@click="sendMessage"
/> </template>
这是您组件的渲染:
创建消息布局
在聊天页面中,我们需要一个页脚组件,让用户输入他们的消息,并且这将需要对我们在之前的示例中创建的聊天布局进行大量修改。为了简化和更容易维护,我们将创建一个专门用于聊天页面的新布局,并将其称为消息布局。
单文件组件<script>部分
现在让我们创建消息布局的<script>
部分:
-
在layouts
文件夹中创建一个名为Messages.vue
的新文件。
-
从src/driver/auth.js
文件中导入signOut
函数和components/ChatInput
中的ChatInput
组件:
import {signOut,} from 'src/driver/auth';
import ChatInput from '../components/ChatInput';
- 导出一个
name
属性定义为"ChatLayout"
的default
JavaScript 对象,具有components
属性和另一个名为methods
的属性:
export default {
name: 'MessagesLayout',
components: {},
methods: { }, };
- 在
components
属性中,添加导入的ChatInput
组件:
components: { ChatInput },
- 在
methods
属性中,添加一个名为logOff
的新的异步函数。在这个函数中,我们将执行signOut
函数,并在其后重新加载浏览器:
methods: {
async logOff() {
await signOut();
window.location.reload();
},
}
单文件组件<template>
部分
在这里,我们将创建 Chat 布局的<template>
部分:
- 创建一个带有
view
属性定义为"hHh lpR fFf"
的QLayout
组件:
<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
组件之前,我们将添加一个dense
,flat
和round
属性被定义为true
的QBtn
组件。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
组件的同级,创建一个带有RouterView
组件作为直接子元素的QPageContainer
组件:
<q-page-container>
<router-view /> </q-page-container>
- 最后,创建一个
class
属性定义为bg-white
的QFooter
组件。添加一个子QToolbar
组件,其中包含一个子QToolbarTitle
组件。在QToolbarTitle
组件内部,添加ChatInput
组件:
<q-footer class="bg-white">
<q-toolbar>
<q-toolbar-title>
<chat-input />
</q-toolbar-title>
</q-toolbar> </q-footer>
更改应用程序路由
在创建 Messages 布局之后,我们需要更改聊天页面路由的挂载方式,以便可以使用新创建的 Messages 布局:
-
打开router
文件夹中的routes.js
文件。
-
找到/chat
路由并提取Messages
路由对象。在/chat
路由之后,创建一个新的 JavaScript 对象,具有path
,component
和children
属性。将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>部分
让我们创建单文件组件的<script>部分:
- 在
src/pages
文件夹中打开Messages.vue
文件。在文件的<script>
部分,从vuex
中导入mapActions
和mapGetters
,以及从quasar
中导入date
:
import { mapActions, mapGetters } from 'vuex'; import { date } from 'quasar';
- 导出一个默认的 JavaScript 对象,其中包含
name
、components
、data
、beforeMount
、beforeDestroy
、watch
、computed
和methods
属性。将name
属性定义为MessagesPage
。在components
属性中,添加一个新的属性,AvatarDisplay
,它将接收一个返回导入组件的匿名函数。最后,在data
属性上,创建一个带有值为null
的interval
属性的对象:
export default {
name: 'MessagesPage',
components: {
AvatarDisplay: () => import('components/AvatarDisplay'),
},
data: () => ({
interval: null,
}),
async beforeMount() {},
beforeDestroy() {},
watch: {},
computed: {},
methods: {}, };
- 在
computed
属性上,首先,我们将解构mapGetters
函数,将user
模块作为第一个参数传递,getUser
作为第二个参数。然后我们将对 chat 模块做同样的操作,获取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; },
单文件组件部分
现在让我们创建单文件组件的部分
-
创建一个带有class
属性定义为q-pa-md row justify-center
的QPage
组件,并将QChatMessage
组件作为子组件添加。
-
在QChatMessage
子组件中,首先在v-for
指令上对currentMessages
进行迭代。
-
将ref
和key
组件属性定义为message.id
,stamp
定义为message.createdAt
,text
定义为[message.content]
。
-
然后将sent
属性定义为评估message.authorId
是否与getUser.id
相同,name
定义为message.author.name
,avatar
定义为传入message.author.avatar
和message.author.name
作为参数的getAvatar
方法。
-
然后,将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 模块直接发送消息,无需通过页面或布局等容器,使组件具有状态。
我们需要添加新的布局和路由修改,因为应用程序的布局需要一个固定在应用程序底部的组件。这个底部是消息输入,需要始终对用户可见。
最后,消息页面是一个自动刷新页面,每秒获取新内容,并始终为用户显示新消息。
另请参阅
-
在quasar.dev/vue-components/chat
找到有关 Quasar Framework 的QChatMessage
组件的更多信息。
-
在quasar.dev/quasar-utils/date-utils
找到有关 Quasar Framework 的date
工具的更多信息。
第七章:将您的应用程序转换为 PWA 并部署到 Web
当我们编码完成时,是时候完成我们的应用程序并准备发布了。现在,我们的自定义聊天应用程序已经与后端的 Amplify 服务以及前端的 Quasar 和 Vue 一起运行,我们已经准备好在网络上运行。
在本章中,您将学习如何将您的应用程序转换为渐进式 Web 应用程序(PWA),在服务工作者上添加一些事件以通知用户您的应用程序的新版本,为 iOS 设备创建自定义横幅以进行安装,并最终部署应用程序。
因此,在本章中,我们将涵盖以下配方:
-
将应用程序转换为 PWA
-
创建应用程序更新通知
-
在 iOS 上添加自定义 PWA 安装通知
-
创建生产环境和部署
技术要求
在本章中,我们将使用Node.js,AWS Amplify和Quasar Framework。
注意,Windows 用户!您需要安装一个名为windows-build-tools
的npm
包,以便能够安装所需的软件包。要执行此操作,请以管理员身份打开 PowerShell 并执行以下命令:
> 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
将应用程序转换为 PWA
为了在当前网络上获得应用程序的最佳体验,您需要有一个PWA,在其中可以使您的应用程序缓存代码的某些部分,离线工作,接收推送通知等等。
在这个配方中,您将学习如何将您的单页应用程序(SPA)转换为 PWA,并重新配置 Amplify CLI 以适应新的配置。
做好准备
此配方的先决条件是 Node.js 12+。
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始将我们的项目转换为 PWA,我们将继续使用在第六章,创建聊天和消息 Vuex,页面和路由中创建的项目。
如何做...
在将应用程序发布到生产环境之前,是时候将我们的应用程序转换为 PWA 了。按照以下步骤将 PWA 模式添加到 Quasar 中:
- 首先,我们需要将 PWA 模式添加到 Quasar 应用程序中。要做到这一点,在项目文件夹中,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> quasar m add pwa
- 在项目根文件夹中打开
quasar.conf.js
文件,并找到pwa
属性。从 JavaScript 对象中删除workboxPluginMode
和workboxOptions
属性,并添加cleanupOutdatedCaches
,skipWaiting
和clientsClaim
属性,定义为true
。最后,在manifest
属性上,更改name
,short_name
和description
以匹配您的应用程序,如下面的代码所示:
pwa: {
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
manifest: {
name: 'Chat Application',
short_name: 'Chat App',
description: 'Quasar & AWS Amplify Chat Application',
...
},
...
}
- 现在是时候将 Amplify CLI 的配置更改为新的配置。为此,在项目文件夹中,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> amplify configure project
- CLI 将要求您是否要更改项目名称。无需更改,因此按Enter继续:
? Enter a name for the project (chatapp)
- CLI 将要求您是否要更改项目默认编辑器。选择
Visual Studio Code
(或者您将在项目中使用的默认编辑器),然后按Enter继续:
? Choose your default editor: (Use arrow keys)
❯ Visual Studio Code
Atom Editor
Sublime Text
IntelliJ IDEA
Vim (via Terminal, Mac OS only)
Emacs (via Terminal, Mac OS only)
None
- CLI 将要求您是否要更改项目应用程序类型。选择
javascript
选项,然后按Enter继续:
? Choose the type of app that you're building (Use arrow keys)
android
ios
❯ javascript
- CLI 将要求您是否要更改项目的 JavaScript 框架。选择
none
,然后按Enter继续:
Please tell us about your project
? What javascript framework are you using (Use arrow keys)
angular
ember
ionic
react
react-native
vue
❯ none
- CLI 将要求您是否要更改项目应用程序源目录。无需更改,因此按Enter继续:
? Source Directory Path: (src)
- CLI 将要求您是否要更改项目应用程序分发目录;更改路径为
dist/pwa
,然后按Enter继续:
? Distribution Directory Path: dist/pwa
- CLI 将要求您输入
Build Command
选项;更改命令为quasar build -m pwa
,然后按Enter继续:
? Build Command: quasar build -m pwa
- CLI 将要求您输入
Start Command
选项;更改为quasar dev -m pwa
,然后按Enter继续:
? Start Command: quasar dev -m pwa
- CLI 将要求您是否要更新或删除项目配置。选择
update
,然后按Enter继续:
Using default provider awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
For the awscloudformation provider.
? Do you want to update or remove the project level configuration (Use arrow keys)
❯ update
remove
cancel
- CLI 将要求您在此更新中是否要使用 AWS 配置文件。输入Y,然后按Enter继续:
? Do you want to use an AWS profile? Y
- 最后,选择您想要使用的配置文件,然后按Enter继续:
? Please choose the profile you want to use (Use arrow keys)
❯ default
工作原理...
在本教程中,我们使用 Quasar CLI 将 PWA 开发环境添加到我们的项目中,使用内置的quasar -m add
命令来添加新的开发环境。
然后,我们配置了quasar.conf.js
文件,以在pwa
属性上添加新属性,以便我们可以在部署的应用程序中添加更好的用户体验。
最后,我们更改了 Amplify CLI 配置,以便它将使用新的pwa
环境作为构建命令和分发文件夹。
另请参阅
-
在quasar.dev/quasar-cli/developing-pwa/introduction
找到有关使用 Quasar 开发 PWA 的更多信息。
-
在docs.amplify.aws/cli
找到有关 Amplify CLI 的更多信息。
创建应用程序更新通知
让用户始终了解应用程序的更新是一个好习惯,因为他们将始终知道应用程序正在维护和改进。
使用 PWA,您可以访问诸如创建原生移动应用程序、允许在移动设备上安装应用程序等功能。
发布更新时,我们需要通知用户并更新当前安装的代码。
在本教程中,我们将学习如何使用服务工作程序生命周期来注册应用程序安装,并将其用于在有新更新时通知用户并应用新的更新版本。
准备好了吗?
本教程的先决条件是 Node.js 12+。
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始添加自定义更新通知,我们将继续使用在将应用程序转换为 PWA教程中创建的项目。
如何做...
按照以下步骤在我们的 PWA 中添加更新通知:
- 打开项目根文件夹中的
quasar.conf.js
文件,找到framework
属性。然后对于plugins
属性,将'Notify'
字符串添加到数组中,以便 Quasar 在应用程序启动时加载Notify
插件:
framework: {
...
plugins: [
'Dialog',
'Notify',
],
...
},
- 打开
src-pwa
文件夹中的register-service-worker.js
文件,并从 Quasar 导入Notify
插件:
import { Notify } from 'quasar';
- 创建一个名为
clearLocalCache
的异步函数。然后创建一个名为cachedFiles
的常量,并将其定义为await caches.keys()
;在cachedFiles
常量上,使用参数file
执行一个数组map
函数;在函数内部执行await caches.delete(file)
。最后,重新加载应用程序:
async function clearLocalCache() {
const cachedFiles = await caches.keys(); await cachedFiles.map(async (file) => {
await caches.delete(file);
}); window.location.reload(); }
- 找到
updatefound
函数,并创建一个名为installKey
的常量,并将其定义为'chatAppInstalled'
。然后验证浏览器的localStorage
项中是否有与您创建的常量名称相同的项。如果存在该项,则执行Notify.create
函数,并将 JavaScript 对象作为参数传递,其中color
属性定义为'dark'
,message
定义为更新消息。如果localStorage
项不存在,则向localStorage
添加一个名为installKey
常量的项,其值为'1'
:
updatefound(/* registration */) {
const installKey = 'chatAppInstalled';
if (localStorage.getItem(installKey)) {
Notify.create({
color: 'dark',
message: 'An update is being downloaded from the server.',
});
} else {
localStorage.setItem(installKey, '1');
}
},
- 最后,找到
updated
函数,并添加一个Notify.create
函数,将 JavaScript 对象作为参数传递。在此对象中,添加一个type
属性,定义为'positive'
,一个message
属性,定义为成功更新的消息,一个带有刷新应用程序指示的caption
属性,以及一个定义为数组的actions
属性。在actions
数组中,添加一个 JavaScript 对象,其中label
属性定义为'Refresh'
,color
属性定义为'white'
,handler
属性定义为clearLocalCache
函数:
updated(/* registration */) {
Notify.create({
type: 'positive',
message: 'The application was updated successfully!',
caption: 'Please refresh the page to apply the new update.',
actions: [
{ label: 'Refresh',
color: 'white',
handler: clearLocalCache,
},
],
}); },
以下是通知的预览:
- 发现新更新:
- 更新已应用:
它是如何工作的...
首先,我们将Notify
插件添加到quasar.conf.js
文件的插件属性中,这样 Quasar CLI 就可以在执行运行时为我们提供它。
然后,在register-service-worker.js
文件中,我们添加了 Notify 插件并创建了一个自定义的缓存清除函数。
在updatefound
生命周期中,我们添加了安装验证,以便新的更新通知只会显示给已在其浏览器上安装了应用程序的用户。
最后,我们在更新的生命周期中添加了更新完成过程的通知,并为用户添加了一个清除缓存并重新启动应用程序的操作按钮。
另请参阅
-
在quasar.dev/quasar-plugins/notify
找到有关 Quasar Notify 插件的更多信息。
-
在developer.mozilla.org/en-US/docs/Web/API/Cache
找到有关 JavaScript 缓存接口的更多信息。
在 iOS 上添加自定义 PWA 安装通知
不幸的是,在 iOS 上,Safari 浏览器引擎没有默认的 PWA 安装横幅。在这种情况下,我们必须实现自己的版本。使用名为a2hs.js
(用于添加到主屏幕)的社区插件,我们可以为 iOS 用户启用自定义安装消息的显示。
在这个教程中,我们将学习如何在项目中添加a2hs.js
插件,并如何使用 Quasar 引导文件将其添加到项目引导序列中。
准备工作
这个教程的先决条件是 Node.js 12+。
需要的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
-
a2h2.js
要开始开发自定义 iOS PWA 安装横幅,我们将继续使用在“创建应用程序更新通知”教程中创建的项目。
如何做...
在 Safari 上的 iOS 平台上,浏览器中没有 PWA 应用的安装横幅。在这些步骤中,我们将添加a2hs.js
插件以添加此缺失的功能:
- 首先,我们需要在项目中安装
a2js.js
插件。要做到这一点,在项目文件夹中,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install --save a2hs.js
- 在
boot
文件夹中,然后在src
文件夹内,创建一个a2hs.js
文件并打开它。接下来,导入a2hs.js
插件:
import AddToHomeScreen from 'a2hs.js';
- 要使 Quasar 引导文件起作用,我们需要创建一个默认导出的函数。在这个函数中,创建一个名为
options
的常量,并将其定义为一个 JavaScript 对象,其中brandName
属性作为应用程序的名称:
export default() => {
const options = {
brandName: 'Chat App', };
AddToHomeScreen(options); };
- 最后,在项目根文件夹中,打开
quasar.conf.js
文件并找到boot
属性。在数组中,添加'a2hs'
字符串,以使其对 Quasar CLI 可用,并加载新创建的引导文件:
boot: [
'amplify',
'axios',
'a2hs', ],
这是在 iOS 设备上弹出的警报的预览:
它是如何工作的...
首先,我们使用npm
安装了a2hs.js
插件到项目中。然后,我们在 Quasar 上创建了一个a2hs.js
文件,用作引导文件。
然后,在新创建的文件中,我们导入了a2hs.js
插件和应用程序 logo,然后实例化了具有自定义选项的a2hs.js
插件。
最后,我们将a2hs
引导文件添加到quasar.conf.js
文件的boot
属性中。
另请参阅
-
您可以在quasar.dev/quasar-cli/boot-files
找到有关 Quasar 引导文件结构的更多信息。
-
您可以在github.com/koddr/a2hs.js/
找到有关a2hs.js
的更多信息。
创建生产环境和部署
在所有工作都已经完成,准备好我们的应用程序后,现在是时候将其构建为一个生产就绪的分发,通过创建一个生产环境并将其部署到该环境。这个新环境将不会有来自测试的数据,并且我们将确保这个环境将专门用于生产状态。
生产环境可以被描述为一个最终用户放置您的应用程序的环境,具有准备好接收最终用户数据的代码和数据库。
在这个教程中,我们将学习如何使用 Amplify CLI 创建一个生产环境,以及如何在 Amplify 控制台上将其定义为默认的生产环境。
准备工作
这个教程的先决条件是 Node.js 12+。
所需的 Node.js 全局对象如下:
-
@aws-amplify/cli
-
@quasar/cli
要开始创建生产环境,我们将继续使用在在 iOS 上添加自定义 PWA 安装通知教程中创建的项目。
如何做...
我们需要准备好我们的应用程序,以在生产环境中发布给我们的用户。按照以下步骤在 Amplify 控制台中创建生产环境并将其定义为我们应用程序的默认生产环境:
- 在项目根目录中,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> amplify env add
- Amplify CLI 会询问您是否要使用现有环境作为基础;按下N和Enter继续:
? Do you want to use an existing environment? (Y/n) n
- 现在 Amplify CLI 会询问您新环境的名称;将
production
作为名称输入并按Enter继续:
? Enter a name for the environment production production
- CLI 会询问您是否要在此更新中使用 AWS 配置文件;输入Y并按Enter继续:
? Do you want to use an AWS profile? (Y/n) y
- 选择要使用的配置文件并按Enter继续:
? Please choose the profile you want to use (Use arrow keys)
❯ default
- 现在,我们需要将所做的更改推送到服务器;为此,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify push
- Amplify CLI 将询问您是否要更新自动生成的 GraphQl 代码;输入Y并按Enter继续:
? Do you want to update code for your updated GraphQL API (Y/n) y
- Amplify CLI 将询问您是否要覆盖当前现有的代码;输入Y并按Enter继续:
? Do you want to generate GraphQL statements (queries, mutations and
subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions (Y/n) y
- 然后将您的网站发布到生产环境。要做到这一点,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify publish
- 对于最后一部分,我们需要配置应用程序设置以使用我们创建的新
production
环境。为此,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:
> amplify console
- 打开侧边菜单,点击“常规”链接:
- 现在,在应用程序详细信息卡中,点击右上角的“编辑”按钮:
- 然后在设置中,打开生产环境选择框,并选择生产环境:
- 最后,要检查更改是否已正确保存,请刷新页面并检查应用程序详细信息卡的“设置”部分:
它是如何工作的...
在这个教程中,我们首先使用 Amplify CLI 向本地 Amplify 实例添加了一个新环境,并选择使用全新的环境。然后,我们将这个新环境发送到云端,更新我们的本地代码库,并最终使用这个新环境发布项目。
最后,我们去 Amplify 控制台配置应用程序的生产环境,作为我们创建的新环境。
另请参阅
-
在docs.amplify.aws/cli
上查找有关 Amplify CLI 的更多信息。
-
在aws.amazon.com/amplify/console/?nc1=h_ls
上查找有关 Amplify 控制台的更多信息。
</script>
标签:指南,Vue,name,创建,应用程序,GraphQL,组件,我们,属性
From: https://www.cnblogs.com/apachecn/p/18195747
PasswordInput
的<template>
部分。按照以下说明来实现正确的输入组件:在<template>
部分,创建一个QInput
组件,并将value
,label
和rules
属性添加为变量。value
将绑定到value
属性,label
将绑定到label
属性,rules
将接收一个函数数组,用于执行对表单输入的基本验证。
对于type
属性,将其定义为一个变量,并将其设置为对isPwd
的三元验证,在"password"
和"text"
之间切换。
将outlined
和lazy-rules
属性设置为true
。
创建一个hint
变量属性,并将其定义为三元运算符,它将检查当前值的长度是否匹配最小值大小;否则,它将向用户显示一条消息。
然后,通过创建一个v-on
指令并使用$listeners
Vue API 作为值来为事件创建事件侦听器。
在QInput
模板内部,我们将添加一个子组件,该组件将占据一个命名插槽v-slot:append
,该插槽将容纳一个QIcon
组件。
对于QIcon
组件,定义name
属性以对isPwd
变量进行响应,因此当isPwd
设置为true
时,它将是'visibility_off'
,或者当isPwd
设置为false
时,它将是'visibility'
。将class
属性定义为"cursor-pointer"
,以便鼠标具有实际鼠标的外观和"hover hand icon"
,并在@click
事件侦听器上,我们将设置isPwd
为当前isPwd
的相反值。
<template>
<q-input
:value="value"
:type="isPwd ? 'password' : 'text'"
:rules="[ val => val.length >= 8 || 'Your password need to have 8
or more characters', val => val !== null && val !== '' ||
'Please type your password']"
:hint=" value.length < 8 ? 'Your password has a minimum of 8
characters' : ''"
:label="label"
outlined
lazy-rules
v-on="$listeners"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input> </template>
NameInput
组件是最简单的,几乎没有改变QInput
组件的行为,只是添加了验证规则和一些个性化。NameInput
组件的<script>
部分:name
和props
:export default {
name: '',
props: {}, };
name
属性中,将值定义为'NameInput'
:name: 'NameInput',
props
属性中,添加一个属性value
,作为一个 JavaScript 对象,里面有三个属性:type
,default
和required
。将value.type
设置为String
,value.default
设置为**''**
,value.required
设置为false
:props: {
value: {
type: String,
default: '',
required: false,
}, },
NameInput
组件的<template>
部分:在<template>
部分,创建一个QInput
组件,并添加value
和rules
属性作为变量。value
将绑定到value
属性,rules
将接收一个函数数组,用于检查表单输入的基本验证。
将outlined
和lazy-rules
属性设置为true
,并将label
属性定义为"Your Name"
。
最后,通过创建一个v-on
指令并将"$listeners"
Vue API 作为值来为事件创建事件监听器。
<template>
<q-input
:value="value"
:rules="[ val => (val && val.length > 0
|| 'Please type a valid Name')]"
outlined
label="Your Name"
lazy-rules
v-on="$listeners"
/> </template>
EmailInput
组件中,我们需要特别注意规则验证的处理,因为我们需要检查正在输入的电子邮件是否是有效的电子邮件地址。EmailInput
组件的<script>
部分:name
,props
和methods
:export default {
name: '',
props: {},
methods: {}, };
name
属性中,将值定义为'EmailInput'
:name: 'EmailInput',
props
属性中,添加一个属性value
,作为一个 JavaScript 对象,里面有三个属性:type
,default
和required
。将value.type
设置为String
,value.default
设置为**'**
,value.required
设置为false
:props: {
value: {
type: String,
default: '',
required: false,
}, },
methods
属性中,我们需要添加一个名为validateEmail
的新方法,该方法接收一个名为email
的参数。此方法将通过正则表达式测试接收到的参数,以检查它是否是有效的表达式,并返回结果:methods: {
validateEmail(email) {
const regex = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;
<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-
z\-]+\.)+[A-Za-z]{2,}))$/;
return regex.test(email);
}, },
EmailInput
组件的<template>
部分:在<template>
部分,创建一个QInput
组件,并将value
和rules
属性作为变量添加。value
将绑定到value
属性,rules
将接收一个函数数组,用于执行基本验证表单输入的检查。
将outlined
和lazy-rules
属性添加为true
,将label
属性定义为"Your E-Mail"
,将type
属性定义为"email"
.
最后,通过创建一个v-on
指令并将"$listeners"
作为值,为事件创建事件侦听器。
<template>
<q-input
:value="value"
:rules="[ val => (val && val.length > 0 && validateEmail(val)
|| 'Please type a valid E-mail')]"
outlined
type="email"
label="Your E-mail"
lazy-rules
v-on="$listeners"
/> </template>
AvatarInput
组件,我们需要添加使用AWS-Amplify Storage
API 驱动程序的逻辑。通过这样做,我们可以直接通过组件上传文件,并使逻辑和组件在整个应用程序中更具可重用性。AvatarInput
组件的<script>
部分:quasar
包中导入uid
和从'src/driver/bucket'
中导入uploadFile
:import { uid } from 'quasar';
import { uploadFile } from 'src/driver/bucket';
name
,props
,data
和methods
:export default {
name: '',
props: {},
data: () => ({})
methods: {}, };
name
属性中,将值定义为"AvatarInput"
:name: 'AvatarInput',
props
属性中,添加一个属性value
,作为 JavaScript 对象,内部有三个属性 - type
,default
和required
。将value.type
设置为Object
,将value.default
设置为返回 JavaScript 对象的工厂函数,将value.required
设置为false
:props: {
value: {
type: Object,
required: false,
default: () => ({}),
}, },
data
属性中,我们需要添加六个新属性:file
,type
,name
,s3file
,photoUrl
和canUpload
:file
属性将是一个数组。
type
,name
和photoUrl
将是字符串。
canUpload
属性将是一个布尔值,定义为false
。
s3file
将是一个具有三个属性的 JavaScript 对象,key
,bucket
和region
,它们都是字符串:
data: () => ({
file: [],
type: '',
name: '',
s3file: {
key: '',
bucket: '',
region: '',
},
photoUrl: '',
canUpload: false, }),
methods
属性上,我们需要添加一个名为uploadFile
的新方法。这个方法将检查是否可以开始上传过程,然后调用uploadFile
函数,传递this.file
、this.name
和this.type
作为参数。在我们收到上传函数的响应后,我们将使用结果来定义this.s3File
和$emit
以及事件'input'
。最后,我们将this.canUpload
定义为false
:async uploadFile() {
try {
if (this.canUpload) {
const file = await uploadFile(this.file, this.name,
this.type);
this.s3file = file;
this.$emit('input', file);
this.canUpload = false;
} } catch (err) {
console.error(err);
} },
getFile
的方法,它接收$event
作为参数。在函数中,我们将把this.type
定义为$event.type
,将this.name
定义为uid
生成函数和文件名的连接。然后,我们将为FileReader
实例创建一个监听器,它将把that.photoURL
设置为读取的结果,并将that.canUpload
设置为true
:getFile($event) {
this.type = $event.type;
this.name = `${uid()}-${$event.name}`;
const that = this;
const reader = new FileReader();
reader.onload = ({ target }) => {
that.photoUrl = target.result;
that.canUpload = true;
};
reader.readAsDataURL(this.file); },
AvatarInput
组件的<template>
部分的时候了:QFile
组件,将v-model
指令绑定到file
数据属性。将outlined
和bottom-slots
属性定义为true
,并将label
属性设置为"Your Avatar"
。对于class
属性,将其设置为"q-pr-md"
,最后将@input
事件监听器设置为目标getFile
方法:<q-file
v-model="file"
outlined
bottom-slots label="Your Avatar"
class="q-pr-md"
@input="getFile" >
</q-file>
QFile
组件内部,我们将添加一个直接子组件,它将放置在一个命名为v-slot:before
的插槽中,并且只有在数据属性中存在任何photoUrl
时才会显示。在这个插槽中,我们将添加一个QAvatar
组件,其子组件是一个HTML img
标签,其中src
属性绑定到photoUrl
数据属性:<template
v-if="photoUrl"
v-slot:before >
<q-avatar>
<img :src="photoUrl">
</q-avatar> </template>
v-slot:after
的插槽下面,里面有一个QBtn
组件。QBtn
将具有以下属性:round
、dense
、flat
、icon
定义为"cloud_upload"
,并且@click
事件监听器绑定到uploadFile
方法:<template v-slot:after>
<q-btn
round
dense flat icon="cloud_upload"
@click="uploadFile"
/> </template>
在src
文件夹下创建一个名为mixins
的新文件夹,然后创建一个名为getAvatar.js
的文件,并打开它。
从driver/bucket
文件中导入getFile
函数。
导出一个带有methods
属性的default
JavaScript 对象。在methods
属性内部,创建一个名为getAvatar
的新函数。此函数将接收两个参数,object
和name
。对于此函数,我们将检查对象是否为null
,以及是否有一个名字来显示初始字母。如果 JavaScript 对象中有属性,我们将返回getFile
函数的结果,将key
属性作为参数传递:
import { uploadFile } from 'src/driver/bucket'; export default {
methods: {
async getAvatar(object, name) {
const baseUrl = 'http://placehold.jp/350/9c27b0/FFFFFF/600x600.png?text='; if (object === null && !name) return `${baseUrl}%20`; if (!object && name) return `${baseUrl}${name.split('').shift()}`; return getFile(object.key);
}, }, };
AvatarDisplay
将负责处理用户名的检查和验证,因此我们不需要在每个需要使用它的页面上重新编写所有规则。AvatarDisplay
组件的<script>
部分:在components
文件夹中创建一个名为AvatarDisplay.vue
的新文件,并打开它。
创建一个带有以下属性的export default
JavaScript 对象:name
,props
,mixins
,beforeMount
,data
,watch
,computed
和methods
:
import { QImg } from 'quasar'; import getAvatar from 'src/mixins/getAvatar'; export default {
name: '',
props: {},
mixins: [],
async beforeMount() {},
data: () => ({}),
watch: {},
computed: {},
methods: {}, };
name
属性,将其定义为"AvatarDisplay"
:name: 'UsernameInput',
props
属性,将其定义为 JavaScript 对象,并添加三个新属性,分别称为avatarObject
,name
和tag
。avatarObject
属性将是一个具有type
,default
和required
属性的 JavaScript 对象。name
和tag
属性需要定义为String
,default
为''
,required
为false
。对于tag
属性,我们将将默认属性设置为'q-img'
:props: {
avatarObject: {
type: Object,
required: false,
default: () => ({}),
},
name: {
type: String,
required: false,
default: '',
},
tag: {
type: String,
required: false,
default: 'q-img',
}, },
mixins
属性,我们将在数组中添加导入的getAvatar
mixin:mixins: [getAvatar],
data
中返回 JavaScript 对象,我们将创建一个名为src
的属性,其默认值为''
:data: () => ({
src: '', }),
computed
属性,创建一个名为 components 的新属性,返回一个三元运算符,检查tag
属性是否等于'q-img'
,并返回 Quasar 中导入的QImg
组件;如果不是,则返回'img'
标签:computed: {
componentIs() {
return this.tag === 'q-img' ? QImg : 'img';
}, },
methods
属性中,创建一个名为updateSrc
的新方法。在这个方法中,我们将src
定义为getAvatar
方法的结果。我们将函数的参数传递给avatarObject
和name
属性:methods: {
async updateSrc() {
this.src = await this.getAvatar(this.avatarObject, this.name);
}, },
beforeMount
生命周期钩子中,我们将调用updateSrc
方法:async beforeMount() {
await this.updateSrc(); },
watch
属性,创建两个属性,avatarObject
和name
。对于avatarObject
属性,将其定义为一个具有两个属性handler
和deep
的 Javascript 对象。在deep
属性中,将其定义为true
,在handler
属性上,将其定义为调用updateSrc
方法的函数。然后在name
属性上,创建一个handler
属性,定义为调用updateSrc
方法的函数:watch: {
avatarObject: {
async handler() {
await this.updateSrc();
},
deep: true,
},
name: {
async handler() {
await this.updateSrc();
},
}, },
AvatarDisplay
组件的<template>
部分:<template>
部分,创建一个component
元素。创建两个动态属性,src
和is
。现在,src
将绑定到数据src
,而is
属性将绑定到componentIs
计算属性。最后,创建一个spinner-color
属性,并将其定义为'primary'
。<template>
<component
:src="src"
:is="componentIs"
spinner-color="primary"
/> </template>
Usernameinput
和Nameinput
,我们在QInput
组件周围创建了一个包装器,添加了验证规则和文本,以便更轻松地开发和重用组件,而无需添加更多逻辑。PasswordInput
组件中,我们添加了控制密码可见性的逻辑,该逻辑会更改输入的类型,并自定义了QInput
组件,以便有一个特殊按钮来触发可见性控制。EmailInput
,我们需要基于正则表达式创建自定义验证规则,检查输入的电子邮件是否是有效的电子邮件,并防止用户意外输入无效的电子邮件。AvatarInput
中,使用QFile
组件,我们创建了一个自定义输入,当浏览器读取文件并将文件上传到 AWS Amplify Storage 时,自动上传文件,并在文件上传后将文件 URL 返回给应用程序。在quasar.dev/vue-components/input
找到有关 Quasar 输入组件的更多信息。
在quasar.dev/vue-components/file-picker
找到有关 Quasar 文件选择器组件的更多信息。
vue-router
结构,以及我们正在尝试访问的页面的最终路由。vue-router
上创建父子责任划分。vue-router
的父子结构中。最后的教程项目
Node.js 12+
@aws-amplify/cli
@quasar/cli
在layouts
文件夹中创建一个名为Base.vue
的新文件。
使用 JavaScript 对象创建一个export default
实例,其中name
属性定义为'BaseLayout'
:
<script> export default {
name: 'BaseLayout', }; </script>
QLayout
组件,其中view
属性定义为"hHh Lpr lff"
:<q-layout view="hHh Lpr lff"> </q-layout>
QLayout
组件内部,我们需要添加一个带有elevated
属性的QHeader
组件:<q-header elevated> </q-header>
QHeader
组件中,我们将添加一个QToolbar
组件,其中包含一个QToolbarTitle
组件作为子元素,以文本作为插槽占位符:<q-toolbar>
<q-toolbar-title>
Chat App
</q-toolbar-title> </q-toolbar>
QHeader
组件之后,创建一个带有RouterView
组件的QPageContainer
组件作为直接子元素:<q-page-container>
<router-view /> </q-page-container>
在layouts
文件夹中创建一个名为Chat.vue
的新文件。
从src/driver/auth.js
中导入signOut
函数:
import {signOut,} from 'src/driver/auth';
export default
实例,包括一个 JavaScript 对象,其中包括两个属性:一个名为name
的属性,定义为'ChatLayout'
,另一个名为methods
的属性:export default {
name: 'ChatLayout',
methods: { }, };
methods
属性中,添加一个名为logOff
的新异步函数;在这个函数中,我们将执行signOut
函数,并在其后重新加载浏览器:async logOff() {
await signOut();
window.location.reload(); }
<template>
部分:view
属性定义为"hHh Lpr lff"
的QLayout
组件:<q-layout view="hHh Lpr lff"> </q-layout>
QLayout
组件内部,我们需要添加一个带有elevated
属性的QHeader
组件:<q-header elevated> </q-header>
QHeader
组件,我们将添加一个QToolbar
组件,其中包含一个QToolbarTitle
组件作为子元素,文本作为插槽占位符:<q-toolbar>
<q-toolbar-title>
Chat App
</q-toolbar-title> </q-toolbar>
QToolbar
组件,在QToolbarTitle
组件之前,我们将添加一个带有dense
、flat
和round
属性定义为true
的QBtn
组件。在icon
属性中,我们将添加一个三元表达式,验证$route.meta.goBack
是否存在,以显示back图标或person图标。最后,对于to
属性,我们将做同样的操作,但值将是$route.meta.goBack
或一个具有name
属性为Edit
的 JavaScript 对象。<q-btn
dense
flat
round
replace
:icon="$route.meta.goBack ? 'keyboard_arrow_left' : 'person'"
:to="$route.meta.goBack ? $route.meta.goBack : {name: 'Edit'}" />
QToolbarTitle
组件之后,我们将添加一个带有dense
、flat
和round
属性的QBtn
组件,这些属性被定义为true
。对于icon
属性,我们将定义为exit_to_app
,对于@click
指令,我们将传递logOff
方法:<q-btn
dense
flat round icon="exit_to_app"
@click="logOff" />
QHeader
组件之后,创建一个带有RouterView
组件作为直接子元素的QPageContainer
组件:<q-page-container>
<router-view /> </q-page-container>
QLayout
、QHeader
和QToolbarTitle
组件。这些组件创建了页面的结构,包括布局容器、头部容器和自定义头部工具栏。关于 Quasar Framework QLayout
组件的更多信息,请访问quasar.dev/layout/layout
。
关于 Quasar Framework QHeader
组件的更多信息,请访问quasar.dev/layout/header-and-footer
。
关于 Quasar Framework QPage
组件的更多信息,请访问quasar.dev/layout/page
。
关于 Quasar Framework QBtn
组件的更多信息,请访问quasar.dev/vue-components/button
。
在您的应用程序中创建用户 Vuex 模块
为您的应用程序创建用户页面和路由
windows-build-tools
的npm
包,以便能够安装所需的软件包。要做到这一点,以管理员身份打开 PowerShell 并执行> npm install -g windows-build-tools
命令。> npm install -g @quasar/cli
> npm install -g @aws-amplify/cli
@aws-amplify/cli
@quasar/cli
在store
文件夹中,创建一个名为user
的新文件夹。在内部,创建一个名为state.js
的新文件并打开它。
创建一个名为createState
的新函数,它返回一个 JavaScript 对象,提供id
,username
,email
,name
,avatar
,password
,loading
,validated
和error
属性。id
,username
,email
,name
和password
属性将被定义为空字符串,而loading
和validated
属性将被定义为false
。error
将被定义为undefined
,avatar
是一个具有三个属性的 JavaScript 对象-key
,bucket
和region
:
export function createState() {
return {
id: '',
username: '',
email: '',
name: '',
avatar: {
key: '',
bucket: '',
region: '',
},
password: '',
loading: false,
validated: false,
error: undefined,
}; }
export default
执行createState
函数:export default createState();
在store/user
文件夹内创建一个名为types.js
的新文件并打开它。
在文件中,导出一个默认的 JavaScript 对象,提供CREATE_USER
,SET_USER_DATA
,CLEAR_USER
,USER_VALIDATED
,LOADING
和ERROR
属性。值与属性相同,但格式为字符串。
export default {
CREATE_USER: 'CREATE_USER', SET_USER_DATA: 'SET_USER_DATA',
CLEAR_USER: 'CLEAR_USER',
USER_VALIDATED: 'USER_VALIDATED',
LOADING: 'LOADING',
ERROR: 'ERROR', };
在store/user
文件夹内创建一个名为mutations.js
的新文件并打开它。
导入新创建的types.js
文件和state.js
中的createState
JavaScript 对象:
import MT from './types'; import { createState } from './state';
setLoading
的新函数,状态作为第一个参数。在内部,我们将设置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; }
createUser
的新函数,以state
作为第一个参数,并以 JavaScript 对象作为第二个参数。这个 JavaScript 对象将提供id
、email
、password
、name
和username
属性。所有属性都将是空字符串。在函数内部,我们将定义state
属性为函数参数中收到的属性:function createUser(state, {
id = '',
email = '',
password = '',
name = '',
username = '', }) {
state.username = username;
state.email = email;
state.name = name;
state.id = id;
state.password = window.btoa(password);
state.loading = false; }
validateUser
的新函数,以state
作为第一个参数。在其中,我们将将state.validated
属性设置为true
,删除state.password
属性,并将state.loading
属性设置为false
:function validateUser(state) {
state.validated = true;
delete state.password;
state.loading = false; }
setUserData
的新函数,以state
作为第一个参数,并以 JavaScript 对象作为第二个参数。这个对象将提供id
、email
、password
、name
和username
属性。它们都将是空字符串。avatar
是一个具有三个属性的 JavaScript 对象:key
、bucket
和region
。在函数内部,我们将定义state
属性为函数参数中收到的属性:function setUserData(state, {
id = '',
email = '',
name = '',
username = '',
avatar = {
key: '',
bucket: '',
region: '',
}, }) {
state.id = id;
state.email = email;
state.name = name;
state.username = username;
state.avatar = avatar || {
key: '',
bucket: '',
region: '',
}; delete state.password; state.validated = true;
state.loading = false; }
clearUser
的新函数,以state
作为第一个参数。然后,在其中的函数中,我们将从createState
函数获取一个新的干净的state
,并迭代当前的state
,将state
属性的值重新定义为默认值:function clearUser(state) {
const newState = createState(); Object.keys(state).forEach((key) => {
state[key] = newState[key];
}); }
将MT.LOADING
设置为setLoading
将MT.ERROR
设置为setError
将MT.CREATE_USER
设置为createUser
将MT.USER_VALIDATED
设置为validateUser
将MT.SET_USER_DATA
设置为setUserData
将MT.CLEAR_USER
设置为clearUser
export default {
[MT.LOADING]: setLoading,
[MT.ERROR]: setError,
[MT.CREATE_USER]: createUser,
[MT.USER_VALIDATED]: validateUser,
[MT.SET_USER_DATA]: setUserData,
[MT.CLEAR_USER]: clearUser, };
getter
函数中,该函数将始终接收到 Vuexstore
的当前state
作为第一个参数。在store/user
文件夹内创建一个名为getters.js
的新文件。
创建一个名为getUserId
的新函数,返回state.id
:
const getUserId = (state) => state.id;
getUserEmail
的新函数,返回state.email
:const getUserEmail = (state) => state.email;
getUserUsername
的新函数,返回state.username
:const getUserUsername = (state) => state.username;
getUserAvatar
的新函数,返回state.avatar
:const getUserAvatar = (state) => state.avatar;
getUser
的新函数,返回一个提供id
、name
、username
、avatar
和email
属性的 JavaScript 对象。这些属性的值将对应于state
:const getUser = (state) => ({
id: state.id,
name: state.name,
username: state.username,
avatar: state.avatar,
email: state.email, });
isLoading
的新函数,返回state.loading
:const isLoading = (state) => state.loading;
hasError
的新函数,返回state.error
:const hasError = (state) => state.error;
getUserId
、getUserEmail
、getUserUsername
、getUserAvatar
、getUser
、isLoading
和hasError
)作为属性的default
JavaScript 对象:export default {
getUserId,
getUserEmail,
getUserUsername,
getUserAvatar,
getUser,
isLoading,
hasError, };
在store/user
文件夹内创建一个名为actions.js
的文件并打开它。
首先,我们需要导入这里将要使用的函数、枚举和类。
从aws-amplify
npm 包中导入graphqlOperation
。
从 GraphQL 查询中导入getUser
和listUsers
。
从 GraphQL 变异中导入createUser
和updateUser
。
从driver/auth.js
中导入signUp
、validateUser
、signIn
、getCurrentAuthUser
和changePassword
函数。
从driver/appsync
导入AuthAPI
。
从./types.js
导入 Vuex 变异类型:
import { graphqlOperation } from 'aws-amplify';
import { getUser, listUsers } from 'src/graphql/queries';
import { createUser, updateUser } from 'src/graphql/mutations';
import { AuthAPI } from 'src/driver/appsync';
import {
signUp,
validateUser,
signIn,
getCurrentAuthUser,
changePassword,
} from 'src/driver/auth';
import MT from './types';
initialLogin
的新异步函数。此函数将接收一个 JavaScript 对象作为第一个参数。这将提供一个commit
属性。在这个函数中,我们将获取当前认证的用户,从 GraphQL API 获取他们的数据,并将用户数据提交到 Vuex 存储中:async function initialLogin({ commit }) {
try {
commit(MT.LOADING); const AuthUser = await getCurrentAuthUser(); const { data } = await AuthAPI.graphql(graphqlOperation(getUser, {
id: AuthUser.username,
})); commit(MT.SET_USER_DATA, data.getUser); return Promise.resolve(AuthUser);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
signUpNewUser
的新异步函数。此函数将接收一个带有commit
属性的 JavaScript 对象作为第一个参数。第二个参数也是一个 JavaScript 对象,但具有email
、name
和password
属性。在这个函数中,我们将执行auth.js
驱动器中的signUp
函数来注册并在 AWS Cognito 用户池中创建用户,然后将用户数据提交到 Vuex 存储中:async function signUpNewUser({ commit }, {
email = '',
name = '',
username = '',
password = '', }) {
try {
commit(MT.LOADING); const userData = await signUp(email, password); commit(MT.CREATE_USER, {
id: userData.userSub,
email,
password,
name,
username,
}); return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
createNewUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和state
属性。对于第二个参数,函数将接收一个code
字符串。在这个函数中,我们将从state
中获取用户数据,并执行auth.js
驱动器中的validateUser
函数,以检查用户是否是 AWS Cognito 用户池中的有效用户。然后,我们将执行auth.js
中的signIn
函数,将email
和password
作为参数传递,需要将password
转换为加密的 base64 字符串,然后发送到函数。之后,我们将获取经过身份验证的用户数据,并将其发送到 GraphQL API 以创建一个新用户:async function createNewUser({ commit, state }, code) {
try {
commit(MT.LOADING);
const {
email,
name,
username,
password,
} = state;
const userData = await validateUser(email, code); await signIn(`${email}`, `${window.atob(password)}`); const { id } = await getCurrentAuthUser(); await AuthAPI.graphql(graphqlOperation(
createUser,
{
input: {
id,
username,
email,
name,
},
},
)); commit(MT.USER_VALIDATED); return Promise.resolve(userData);
} catch (err) {
commit(MT.ERROR, err);
return Promise.reject(err);
} }
signInUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和dispatch
属性。第二个参数也是一个 JavaScript 对象,包含email
和password
属性。在这个函数内部,我们将执行auth.js
驱动器中的signIn
函数,将email
和password
作为参数传递,然后触发initialLogin
Vuex 动作:async function signInUser({ commit, dispatch }, { email = '', password = '' }) {
try {
commit(MT.LOADING); await signIn(`${email}`, `${password}`); await dispatch('initialLogin'); return Promise.resolve(true);
} catch (err) {
commit(MT.ERROR);
return Promise.reject(err);
} }
editUser
的新异步函数。这个函数将接收一个 JavaScript 对象作为第一个参数,其中包含commit
和state
属性。第二个参数也是一个 JavaScript 对象,包含username
、name
、avatar
、password
和newPassword
属性。在这个函数内部,我们将合并state
的值和作为参数接收到的新值。然后将它们发送到 GraphQL API 以更新用户信息。然后,我们将检查是否password
和newPasssword
属性都填写了。如果是,我们将执行auth.js
驱动器中的changePassword
函数,以在 AWS Cognito 用户池中更改用户的密码:async function editUser({ commit, state }, {
username = '',
name = '',
avatar = {
key: '',
bucket: '',
region: '',
},
password = '',
newPassword = '', }) {
try {
commit(MT.LOADING); const updateObject = {
...{
name: state.name,
username: state.username,
avatar: state.avatar,
},
...{
name,
username,
avatar,
},
}; const { data } = await AuthAPI.graphql(graphqlOperation(updateUser,
{ input: { id: state.id, ...updateObject } })); if (password && newPassword) {
await changePassword(password, newPassword);
} commit(MT.SET_USER_DATA, data.updateUser); return Promise.resolve(data.updateUser);
} catch (err) {
return Promise.reject(err);
} }
listAllUsers
的新异步函数。这个函数将获取数据库中的所有用户并返回一个列表:async function listAllUsers() {
try {
const {
data: {
listUsers: {
items: usersList,
},
},
} = await AuthAPI.graphql(graphqlOperation(
listUsers,
)); return Promise.resolve(usersList);
} catch (e) {
return Promise.reject(e);
} }
export default {
initialLogin,
signUpNewUser,
createNewUser,
signInUser,
editUser,
listAllUsers, };
在store/user
文件夹内创建一个名为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';
export default
,提供state
、actions
、mutations
、getters
和namespaced
(设置为true
)属性:export default {
namespaced: true,
state,
actions,
mutations,
getters, };
打开store
文件夹中的index.js
文件。
在store/user
文件夹中导入新创建的index.js
:
import Vue from 'vue'; import Vuex from 'vuex'; import user from './user';
modules
的新属性,并将其定义为 JavaScript 对象。然后,我们需要添加一个新的user
属性-这将自动用作值,因为它与上一步中导入的 User 模块具有相同的名称:export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
user,
},
strict: process.env.DEV,
}); return Store; }
state
、mutations
和actions
。这些属性作为一个单一的结构,通过注入的$store
原型或导出的store
变量绑定到 Vue 应用程序。state
是一个集中的对象,保存着你的信息,并使其可以被mutations
、actions
或components
使用。改变state
总是需要通过mutation
执行同步函数。mutation
是一个同步函数,可以改变state
并被追踪。这意味着在开发时,你可以在 Vuex 存储中时间旅行通过所有执行的mutations
。action
是一个异步函数,可以用来保存业务逻辑、API 调用、分发其他actions
和执行mutations
。这些函数是当你需要对 Vuex 存储进行更改时的常见入口点。您可以在aws-amplify.github.io/docs/js/api#amplify-graphql-client
找到有关 Amplify 的 AppSync GraphQL 客户端的更多信息。
您可以在https://vuex.vuejs.org/找到有关 Vuex 的更多信息。
您可以在vuex.vuejs.org/guide/modules.html
找到有关 Vuex 模块的更多信息
我们在上一个食谱中创建的项目
Node.js 12+
@aws-amplify/cli
@quasar/cli
quasar.conf.js
文件,并找到framework
属性。然后,在plugins
属性中,将'Dialog'
字符串添加到数组中,以便 Quasar 在启动应用程序时加载Dialog
插件:framework: {
...
plugins: [
'Dialog',
],
...
},
PasswordInput
和EmailInput
。<script>
部分<script>
部分的时候了:在src/pages
文件夹中,打开Index.vue
文件。
从vuex
包中导入mapActions
和mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
export default
JavaScript 对象;即name
(定义为'Index'
),components
,data
,computed
和methods
:export default {
name: 'Index',
components: {
},
data: () => ({ }),
computed: { },
methods: { }, };
components
属性中,添加两个名为PasswordInput
和EmailInput
的新属性。将PasswordInput
定义为一个匿名函数,其返回值为import('components/PasswordInput')
,并将EmailInput
定义为一个匿名函数,其返回值为import('components/EmailInput')
:components: {
PasswordInput: () => import('components/PasswordInput'),
EmailInput: () => import('components/EmailInput'), },
data
属性中,我们将返回一个提供两个属性email
和password
的 JavaScript 对象,它们都将是空字符串:data: () => ({
email: '',
password: '', }),
computed
属性中,我们将解构mapGetters
函数,将我们想要的模块的命名空间作为第一个参数(在本例中为'user'
)。我们将把我们想要导入的getters
数组(在本例中为isLoading
)作为第二个参数传递进去:computed: {
...mapGetters('user', [
'isLoading',
'getUserId',
]), },
beforeMount
生命周期钩子上,我们将添加一个if
语句,检查getUserId
是否为真,并将用户重定向到Contacts
路由。async beforeMount() {
if (this.getUserId) {
await this.$router.replace({ name: 'Contacts' });
} },
methods
属性中,我们将解构mapActions
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递进去。对于第二个参数,我们将使用一个包含我们想要导入的actions
的数组(在这种情况下,这是signInUser
)。接下来,我们需要添加异步的onSubmit
方法,该方法将调度signInUser
并将用户发送到Contacts
路由,以及createAccount
方法,该方法将用户发送到SignUp
路由:methods: {
...mapActions('user', [
'signInUser',
]),
async onSubmit() {
try {
await this.signInUser({
email: this.email,
password: this.password,
});
await this.$router.push({ name: 'Contacts' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
createAccount() {
this.$router.push({ name: 'SignUp' });
}, },
<template>
部分<template>
部分来完成我们的页面:QPage
的组件,其class
属性定义为"bg-grey-1 flex flex-center"
:<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
QPage
组件内部,创建一个QCard
组件,其style
属性定义为"width: 350px"
:<q-card style="width: 350px"> </q-card>
QCard
组件内部,创建一个带有class
属性定义为no-margin
的QCardSection
和一个h6
子组件:<q-card-section>
<h6 class="no-margin">Chat Application</h6> </q-card-section>
QCardSection
,其中包含一个QForm
子组件,其 class 属性定义为q-gutter-md
。在QForm
组件内部,创建一个EmailInput
组件,其v-model
指令绑定到data.email
,以及一个PasswordInput
组件,其v-model
指令绑定到data.password
属性:<q-card-section>
<q-form
class="q-gutter-md"
>
<email-input
v-model.trim="email"
/>
<password-input
v-model.trim="password"
/>
</q-form> </q-card-section>
QCardActions
组件,其中定义了一个 align
属性为 right
。在内部,添加一个 QBtn
,label
属性设置为 "Create new Account"
,color
设置为 primary
,class
设置为 q-ml-sm
,flat
设置为 true
,并且 @click
事件监听器绑定到 createAccount
方法。接下来,创建另一个 QBtn
组件,label
属性设置为 "Login"
,type
设置为 "submit"
,color
设置为 primary
,并且 @click
事件监听器绑定到 onSubmit
方法:<q-card-actions align="right">
<q-btn
label="Create new account"
color="primary"
flat
class="q-ml-sm"
@click="createAccount"
/>
<q-btn
label="Login"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
QInnerLoading
组件,将 :showing
属性绑定到 computed.isLoading
。这将需要有一个 QSpinner
子组件,提供 size
属性。将其设置为 50px
,color
设置为 primary
:<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
> quasar dev
npm run lint --fix
,以自动修复任何代码 lint 错误。NameInput
,UsernameInput
,PasswordInput
和 EmailInput
。<script>
部分:在 **src/pages
** 文件夹中,创建一个名为 SignUp.vue
的新文件并打开它。
从 vuex
包中导入 mapActions
和 mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
export default
JavaScript 对象,提供五个属性:name
(定义为 'SignUp'
),components
,data
,computed
和 methods
:export default {
name: 'SignUp',
components: {}, data: () => ({ }),
computed: { },
methods: { }, };
components
属性中,添加四个新属性:NameInput
,UsernameInput
,PasswordInput
和 EmailInput
。像这样定义它们:NameInput
作为一个匿名函数,返回值为 import('components/NameInput')
UsernameInput
作为一个匿名函数,返回值为 import('components/UsernameInput')
PasswordInput
作为一个匿名函数,返回值为 import('components/PasswordInput')
EmailInput
作为一个匿名函数,返回值为 import('components/EmailInput')
components: {
PasswordInput: () => import('components/PasswordInput'),
EmailInput: () => import('components/EmailInput'),
UsernameInput: () => import('components/UsernameInput'),
NameInput: () => import('components/NameInput'), },
data
属性中,我们将返回一个提供四个属性的 JavaScript 对象 - name
,username
,email
和 password
- 其中所有属性都将是空字符串:data: () => ({
name: '',
username: '',
email: '',
password: '', }),
computed
属性中,我们将解构mapGetters
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递。对于第二个参数,我们将使用一个要导入的getters
数组,这种情况下是isLoading
:computed: {
...mapGetters('user', [
'isLoading',
]), },
methods
属性,首先,我们将解构mapActions
函数,将我们想要的模块的命名空间(在本例中为'user'
)作为第一个参数传递。对于第二个参数,我们将传递一个要导入的actions
数组,这种情况下是signUpNewUser
。接下来,我们需要添加异步的onSubmit
方法,它将分发signUpNewUser
,然后将用户发送到Validate
路由,以及onReset
方法,它将清除数据:methods: {
...mapActions('user', [
'signUpNewUser',
]),
async onSubmit() {
try {
await this.signUpNewUser({
name: this.name,
username: this.username,
email: this.email,
password: this.password,
});
await this.$router.replace({ name: 'Validate' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
onReset() {
this.email = '';
this.password = '';
}, },
<template>
部分<template>
部分:QPage
组件,class
属性定义为"bg-grey-1 flex flex-center"
:<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
QPage
组件内部,创建一个QCard
组件,style
属性定义为"width: 350px"
:<q-card style="width: 350px"> </q-card>
QCard
组件内部,创建一个QCardSection
,其中包含一个h6
子组件,其中class
属性定义为no-margin
:<q-card-section>
<h6 class="no-margin">Create a new Account</h6> </q-card-section>
QCardSection
,其中包含一个QForm
子组件,其中class
属性定义为q-gutter-md
。在QForm
组件内部,创建一个NameInput
组件,v-model
指令绑定到data.name
,一个UsernameInput
组件,v-model
指令绑定到data.username
,一个EmailInput
组件,v-model
指令绑定到data.email
,以及一个PasswordInput
组件,v-model
指令绑定到data.password
属性:<q-card-section>
<q-form class="q-gutter-md"
>
<name-input
v-model.trim="name"
/>
<username-input
v-model.trim="username"
/>
<email-input
v-model.trim="email"
/>
<password-input
v-model.trim="password"
/>
</q-form> </q-card-section>
QCardActions
组件,align
属性设置为right
。在内部,添加一个QBtn
,label
属性设置为"Reset"
,color
设置为primary
,class
设置为q-ml-sm
,flat
设置为true
,@click
事件监听器绑定到onReset
方法。然后,创建另一个QBtn
组件,label
属性设置为"Create"
,type
设置为"submit"
,color
设置为primary
,@click
事件监听器绑定到onSubmit
方法:<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
@click="onReset"
/>
<q-btn
label="Create"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
QInnerLoading
组件,:showing
属性绑定到computed.isLoading
。这将需要一个QSpinner
子组件。size
属性需要设置为50px
,color
需要设置为primary
:<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
> quasar dev
npm run lint --fix
,自动修复任何代码 lint 错误。<script>
部分<script>
部分:在src/pages
文件夹内,创建一个名为Validate.vue
的新文件并打开它。
从vuex
包中导入mapActions
和mapGetters
函数,以及从src/driver/auth
导入resendValidationCode
:
import { mapActions, mapGetters } from 'vuex';
import { resendValidationCode } from 'src/driver/auth';
export default
的 JavaScript 对象,提供四个属性:name
(定义为'Validate'
)、data
、computed
和methods
:export default {
name: 'Validate', data: () => ({ }),
computed: { },
methods: { }, };
data
属性内,我们将返回一个具有空字符串code
属性的 JavaScript 对象:data: () => ({
code: '', }),
computed
属性内,我们将解构mapGetters
函数,传递我们想要的模块的命名空间作为第一个参数,例如'user'
。对于第二个参数,我们将传递一个要导入的getters
数组,例如isLoading
和getUserEmail
:computed: {
...mapGetters('user', [
'isLoading',
'getUserEmail',
]), },
methods
属性中,我们将解构mapActions
函数,传递我们想要的模块的命名空间作为第一个参数,例如'user'
。对于第二个参数,我们将传递一个要导入的actions
数组,例如createNewUser
。接下来,我们需要添加异步的onSubmit
方法,它将分发createNewUser
并将用户发送到Index
路由;resendCode
方法,它将重新发送用户另一个验证代码;以及onReset
方法,它将重置数据:methods: {
...mapActions('user', [
'createNewUser',
]),
async onSubmit() {
try {
await this.createNewUser(this.code);
await this.$router.replace({ name: 'Index' });
} catch (e) {
console.error(e);
this.$q.dialog({
message: e.message,
});
}
},
async resendCode() {
await resendValidationCode(this.getUserEmail);
},
onReset() {
this.code = '';
}, },
<template>
部分<template>
部分:QPage
组件,定义class
属性为"bg-grey-1 flex flex-center"
:<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
QPage
组件内部,创建一个QCard
组件,定义style
属性为"width: 350px"
:<q-card style="width: 350px"> </q-card>
QCard
组件内,创建一个带有 h6
子组件和定义为 no-margin
的 class
属性的 QCardSection
。然后,创建一个兄弟元素,其 class
属性定义为 text-subtitle2
:<q-card-section>
<h6 class="no-margin">Validate new account</h6>
<div class="text-subtitle2">{{ getUserEmail }}</div> </q-card-section>
QCardSection
。这些将是 HTML 元素 P
:<q-card-section>
<p>A validation code were sent to you E-mail.</p>
<p>Please enter it to validate your new account.</p> </q-card-section>
QForm
子组件和定义为 q-gutter-md
的 class
属性的 QCardSection
。在 QForm
组件内,添加 QInput
组件作为子元素。然后,在 QInput
组件内,将 v-model
指令绑定到 data.code
。在 QInput
的 rules
属性内,将 rules
值定义为一个验证数组,用于检查是否已输入任何代码。启用 lazy-rules
,以便它只在一段时间后进行验证:<q-card-section>
<q-form
class="q-gutter-md"
>
<q-input
v-model.trim="code"
:rules="[ val => val && val.length > 0
|| 'Please type the validation code']"
outlined
label="Validation Code"
lazy-rules
/>
</q-form> </q-card-section>
align
属性设置为 right
的 QCardActions
组件。在内部,添加一个 label
属性设置为 "Reset"
、color
设置为 primary
、class
设置为 q-ml-sm
、flat
设置为 true
,并且 @click
事件监听器绑定到 onReset
方法的 QBtn
。创建另一个 label
属性设置为 "Re-send code"
、color
设置为 secondary
、class
设置为 q-ml-sm
、flat
设置为 true
,并且 @click
事件监听器绑定到 resendCode
方法的 QBtn
。最后,创建一个带有 label
属性设置为 "Validate"
、type
设置为 "submit"
、color
设置为 primary
,并且 @click
事件监听器绑定到 onSubmit
方法的 QBtn
组件:<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
/>
<q-btn
flat
label="Re-send code"
color="secondary"
class="q-ml-sm"
@click="resendCode"
/>
<q-btn
label="Validate"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
:showing
属性绑定到 computed.isLoading
的 QInnerLoading
组件。它应该有一个 size
设置为 50px
和 color
设置为 primary
的 QSpinner
子组件:<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
> quasar dev
npm run lint --fix
,自动修复任何代码 lint 错误。NameInput
、UsernameInput
、AvatarInput
和 PasswordInput
。<script>
部分:在 src/pages
文件夹内,创建一个名为 Edit.vue
的新文件并打开它。
从vuex
包中导入mapActions
和mapGetters
函数:
import { mapActions, mapGetters } from 'vuex';
export default
的 JavaScript 对象,提供四个属性:name
(定义为'SignUp'
)、data
、computed
和methods
:export default {
name: 'EditUser',
components: {}, data: () => ({ }),
created() {},
computed: { },
methods: { }, };
components
属性中,添加四个名为NameInput
、UsernameInput
、PasswordInput
、AvatarInput
的新属性。设置它们如下:NameInput
作为一个匿名函数,返回值为import('components/NameInput')
UsernameInput
作为一个匿名函数,返回值为import('components/UsernameInput')
PasswordInput
作为一个匿名函数,返回值为import('components/PasswordInput')
AvatarInput
作为一个匿名函数,返回值为import('components/AvatarInput')
:components: {
AvatarInput: () => import('/components/AvatarInput'),
PasswordInput: () => import('components/PasswordInput'),
UsernameInput: () => import('components/UsernameInput'),
NameInput: () => import('components/NameInput'), },
data
属性中,我们将返回一个提供五个属性的 JavaScript 对象:name
、username
、avatar
、email
和password
。所有这些都将是空字符串:data: () => ({
name: '',
username: '',
avatar: '',
password: '',
newPassword: '', }),
created
生命周期钩子中,将data.name
定义为getUser.name
,data.username
定义为getUser.username
,以及data.avatar
定义为getUser.avatar
:created() {
this.name = this.getUser.name;
this.username = this.getUser.username;
this.avatar = this.getUser.avatar; },
computed
属性中,我们将解构mapGetters
函数,传递我们想要的模块的命名空间作为第一个参数,这里是'user'
。对于第二个参数,我们将传递一个要导入的getters
数组,这种情况下是isLoading
:computed: {
...mapGetters('user', [
'getUser',
'isLoading',
]), },
methods
属性中,我们将解构mapActions
函数,传递我们想要的模块的命名空间作为第一个参数,这里是'user'
。对于第二个参数,我们将传递一个要导入的actions
数组,这种情况下是editUser
。接下来,我们需要添加异步的onSubmit
方法,它将调度$refs.avatar.uploadFile()
,然后调度editUser
发送用户到Chat
路由,以及onReset
方法,它将清除数据:methods: {
...mapActions('user', [
'editUser',
]),
async onSubmit() {
try {
await this.$refs.avatar.uploadFile(); await this.editUser({
name: this.name,
avatar: this.$refs.avatar.s3file,
username: this.username,
password: this.password,
newPassword: this.newPassword,
}); await this.$router.replace({ name: 'Contacts' });
} catch (e) {
this.$q.dialog({
message: e.message,
});
}
},
onReset() {
this.name = this.getUser.name;
this.username = this.getUser.username; this.password = '';
this.newPassword = '';
}, },
<template>
部分:class
属性定义为"bg-grey-1 flex flex-center"
的QPage
组件:<q-page padding class="bg-grey-1 flex flex-center">
</q-page>
QPage
组件内部,创建一个带有style
属性定义为"width: 350px"
的QCard
组件:<q-card style="width: 350px"> </q-card>
QCard
组件内部,创建一个带有h6
子组件的QCardSection
,并且class
属性定义为no-margin
:<q-card-section>
<h6 class="no-margin">Edit user account</h6> </q-card-section>
class
属性定义为q-gutter-md
的QCardSection
,其中包含一个QForm
子组件。在QForm
组件内部,创建一个AvatarInput
组件,其中reference
指令定义为avatar
,v-model
指令绑定到data.avatar
,一个NameInput
组件,其中v-model
指令绑定到data.name
,一个UsernameInput
组件,其中v-model
指令绑定到data.username
,一个EmailInput
组件,其中v-model
指令绑定到data.email
,以及一个PasswordInput
组件,其中v-model
指令绑定到data.password
属性:<q-card-section>
<q-form
class="q-gutter-md"
>
<avatar-input
v-model="avatar"
ref="avatar"
/>
<name-input
v-model.trim="name"
/>
<username-input
v-model.trim="username"
/>
<q-separator/>
<password-input
v-model.trim="password"
label="Your old password"
/>
<password-input
v-model.trim="newPassword"
label="Your new password"
/>
</q-form> </q-card-section>
align
属性设置为right
的QCardActions
组件。在内部,添加一个label
属性设置为"Reset"
,color
设置为primary
,class
设置为q-ml-sm
,flat
设置为true
,并且@click
事件监听器绑定到onReset
方法的QBtn
。然后,创建另一个QBtn
组件,其中label
属性设置为"Create"
,type
设置为"submit"
,color
设置为primary
,并且@click
事件监听器绑定到onSubmit
方法:<q-card-actions align="right">
<q-btn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
@click="onReset"
/>
<q-btn
label="Update"
type="submit"
color="primary"
@click="onSubmit"
/> </q-card-actions>
QInnerLoading
组件,其中:showing
属性绑定到computed.isLoading
。它应该有一个QSpinner
子组件,size
设置为50px
,color
设置为primary
:<q-inner-loading :showing="isLoading">
<q-spinner size="50px" color="primary"/> </q-inner-loading>
> quasar dev
npm run lint --fix
,以自动修复任何代码 lint 错误。打开router
文件夹内的routes.js
文件。
将routes
常量设置为空数组:
const routes = [];
path
、component
和children
的 JavaScript 对象。path
属性是一个字符串,将是一个静态 URL,component
属性是一个匿名函数,将返回一个带有将要渲染的组件的 WebPackimport
函数,children
属性是一个将在path
内渲染的组件数组。每个子组件都是一个具有相同属性的 JavaScript 对象,另外还有一个叫做name
的新属性。{
path: '/',
component: () => import('layouts/Base.vue'),
children: [
{
path: '',
name: 'Index',
meta: {
authenticated: false,
},
component: () => import('pages/Index.vue'),
},
], },
/chat
URL,我们需要在pages
文件夹内创建两个新的占位符页面:Contacts.vue
和Messages.vue
。在这些文件内,创建一个带有以下模板的空组件:<template>
<div />
</template>
<script>
export default {
name: 'PlaceholderPage',
};
</script>
message
路由内,我们需要添加两个特殊参数::id
和path
。这些参数将用于在用户之间获取特定的消息。{
path: '/chat',
component: () => import('layouts/Chat.vue'),
children: [
{
path: 'contacts',
name: 'Contacts',
component: () => import('pages/Contacts.vue'),
},
{
path: 'messages/:id/:name',
name: 'Messages',
meta: {
authenticated: true,
goBack: {
name: 'Contacts',
},
},
component: () => import('pages/Messages.vue'),
},
], },
/user
URL,我们将只创建一个子路由,即edit
路由。在这个路由内,我们使用alias
属性,因为vue-router
需要有一个path
为空的子路由进行首次子路由渲染。我们还将在我们的应用程序内有一个/user/edit
路由可用。{
path: '/user',
component: () => import('layouts/Chat.vue'),
children: [
{
path: '',
alias: 'edit',
name: 'Edit',
meta: {
authenticated: true,
goBack: {
name: 'Contacts',
},
},
component: () => import('pages/Edit.vue'),
},
], },
/register
URL,其中包括两个子路由:SignUp
和Validate
。SignUp
路由将是注册 URL 上的主要路由,并且当用户进入此 URL 时将直接调用。Validate
路由只有在用户被重定向到/register/validate
URL 时才会被调用。{
path: '/register',
component: () => import('layouts/Base.vue'),
children: [
{
path: '',
alias: 'sign-up',
name: 'SignUp',
meta: {
authenticated: false,
},
component: () => import('pages/SignUp.vue'),
},
{
path: 'validate',
name: 'Validate',
meta: {
authenticated: false,
},
component: () => import('pages/Validate.vue'),
},
], },
在src/boot
文件夹内创建一个名为routeGuard.js
的新文件。
创建一个默认的导出异步函数。在这个参数内,添加一个名为app
的 JavaScript 对象属性。在函数内部,创建一个常量,使用app
的对象重构获取store
属性。然后,创建一个try/catch
块。在try
部分,检查'user/getUserId'
是否存在,如果不存在则调度'user/initialLogin'
。最后,在catch
块内,将用户重定向到Index
路由。
export default async ({ app }) => {
const { store } = app; try {
if (!store.getters['user/getUserId']) {
await store.dispatch('user/initialLogin');
}
} catch {
await app.router.replace({ name: 'Index' });
} };
quasar.conf.js
文件,并找到boot
属性。将'routerGuard'
项添加到数组中。boot: [
'amplify',
'axios',
'routeGuard', ],
NameInput
,EmailInput
等,以简化开发宏组件或容器(如页面)的过程。vue-router
来管理使用自定义布局包装页面的父子过程,我们使用了本书先前配方中创建的布局来为我们的应用程序创建路由。我们使它们可用,以便我们可以像正常的 Web 应用程序一样访问应用程序,具有自定义 URL 和路由。> quasar dev
npm run lint --fix
,以自动修复任何代码 lint 错误。您可以在router.vuejs.org/guide/essentials/nested-routes.html
找到有关vue-router
嵌套路由的更多信息。
您可以在router.vuejs.org/guide/advanced/lazy-loading.html
找到有关vue-router
懒加载的更多信息。
你可以在quasar.dev/vue-components/inner-loading
找到有关 Quasar 框架的QInnerLoading
组件的更多信息。
创建 GraphQL 查询和片段
在您的应用程序上创建 Chat Vuex 模块
创建应用程序的联系人页面
创建应用程序的消息页面
windows-build-tools
的npm
包,以便能够安装所需的包。要执行此操作,请以管理员身份打开 PowerShell 并执行以下命令:> npm install -g windows-build-tools
> npm install -g @quasar/cli
> npm install -g @aws-amplify/cli
在第五章的食谱为您的应用程序创建用户页面和路由中的项目,创建用户 Vuex 模块、页面和路由
Node.js 12+
@aws-amplify/cli
@quasar/cli
在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 } } `;
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 }
} `;
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 } } `;
const createConversationLink = graphql`mutation CreateConversationLink(
$conversationLinkConversationId: ID!,
$conversationLinkUserId: ID ) { createConversationLink(input: {
conversationLinkConversationId: $conversationLinkConversationId,
conversationLinkUserId: $conversationLinkUserId
}) { id conversationLinkUserId conversationLinkConversationId conversation { id name }
} } `;
export {
getUser,
listUsers,
getUserAndConversations,
getConversation,
createMessage,
createConversation,
createConversationLink, };
在 store/user 文件夹中打开 actions.js 文件。
在import
部分,我们将从 src/graphql/queries 替换 getUser 和 listUsers 为新创建的 src/graphql/fragments。
import { listUsers, getUser } from 'src/graphql/fragments';
在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
找到有关模板文字标签函数的更多信息。
在graphql.org/learn/queries/
找到有关 GraphQL 查询、变异和片段的更多信息。
来自上一个教程的项目
Node.js 12+
@aws-amplify/cli
@quasar/cli
在 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,
}; }
createState
函数的执行导出为默认值:export default createState();
在 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; }
将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, };
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;
getConversations
,getChatMessages
,isLoading
和hasError
:export default {
getConversations,
getChatMessages,
isLoading,
hasError, };
在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_MESSAGES
mutation 添加到状态中,并返回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, };
在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';
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';
modules
的新属性,并将导入的用户文件添加到此属性中:export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
user,
chat,
},
strict: process.env.DEV,
}); return Store; }
在vuex.vuejs.org/api/#getters
找到有关 Vuex getters API 的更多信息。
在vuex.vuejs.org/guide/getters.html#method-style-access
找到有关 Vuex getters 方法数据访问的更多信息。
来自上一个示例的项目
Node.js 12+
@aws-amplify/cli
@quasar/cli
<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
属性中,首先,我们将从用户模块中解构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();
}
}, },
NewConversation
组件创建<template>
部分:value
属性定义为value
的QDialog
组件。还创建一个事件监听器input
,定义为$emit
函数,发送带有$event
作为数据的'input'
事件:<q-dialog
:value="value"
@input="$emit('input', $event)" ></q-dialog>
QDialog
组件内部,创建一个带有style
属性定义为min-width: 400px; min-height: 100px;
的QCard
组件。在QCard
组件内部,创建两个QCardSection
子组件。在第一个组件中,添加一个class
属性定义为row items-center q-pb-none
:<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
组件上,添加一个带有class
属性定义为text-h6
的div
,并将内部 HTML 定义为New Conversation
。然后添加一个QSpace
组件。最后,添加一个带有icon
属性定义为close
的QBtn
,并将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
组件内部,创建一个带有avatar
属性定义为true
的QItemSection
组件。然后创建一个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
。在其中添加一个name
属性设置为add_comment
,color
设置为green
的QIcon
组件:<q-item-section side>
<q-icon name="add_comment" color="green"/> </q-item-section>
QList
组件的同级元素,创建一个带有showing
属性定义为pending
的QInnerLoading
组件。在其中添加一个size
属性设置为50px
,color
属性定义为primary
的QSpinner
组件:<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
混合的数组。在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
组件内部,创建一个带有v-for
指令的QItem
组件,迭代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>
NewConversation
组件的<template>
部分与联系人页面的<template>
部分之间的相似之处是有意为之,这样用户在创建新对话和查看当前联系人列表时有相同的体验。在quasar.dev/vue-components/button
找到有关 Quasar QBtn
组件的更多信息。
在quasar.dev/vue-components/dialog
找到有关 Quasar QDialog
组件的更多信息。
在quasar.dev/vue-components/inner-loading
找到有关 Quasar QInnerLoading
组件的更多信息。
在quasar.dev/vue-components/spinners
找到有关 Quasar QSpinners
的更多信息。
在quasar.dev/layout/page-sticky
找到有关 Quasar QPageSticky
组件的更多信息。
在quasar.dev/vue-directives/close-popup
找到有关 Quasar ClosePopup
指令的更多信息。
在vuejs.org/v2/guide/mixins.html
找到有关 Vue mixins 的更多信息。
ChatInput
组件和消息布局。来自上一个配方的项目
Node.js 12+
@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
组件内部,创建一个带有v-slot
指令的Template
组件,名称为after
。然后创建一个子QBtn
组件,属性为round
和flat
定义为true
,然后icon
定义为"send"
。最后,在@click
事件上添加事件监听器,执行sendMessage
函数:<template v-slot:after>
<q-btn
round
flat icon="send"
@click="sendMessage"
/> </template>
<script>
部分:在layouts
文件夹中创建一个名为Messages.vue
的新文件。
从src/driver/auth.js
文件中导入signOut
函数和components/ChatInput
中的ChatInput
组件:
import {signOut,} from 'src/driver/auth';
import ChatInput from '../components/ChatInput';
name
属性定义为"ChatLayout"
的default
JavaScript 对象,具有components
属性和另一个名为methods
的属性:export default {
name: 'MessagesLayout',
components: {},
methods: { }, };
components
属性中,添加导入的ChatInput
组件:components: { ChatInput },
methods
属性中,添加一个名为logOff
的新的异步函数。在这个函数中,我们将执行signOut
函数,并在其后重新加载浏览器:methods: {
async logOff() {
await signOut();
window.location.reload();
},
}
<template>
部分<template>
部分:view
属性定义为"hHh lpR fFf"
的QLayout
组件:<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
组件之前,我们将添加一个dense
,flat
和round
属性被定义为true
的QBtn
组件。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
组件的同级,创建一个带有RouterView
组件作为直接子元素的QPageContainer
组件:<q-page-container>
<router-view /> </q-page-container>
class
属性定义为bg-white
的QFooter
组件。添加一个子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
路由之后,创建一个新的 JavaScript 对象,具有path
,component
和children
属性。将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'),
},
], },
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
属性。将name
属性定义为MessagesPage
。在components
属性中,添加一个新的属性,AvatarDisplay
,它将接收一个返回导入组件的匿名函数。最后,在data
属性上,创建一个带有值为null
的interval
属性的对象:export default {
name: 'MessagesPage',
components: {
AvatarDisplay: () => import('components/AvatarDisplay'),
},
data: () => ({
interval: null,
}),
async beforeMount() {},
beforeDestroy() {},
watch: {},
computed: {},
methods: {}, };
computed
属性上,首先,我们将解构mapGetters
函数,将user
模块作为第一个参数传递,getUser
作为第二个参数。然后我们将对 chat 模块做同样的操作,获取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; },
创建一个带有class
属性定义为q-pa-md row justify-center
的QPage
组件,并将QChatMessage
组件作为子组件添加。
在QChatMessage
子组件中,首先在v-for
指令上对currentMessages
进行迭代。
将ref
和key
组件属性定义为message.id
,stamp
定义为message.createdAt
,text
定义为[message.content]
。
然后将sent
属性定义为评估message.authorId
是否与getUser.id
相同,name
定义为message.author.name
,avatar
定义为传入message.author.avatar
和message.author.name
作为参数的getAvatar
方法。
然后,将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 模块直接发送消息,无需通过页面或布局等容器,使组件具有状态。在quasar.dev/vue-components/chat
找到有关 Quasar Framework 的QChatMessage
组件的更多信息。
在quasar.dev/quasar-utils/date-utils
找到有关 Quasar Framework 的date
工具的更多信息。
将应用程序转换为 PWA
创建应用程序更新通知
在 iOS 上添加自定义 PWA 安装通知
创建生产环境和部署
windows-build-tools
的npm
包,以便能够安装所需的软件包。要执行此操作,请以管理员身份打开 PowerShell 并执行以下命令:> npm install -g windows-build-tools
> npm install -g @quasar/cli
> npm install -g @aws-amplify/cli
@aws-amplify/cli
@quasar/cli
> quasar m add pwa
quasar.conf.js
文件,并找到pwa
属性。从 JavaScript 对象中删除workboxPluginMode
和workboxOptions
属性,并添加cleanupOutdatedCaches
,skipWaiting
和clientsClaim
属性,定义为true
。最后,在manifest
属性上,更改name
,short_name
和description
以匹配您的应用程序,如下面的代码所示:pwa: {
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
manifest: {
name: 'Chat Application',
short_name: 'Chat App',
description: 'Quasar & AWS Amplify Chat Application',
...
},
...
}
> amplify configure project
? Enter a name for the project (chatapp)
Visual Studio Code
(或者您将在项目中使用的默认编辑器),然后按Enter继续:? Choose your default editor: (Use arrow keys)
❯ Visual Studio Code
Atom Editor
Sublime Text
IntelliJ IDEA
Vim (via Terminal, Mac OS only)
Emacs (via Terminal, Mac OS only)
None
javascript
选项,然后按Enter继续:? Choose the type of app that you're building (Use arrow keys)
android
ios
❯ javascript
none
,然后按Enter继续:Please tell us about your project
? What javascript framework are you using (Use arrow keys)
angular
ember
ionic
react
react-native
vue
❯ none
? Source Directory Path: (src)
dist/pwa
,然后按Enter继续:? Distribution Directory Path: dist/pwa
Build Command
选项;更改命令为quasar build -m pwa
,然后按Enter继续:? Build Command: quasar build -m pwa
Start Command
选项;更改为quasar dev -m pwa
,然后按Enter继续:? Start Command: quasar dev -m pwa
update
,然后按Enter继续:Using default provider awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
For the awscloudformation provider.
? Do you want to update or remove the project level configuration (Use arrow keys)
❯ update
remove
cancel
? Do you want to use an AWS profile? Y
? Please choose the profile you want to use (Use arrow keys)
❯ default
quasar -m add
命令来添加新的开发环境。quasar.conf.js
文件,以在pwa
属性上添加新属性,以便我们可以在部署的应用程序中添加更好的用户体验。pwa
环境作为构建命令和分发文件夹。在quasar.dev/quasar-cli/developing-pwa/introduction
找到有关使用 Quasar 开发 PWA 的更多信息。
在docs.amplify.aws/cli
找到有关 Amplify CLI 的更多信息。
@aws-amplify/cli
@quasar/cli
quasar.conf.js
文件,找到framework
属性。然后对于plugins
属性,将'Notify'
字符串添加到数组中,以便 Quasar 在应用程序启动时加载Notify
插件:framework: {
...
plugins: [
'Dialog',
'Notify',
],
...
},
src-pwa
文件夹中的register-service-worker.js
文件,并从 Quasar 导入Notify
插件:import { Notify } from 'quasar';
clearLocalCache
的异步函数。然后创建一个名为cachedFiles
的常量,并将其定义为await caches.keys()
;在cachedFiles
常量上,使用参数file
执行一个数组map
函数;在函数内部执行await caches.delete(file)
。最后,重新加载应用程序:async function clearLocalCache() {
const cachedFiles = await caches.keys(); await cachedFiles.map(async (file) => {
await caches.delete(file);
}); window.location.reload(); }
updatefound
函数,并创建一个名为installKey
的常量,并将其定义为'chatAppInstalled'
。然后验证浏览器的localStorage
项中是否有与您创建的常量名称相同的项。如果存在该项,则执行Notify.create
函数,并将 JavaScript 对象作为参数传递,其中color
属性定义为'dark'
,message
定义为更新消息。如果localStorage
项不存在,则向localStorage
添加一个名为installKey
常量的项,其值为'1'
:updatefound(/* registration */) {
const installKey = 'chatAppInstalled';
if (localStorage.getItem(installKey)) {
Notify.create({
color: 'dark',
message: 'An update is being downloaded from the server.',
});
} else {
localStorage.setItem(installKey, '1');
}
},
updated
函数,并添加一个Notify.create
函数,将 JavaScript 对象作为参数传递。在此对象中,添加一个type
属性,定义为'positive'
,一个message
属性,定义为成功更新的消息,一个带有刷新应用程序指示的caption
属性,以及一个定义为数组的actions
属性。在actions
数组中,添加一个 JavaScript 对象,其中label
属性定义为'Refresh'
,color
属性定义为'white'
,handler
属性定义为clearLocalCache
函数:updated(/* registration */) {
Notify.create({
type: 'positive',
message: 'The application was updated successfully!',
caption: 'Please refresh the page to apply the new update.',
actions: [
{ label: 'Refresh',
color: 'white',
handler: clearLocalCache,
},
],
}); },
Notify
插件添加到quasar.conf.js
文件的插件属性中,这样 Quasar CLI 就可以在执行运行时为我们提供它。register-service-worker.js
文件中,我们添加了 Notify 插件并创建了一个自定义的缓存清除函数。updatefound
生命周期中,我们添加了安装验证,以便新的更新通知只会显示给已在其浏览器上安装了应用程序的用户。在quasar.dev/quasar-plugins/notify
找到有关 Quasar Notify 插件的更多信息。
在developer.mozilla.org/en-US/docs/Web/API/Cache
找到有关 JavaScript 缓存接口的更多信息。
a2hs.js
(用于添加到主屏幕)的社区插件,我们可以为 iOS 用户启用自定义安装消息的显示。a2hs.js
插件,并如何使用 Quasar 引导文件将其添加到项目引导序列中。@aws-amplify/cli
@quasar/cli
a2h2.js
a2hs.js
插件以添加此缺失的功能:a2js.js
插件。要做到这一点,在项目文件夹中,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:> npm install --save a2hs.js
boot
文件夹中,然后在src
文件夹内,创建一个a2hs.js
文件并打开它。接下来,导入a2hs.js
插件:import AddToHomeScreen from 'a2hs.js';
options
的常量,并将其定义为一个 JavaScript 对象,其中brandName
属性作为应用程序的名称:export default() => {
const options = {
brandName: 'Chat App', };
AddToHomeScreen(options); };
quasar.conf.js
文件并找到boot
属性。在数组中,添加'a2hs'
字符串,以使其对 Quasar CLI 可用,并加载新创建的引导文件:boot: [
'amplify',
'axios',
'a2hs', ],
npm
安装了a2hs.js
插件到项目中。然后,我们在 Quasar 上创建了一个a2hs.js
文件,用作引导文件。a2hs.js
插件和应用程序 logo,然后实例化了具有自定义选项的a2hs.js
插件。a2hs
引导文件添加到quasar.conf.js
文件的boot
属性中。您可以在quasar.dev/quasar-cli/boot-files
找到有关 Quasar 引导文件结构的更多信息。
您可以在github.com/koddr/a2hs.js/
找到有关a2hs.js
的更多信息。
@aws-amplify/cli
@quasar/cli
> amplify env add
? Do you want to use an existing environment? (Y/n) n
production
作为名称输入并按Enter继续:? Enter a name for the environment production production
? Do you want to use an AWS profile? (Y/n) y
? Please choose the profile you want to use (Use arrow keys)
❯ default
> amplify push
? Do you want to update code for your updated GraphQL API (Y/n) y
? Do you want to generate GraphQL statements (queries, mutations and
subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions (Y/n) y
> amplify publish
production
环境。为此,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),进入项目文件夹,并执行以下命令:> amplify console
在docs.amplify.aws/cli
上查找有关 Amplify CLI 的更多信息。
在aws.amazon.com/amplify/console/?nc1=h_ls
上查找有关 Amplify 控制台的更多信息。