Vue2-Bootstrap4-Web-开发-全-
Vue2 Bootstrap4 Web 开发(全)
原文:
zh.annas-archive.org/md5/7E556BCDBA065D692175F778ABE043D8
译者:飞龙
第一章:请介绍你自己 - 教程
你好,用户
你好亲爱的读者,我的名字是 Olga。你也想介绍一下自己吗?打开pleaseintroduceyourself.xyz/
给我和其他读者留言。
页面本身看起来并不特别。它只是一个允许用户写消息的网页,然后,这条消息立即与其他用户的消息一起以倒序显示:
请介绍你自己页面
你想知道我创建这个页面花了多长时间吗?大约花了我半个小时,我不仅指的是编写 HTML 标记或者倒序排列消息,还包括数据库设置、部署和托管。
你可能注意到第一条消息从未改变,实际上这是我的消息,我写道我喜欢学习和教学。这是真的。这就是为什么我会把这一章节专门用来教你如何在短短 15 分钟内创建完全相同的页面。你准备好了吗?让我们开始吧!
在 Firebase 控制台中创建一个项目
如果你还没有谷歌账号,但是你真的想继续这个教程,那么,很抱歉,你这次必须创建一个。Firebase 是由谷歌提供的服务,所以谷歌账号是必须的。
如果你已经有账号,请登录 Firebase 控制台:
让我们开始创建你的新 Firebase 项目。点击添加项目按钮。给它一个有意义的名字,并从列表中选择你的国家。完成后,点击创建项目:
使用 Firebase 控制台创建一个项目
完成了!现在,你可以使用 Firebase 后端为你的应用程序提供支持,包括实时数据库、认证机制、托管和分析。
向 Firebase 应用程序数据库添加第一个条目
让我们添加第一个数据库条目。点击左侧的数据库选项卡。你应该看到类似这样的仪表板:
Firebase 项目仪表板上的实时数据库
通过点击加号来添加一个名为messages
的条目,以及作为键值对象包含title
、text
和timestamp
的第一条消息:
向 Firebase 实时数据库添加第一个值
点击添加按钮,您的数据库将保留添加的条目。添加尽可能多的消息条目,或者保持原样。现在,为了简单起见,让我们改变我们数据库的规则,使其对每个人都可读可写。注意!不要为公共使用的生产环境做这个操作。在这个例子中,我们只是想测试一些 Firebase 功能,但是您未来的应用程序必须是智能和安全的。点击规则选项卡,在打开的文本区域中输入以下规则:
{
"rules": {
".read": true,
".write": true
}
}
因此,您的规则选项卡现在看起来是这样的:
更改规则后的规则选项卡
点击发布按钮,您就完成了!现在,开始在我们的应用程序中使用这些数据将会很有趣。但是,首先我们必须创建这个应用程序,并将其连接到我们的项目中。
搭建 Vue.js 应用程序
在本节中,我们将创建一个Vue.js应用程序,并将其连接到我们在上一步中创建的 Firebase 项目。确保您的系统上已安装Node.js。
您还必须安装 Vue.js。请查看官方 Vue 文档的说明页面vuejs.org/v2/guide/installation.html
。或者,只需运行npm install
命令:
**$ npm install -g vue-cli**
现在,一切都准备好开始搭建我们的应用程序了。转到您希望应用程序驻留的文件夹,并输入以下代码行:
**vue init webpack please-introduce-yourself**
它会问您几个问题。只需选择默认答案,然后对每个问题按Enter。初始化后,您就可以安装和运行您的应用程序了:
**cd please-introduce-yourself**
**npm install**
**npm run dev**
如果一切正常,以下页面将自动在您的默认浏览器中打开:
安装和运行后的默认 Vue.js 应用程序
如果不是,请再次检查 Vue.js 官方安装页面。
将 Vue.js 应用程序连接到 Firebase 项目
要能够将您的应用程序连接到 Firebase 项目,您必须安装Firebase和VueFire。在您的新应用程序的根目录中运行npm install
命令:
**cd please-introduce-yourself**
**npm install firebase vuefire --save**
现在,您可以在应用程序内部使用 Firebase 强大的功能。让我们检查一下是否成功!我们只需执行以下操作:
-
导入 Firebase
-
创建一个包含 Firebase 应用程序 ID、项目域、数据库域和连接到我们项目所需的其他一些内容的
config
对象 -
编写将使用 Firebase API 和创建的
config
文件连接到 Firebase 项目的代码。 -
使用它
我们从哪里获取配置我们的 Firebase 实例所需的信息?转到 Firebase 控制台,单击概述选项卡右侧的齿轮图标,然后选择项目设置。现在,单击将 Firebase 添加到您的网络应用按钮:
单击将 Firebase 添加到您的网络应用按钮
将打开一个包含我们所需所有信息的弹出窗口:
所需的配置对象的所有信息都在这里
好的,现在,只需保留此弹出窗口打开,转到您的 Vue 应用程序,并打开位于应用程序的src
目录中的main.js
文件。在这里,我们需要告诉我们的 Vue 应用程序它将使用 VueFire。这样,我们就能在应用程序内部使用 Firebase 提供的所有功能。将以下行添加到main.js
文件的导入部分:
//main.js
import VueFire from 'vuefire'
**Vue.use(VueFire)**
太棒了!现在,打开App.vue
文件。在这里,我们将导入 Firebase 并在 Vue 应用程序内初始化我们的 Firebase 应用程序。在<script>
标签内添加以下代码行:
//App.vue
<script>
import Firebase from 'firebase'
let config = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
databaseURL: 'YOUR_DATABASE_URL',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_STORAGE_BUCKET',
messagingSenderId: 'YOUR_MESSAGING_SENDER_ID'
}
let app = Firebase.initializeApp(config)
</script>
从我们在上一步中打开的弹出窗口中复制config
对象信息所需的内容。
现在,我们将获取到我们的消息数据库对象的引用。使用 Firebase API 非常简单:
//App.vue
<script>
<...>
**let db = app.database()**
**let messagesRef = db.ref('messages')**
</script>
我们快完成了。现在,我们只需在 Vue 数据对象中导出messages
对象,以便我们能够在模板部分内使用它。因此,在export
部分内,添加一个带有firebase
键的条目,并将messages
指向messagesRef
:
export default {
firebase: {
messages: messagesRef
},
}
现在,在<template>
标签内,我们将使用v-for
指令来遍历messages
数组并打印有关每条消息的所有信息。请记住,每条消息由title
、text
和timestamp
组成。因此,请在模板中添加以下<div>
:
//App.vue
<div v-for="message in messages">
<h4>{{ message.title }}</h4>
<p>{{ message.text }}</p>
<p>{{ message.timestamp }}</p>
</div>
最后,您的App.vue
组件将如下所示:
//App.vue
<template>
<div id="app">
<div v-for="message in messages">
<h4>{{ message.title }}</h4>
<p>{{ message.text }}</p>
<p>{{ message.timestamp }}</p>
</div>
</div>
</template>
<script>
import Firebase from 'firebase'
let config = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
databaseURL: 'YOUR_DATABASE_URL',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_STORAGE_BUCKET',
messagingSenderId: 'YOUR_MESSAGING_SENDER_ID'
}
let app = Firebase.initializeApp(config)
let db = app.database()
let messagesRef = db.ref('messages')
export default {
name: 'app',
firebase: {
messages: messagesRef
}
}
</script>
如果您在应用程序初始化时选择了默认的代码检查设置,那么您从 Firebase 复制并粘贴到应用程序中的代码将无法通过代码检查。这是因为 Vue-cli 初始化的默认代码检查设置要求使用单引号,并且行尾不使用分号。顺便说一下,Evan You特别自豪于这个不使用分号的规则。所以,请让他高兴一下;从复制的代码中删除所有分号,并将双引号替换为单引号。
您难道不好奇去查看页面吗?如果您还没有运行应用程序,请切换到应用程序文件夹并运行它:
**cd please-introduce-yourself**
**npm run dev**
我非常确定您看到了以下截图:
显示来自 Firebase 数据库的 Vue.js Web 应用程序信息
恭喜!您已成功完成我们教程的第一部分,将 Vue.js 应用程序连接到 Firebase 实时数据库。
添加基于 Bootstrap 的标记
让我们通过添加 Bootstrap 并使用其类来为我们的应用程序添加基本样式。
首先,让我们从 Bootstrap 的 CDN 中包含 Bootstrap 的CSS
和JS
文件。我们将使用即将推出的版本 4,目前还处于 alpha 版。打开index.html
文件,在<head>
部分添加必要的link
和script
标签:
//index.html
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.min.js"crossorigin="anonymous"></script>
<script src="https://npmcdn.com/tether@1.2.4/dist/js/tether.min.js">
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"crossorigin="anonymous">
</script>
您可能已经注意到我还添加了jQuery和Tether依赖项;这是因为 Bootstrap 依赖于它们。
现在,我们可以在应用程序中使用 Bootstrap 类和组件。让我们从使用 Bootstrap 的类开始添加一些样式。
我将整个应用程序div
标签包装到jumbotron
类中,然后将其内容包装到container
类中。因此,模板将会有些不同:
//App.vue
<template>
<div id="app" class="**jumbotron**">
<div class="**container**">
<h1>Hello! Nice to meet you!</h1>
<hr />
<div v-for="message in messages">
<...>
</div>
</div>
</div>
</template>
查看页面;它看起来不一样吗?现在,我想将每条消息的内容包装在card
类中。卡片似乎是这种东西的适当容器。查看官方 Bootstrap 关于卡片的文档v4-alpha.getbootstrap.com/components/card/
。我将添加一个带有card-group
类的div
标签,并将所有带有消息的卡片放在这个容器中。因此,我不需要担心定位和布局。一切都会自动变得响应!因此,消息的标记将如下所示:
//App.vue
<template>
<...>
<div class="**card-group**">
<div class="card" v-for="message in messages">
<div class="**card-block**">
<h5 class="**card-title**">{{ message.title }}</h5>
<p class="**card-text**">{{ message.text }}</p>
<p class="**card-text**"><small class="text-muted">Added on {{ message.timestamp }}</small></p>
</div>
</div>
</div>
</template>
查看页面。它看起来几乎很好!在几个步骤中,我们能够很好地显示存储在我们 Firebase 数据库中的消息。尝试使用 Firebase 实时数据库仪表板添加另一条消息。保持网页打开!填写 Firebase 数据库字段:
向 Firebase 数据库添加条目
现在,点击“添加”按钮。新消息会自动出现在您的网页上:
一旦我们点击“添加”按钮,新消息立即出现在我们的网页上
这不是很棒吗?现在,我们可以添加任意多的消息。我们还可以删除它们并操纵它们,所有的更改都会自动传播到我们的网页上。这很不错,但我们真的想继续在后端数据库上玩耍,看到网页上的变化吗?当然不!我们希望我们网页的用户能够使用我们的页面而不是我们的数据库仪表板来添加他们的消息。让我们回到我们的 Vue.js 应用程序,并添加一个表单,让我们能够添加新的消息。
使用 Bootstrap 添加表单
让我们向我们的应用程序添加一个简单的表单,这样我们就可以向我们的留言板添加新消息。查看 Bootstrap 关于表单的文档v4-alpha.getbootstrap.com/components/forms/
。
让我们在消息列表之前添加一个表单。这个表单将包含标题的输入,消息的文本区域和提交按钮。它将看起来像这样:
//App.vue
<template>
<div id="app" class="jumbotron">
<div class="container">
<h1>Hello! Nice to meet you!</h1>
<hr />
<form>
<div>
**<input maxlength="40" autofocus placeholder="Please introduce yourself :)" />**
</div>
<div>
**<textarea placeholder="Leave your message!" rows="3">**
**</textarea>**
</div>
**<button type="submit">Send</button>**
</form>
<hr />
<...>
</div>
</div>
</template>
看看页面。看起来不太美观,是吗?
我们的表单看起来不太美观。
实际上,让我们诚实一点,它看起来很丑!但是,使用 Bootstrap 类,修复它真的很容易。如果我们将form-control
类添加到input
和textarea
元素中,将form-group
类添加到围绕这些元素的每个div
标签中,可能还将btn btn-primary
类添加到submit
按钮中…嗯,我们会得到更好的东西!
表格看起来真的很漂亮,使用了 Bootstrap 类
好的,现在我们有一个看起来不错的表单,但是如果我们尝试填写它,什么也不会发生。我们必须使其功能化,为此,我们将使用 Vue.js 的强大功能。
使用 Vue.js 使事情功能化
那么,我们想要通过我们的表单实现什么?我们希望创建新消息。这条消息必须由标题、文本和时间戳组成。我们还希望将此消息添加到我们的消息引用数组中。
让我们将这条新消息称为newMessage
,并将其添加到App.vue
的data
属性中:
//App.vue
<script>
<...>
export default {
data () {
return {
**newMessage: {**
**title: '',**
**text: '',**
**timestamp: null**
**}**
}
},
<...>
}
</script>
现在,让我们将newMessage
对象的标题和文本绑定到表单的input
和textarea
上。我们还将一个名为addMessage
的方法绑定到表单的提交处理程序,使整个表单的标记看起来像这样:
<template>
<...>
<form **@submit="addMessage"**>
<div class="form-group">
<input class="form-control"**v-model="newMessage.title"**maxlength="40"autofocus placeholder="Please introduce yourself :)" />
</div>
<div class="form-group">
<textarea class="form-control"**v-model="newMessage.text"** placeholder="Leave your message!" rows="3"></textarea>
</div>
<button class="btnbtn-primary" type="submit">Send</button>
</form>
<...>
</template>
嗯,我们已经将"addMessage"
方法绑定到表单的submit
回调,但是我们还没有定义这个方法!因此,让我们定义它。在我们的App.vue
导出部分添加methods
对象,并在其中定义addMessage
方法。此方法将从我们的表单接收事件属性,然后只需获取newMessage
对象并将其推送到messagesRef
数组中。听起来很容易吧?
//App.vue
<script>
export default {
<...>
**methods: {**
**addMessage (e) {**
**e.preventDefault()**
**this.newMessage.timestamp = Date.now()**
**messagesRef.push(this.newMessage)**
**}**
**}**
}
</script>
现在,打开页面,填写表单,然后点击发送按钮。您会立即看到您的消息出现在消息列表中:
我们在表单中输入的消息立即传播到消息列表中
还有一些东西我们需要修复。我们不希望填写表单的值在消息添加到消息列表后仍然保留在那里。因此,我们需要在addMessage
方法中清除它。可能,至少对标题进行一些基本检查也会很好。因此,将方法重写如下:
//App.vue
addMessage (e) {
e.preventDefault()
**if (this.newMessage.title === '') {**
**return**
**}**
this.newMessage.timestamp = Date.now()
messagesRef.push(this.newMessage)
**this.newMessage.text = ''**
**this.newMessage.title = ''**
**this.newMessage.timestamp = null**
}
现在,如果你开始添加更多的消息,事情看起来有点奇怪。我们显示消息的方式可能不是最适合我们的情况。你还记得我们将消息卡片包装在带有card-group
类的div
中吗?让我们尝试用card-columns
类替换它,看看是否更好看。事实上,是的。让我们保持这样。
添加实用函数使事情看起来更美观
我们已经有一个完全功能的单页面应用程序,但它仍然缺少一些令人惊叹的东西。例如,时间显示为时间戳并不真的美观。让我们编写实用函数,将我们的时间戳转换成美观的形式。
我们将使用Moment.js库(momentjs.com/
)。在应用程序文件夹中安装它:
**npm install moment --save**
创建一个名为utils
的文件夹。在这个文件夹中添加一个名为utils.js
的文件。导入moment
并编写以下函数:
//utils.js
import moment from 'moment'
function dateToString (date) {
if (date) {
**return moment(date).format('MMMM Do YYYY, h:mm:ss a')**
}
return''
}
在文件的末尾导出它:
//utils.js
<...>
export { dateToString }
让我们将这个函数导入到App.vue
中,并用它来格式化我们的时间戳。打开App.vue
文件,在script
部分的开头添加import
语句:
//App.vue
<script>
import Firebase from 'firebase'
**import { dateToString } from './utils/utils'**
<...>
</script>
为了能够在 Vue 模板中使用这个函数,我们必须在methods
部分导出它。只需向methods
对象添加一个新条目:
//App.vue
<script>
export default {
<...>
methods: {
**dateToString: dateToString**,
<...>
}
</script>
由于我们使用 ES6,我们可以直接编写以下代码行:
methods: {
**dateToString**
}
现在,我们可以在模板部分使用这种方法。只需将message.timestamp
绑定对象包装在dataToString
方法中:
<p class="card-text"><small class="text-muted">Added on {{ **dateToString(message.timestamp)** }}</small></p>
查看页面!现在,你可以看到美观的日期,而不是 Unix 时间戳。
练习
我有一个小练习给你。你看到了将实用函数添加到将时间戳转换为格式良好的日期是多么容易。现在,创建另一个实用函数,将其命名为reverse
。这个函数应该用于以相反的顺序显示消息数组,所以最近的消息应该首先出现。如果你有疑问,请查看本章的代码。
将消息卡片提取到它们自己的组件中
你可能注意到了演示应用程序的第一条消息总是在那里。它不会被其他新消息项移动。所以,它似乎是一种特殊的消息,并且以一种特殊的方式对待。事实上,确实如此。如果你想让一张卡片固定,只需在遍历其他消息的card
元素之前添加它。你还可以给这张卡片添加一些类,以显示它是真的特别。在我的例子中,我添加了 Bootstrap 的card-outline-success
类,用漂亮的绿色轮廓显示元素:
//App.vue
<div class="card-columns">
**<div class="card card-outline-success">**
**<div class="card-block">**
**<h5 class="card-title">Hello!</h5>**
**<p class="card-text">This is our fixed card!</p>**
**<p class="card-text"><small class="text-muted">Added on {{ dateToString(Date.now()) }}</small></p>**
**</div>**
**</div>**
<div class="card" v-for="message in messages">
<div class="card-block">
<h5 class="card-title">{{ message.title }}</h5>
<p class="card-text">{{ message.text }}</p>
<p class="card-text"><small class="text-muted">Added on {{ dateToString(message.timestamp) }}</small></p>
</div>
</div>
</div>
现在,你有一张漂亮的固定卡片,颜色与其他卡片的颜色不同。但是... 你没有看到任何问题吗?我们在模板中重复了完全相同的代码两次。我非常确定你知道任何开发者的黄金法则:DRY—不要重复自己!
让我们将卡片提取到一个单独的组件中。这很容易。在components
文件夹中添加一个名为Card.vue
的组件。这个组件的代码非常简单:
//Card.vue
<template>
<div class="card">
<div class="card-block">
<h5 class="card-title">**{{ title }}**</h5>
<p class="card-text">**{{ text }}**</p>
<p class="card-text"><small class="text-muted">**{{ footer }}**</small></p>
</div>
</div>
</template>
<script>
export default {
props: [**'title', 'text', 'footer'**]
}
</script>
现在,让我们在App.vue
中调用这个组件,为标题、文本和页脚添加不同的值。首先,它应该在 Vue 的components
对象中被导入和导出:
//App.vue
<script>
<...>
**import Card from './components/Card'**
<...>
export default {
<...>
**components: {**
**Card**
**}**
}
</script>
现在,我们可以在模板中使用<card>
元素。我们需要绑定标题、文本和页脚。页脚实际上是显示添加于...的文本。因此,第一张卡片的标记看起来像这样:
//App.vue
<template>
<div class="card-columns">
<card class="card-outline-success"**:title="'Hello!'":text="'This is our fixed card!'":footer="'Added on ' + dateToString(Date.now())"**></card>
</div>
</div>
</template>
其他消息的列表将遵循相同的逻辑。对于messages
数组中的每条消息,我们将绑定相应消息的条目(标题、文本和时间戳)。因此,消息卡片列表的标记看起来像这样:
<div class="card-columns">
<...>
<card v-for="message in messages"**:title="message.title":text="message.text":footer="'Added on ' + dateToString(message.timestamp)"**></card>
</div>
</div>
你可以看到,我们用两行代码替换了十四行代码!当然,我们的组件也包含一些代码,但现在,我们可以一遍又一遍地重用它。
练习
我们将卡片代码提取到其各自的组件中的方式无疑是很好的,但我们为第一条消息绑定属性的方式有点丑陋。如果在某个时候我们需要更改消息的文本怎么办?首先,很难在标记内找到文本。此外,在标记属性内管理文本非常困难,因为我们必须非常小心,以免弄乱双引号/单引号。而且,承认吧,这很丑陋。您在这个练习中的任务是将第一条消息的标题、文本和日期提取出来,放入数据对象中,并以与绑定其他消息相同的方式绑定它。如果您对这个练习有疑问,请查看本章的代码。
注意
不要被提供的代码中的v-bind
指令所困惑。我们已经在使用它,只是它的缩写版本——在分号后面写绑定属性的名称。因此,例如,v-bind:messages
与:messages
是相同的。
部署您的应用程序
好了,现在我们手头有一个完全可用的应用程序,是时候将其公开了。为了做到这一点,我们将把它部署到 Firebase 上。
首先安装 Firebase 工具:
**npm install -g firebase-tools**
现在,您必须告诉 Firebase 工具,您实际上是一个拥有账户的 Firebase 用户。为此,您必须使用 Firebase 工具登录。运行以下命令:
**firebase login**
按照说明进行登录。
现在,您必须在应用程序中初始化 Firebase。从应用程序根目录调用以下内容:
**firebaseinit**
您将被问一些问题。对于第一个问题,请选择第三个选项:
对于第一个问题,选择 Hosting 选项
从项目列表中选择PleaseIntroduceYourself
项目以关联到应用程序。
初始化已完成。检查项目文件夹中是否已创建名为firebase.json
的文件。该文件可以包含无数个配置。在这方面,请查看官方 Firebase 文档firebase.google.com/docs/hosting/full-config
。对于我们来说,部署的公共目录的基本指示就足够了。vue-cli
构建生产就绪资产的目录称为dist
;因此,我们希望部署该目录的内容。因此,请将以下代码添加到您的firebase.json
文件中:
{
"hosting": {
"public": "**dist**",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
不要忘记保存您的firebase.json
文件。现在让我们构建和部署我们的应用程序。听起来像是一个很大的 DevOps 任务,对吧?其实并不是很大。运行npm build
,然后运行firebase deploy
。
**npm run build**
**firebase deploy**
有多难?成功部署后,Firebase 将输出您项目的 URL。现在,您可以开始使用它并将其发送给您的朋友。这可能不是世界上最美丽的 URL,对吧?也许您想将其连接到您的域名?当然,这是可能的!
额外里程-将您的 Firebase 项目连接到自定义域
将 Firebase 项目连接到自定义域名非常容易。首先,当然,您需要购买这个域名。对于这个应用程序,我购买了pleaseintroduceyourself域名,使用最便宜的顶级域名.xyz
。在 GoDaddy 上,每年花费我一美元多一点(godaddy.com
)。拥有了您的域名之后,就非常容易了。转到项目的 Firebase Web 控制台。点击左侧的托管选项卡。然后,点击连接域按钮:
点击连接域按钮
在弹出窗口中,输入您的域名:
输入您的域名
它会建议您向您的域名添加 TXT DNS 记录。只需打开您的 DNS 提供商页面,选择您的域名,找出如何添加 DNS 记录,并添加类型为TXT
的记录。在我的情况下,使用 GoDaddy,记录添加部分看起来像这样:
向我们的域名添加 DNS TXT 记录
握手建立后(注意,可能需要一些时间),Firebase 将建议您进行最后一步-向您的域名添加A记录。按照与上一步完全相同的步骤进行;只是不再添加类型为TXT
的记录,而是添加类型为A的记录。
直到更改完全传播需要一些时间。在我的情况下,大约需要一个小时。过一段时间,您将能够使用https://<您的域名>.<您的顶级域名>
地址打开您的新页面。在我的情况下,正如您已经知道的那样,它是pleaseintroduceyourself.xyz/
。
总结
在本章中,我们按照教程从头开始开发了一个单页面应用程序。我们使用 Vue.js 框架来构建我们的应用程序,使用 Bootstrap 框架来应用样式,使用 Firebase 平台来管理应用程序的持久层和托管。
尽管能够取得可观的成果(一个完全功能的部署应用程序),但我们在不了解背后发生的事情的情况下完成了所有工作。教程没有解释 Vue.js、Bootstrap 或 Firebase 是什么。我们只是理所当然地接受了它。
在下一章中,我们将详细了解底层技术。我们将做以下事情:
-
仔细研究 Vue.js 框架,从基本理解开始,然后涵盖诸如指令、数据绑定、组件、路由等主题
-
深入了解 Bootstrap 框架,查看可以使用它实现什么以及如何实现
-
更好地了解 Firebase 平台;我们将获得一些基本的了解,并涉及更复杂的主题,如数据存储或函数
-
了解使用这三个不同项目的不同技术,为我们的应用程序增加简单性、强大性和灵活性
第二章:底层-教程解释
在上一章中,我们从头开始构建了一个简单的单页面应用程序。我们使用 Vue.js 来实现应用程序的功能,使用 Bootstrap 使其美观,并使用 Firebase 来管理应用程序的后端部分。
在本章中,我们将深入了解所有这些技术,看看它们如何以及为什么能够很好地协同工作。我们将主要讨论 Vue.js,因为这将是我们构建应用程序的首选框架。然后,我们将涉及 Bootstrap 和 Firebase,以基本了解这些技术有多强大。话虽如此,在本章中我们将:
-
讨论 Vue.js 框架、反应性和数据绑定。我们不仅将涵盖 Vue.js 的基础知识,还将深入探讨诸如指令、组件、路由等主题。
-
讨论 Bootstrap 框架。我们将看到它可以实现什么,讨论它如何有助于布局应用程序,并讨论它的组件如何为您的应用程序提供有用的自包含功能。
-
讨论 Firebase 平台。我们将看到它是什么,它提供了哪些功能,并且如何使用其 API 将这些功能带到应用程序中。
-
检查所有提到的技术如何结合在一起,以实现在开发复杂事物时的简单性。
Vue.js
官方 Vue.js 网站建议 Vue 是一个渐进式 JavaScript 框架:
来自官方 Vue.js 网站的截图
这意味着什么?以非常简化的方式,我可以将 Vue.js 描述为一个为 Web 应用程序带来反应性的 JavaScript 框架。
不可否认的是,每个应用程序都有一些数据和一些界面。在某种程度上,界面负责显示数据。数据可能在运行时发生变化,也可能不会。界面通常必须以某种方式对这些变化做出反应。界面可能有一些交互元素,这些元素可能会或可能不会被应用程序的用户使用。数据通常必须对这些交互做出反应,因此,其他界面元素必须对已对数据所做的更改做出反应。所有这些听起来都很复杂。这种复杂架构的一部分可以在后端实现,靠近数据所在的地方;另一部分可能在前端实现,靠近界面。
Vue.js 允许我们简单地将数据绑定到界面并放松。所有数据和界面之间必须发生的反应都将自动发生。让我们看一个非常简单的例子,我们将在页面标题上绑定一条消息。首先定义一个简单的 HTML 结构:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Vue.js - binding data</title>
</head>
<body>
<div id="app">
<h1>Hello, reader! Let's learn Vue.js</h1>
</div>
</body>
</html>
现在,让我们在此页面上初始化一个Vue.js实例,并将其数据绑定到<h1>
元素。对于这个简单的例子,我们将使用一个独立的Vue.js
文件。从 Vue.js 官方页面vuejs.org/js/vue.js
下载它。在<script>
标签中导入它。现在让我们初始化一个Vue实例。Vue.js 实例需要的最少的是要附加到的元素和data
对象。我们想要将我们的 Vue 实例附加到具有app
ID 的主<div>
标记。让我们还创建一个包含名称条目的数据对象:
var data = {name:'Olga'}
让我们用这些数据创建我们的 Vue.js 实例:
new Vue({
el: '#app',
**data**
})
现在让我们将data
绑定到我们的 HTML 元素上。我们将使用双大括号({{}}
)来实现这一点。一旦元素被附加到Vue
实例上,它内部的所有内容都变得特殊 - 即使是大括号。双大括号内的任何内容都将被解释和计算。因此,如果您在大括号内放入,例如,2 + 2
,4
将在页面上呈现。试一试。任何表达式,任何语句都将被编译和计算。不要太激动,不要开始在这些括号内编写大段的 JavaScript 代码。让我们把计算留给脚本逻辑,脚本逻辑写在脚本所在的地方。使用括号访问您传递给Vue
实例的数据。因此,在我们的例子中,如果您在 HTML 标记中的任何位置插入{{name}}
,您将看到我们在数据对象中传递给Vue
实例的名称。例如,用{{name}}
替换<h1>
元素中的reader
:
<h1>Hello, **{{name}}**! Let's learn Vue.js</h1>
刷新页面后,您会看到我们传递给 Vue 实例的名称被呈现出来。尝试在开发者工具控制台中更改data.name
属性。您会立即看到更改被传播。我们在这里看到的是单向数据绑定 - 数据发生的更改会被动地传播到绑定数据的元素。Vue.js 还支持双向数据绑定;因此,页面上元素发生的更改也会传播到绑定元素的数据中。
为了实现这一点,只需使用v-model
属性将给定的数据片段绑定到元素上。例如,让我们在页面上添加一个文本输入,并将其绑定到数据属性name
:
<input type="text"**v-model="name"**>
现在,一旦您开始在文本输入中输入,更改将立即传播到绑定到此数据片段的任何其他元素:
数据的更改会通过所有绑定的元素进行响应式传播
HTML 标记和 JavaScript 代码的完整代码如下:
<body>
<div id="app">
<div>
<label for="name">What's your name? </label>
<input id="name" type="text" **v-model="name"**>
</div>
<h1>Hello, <strong>**{{name}}**</strong>! Let's learn Vue.js</h1>
</div>
<script src="vue.js"></script>
<script>
**var data = {name:'Olga'}**
**new Vue({**
**el: '#app',**
**data**
**})**
</script>
</body>
如您所见,这一点都不难。您只需要将data
传递给Vue
实例,并将其绑定到页面的元素上。Vue 框架会处理其他所有事情。在接下来的章节中,我们将了解使用 Vue.js 还有哪些可能性,以及如何启动一个 Vue.js 项目。
Vue 项目-入门
现在,我们知道了 Vue.js 的用途和主要重点,我们想要动手开始一个 Vue.js 项目,并探索所有 Vue.js 的特性。有很多种方式可以将 Vue 包含到项目中。让我们一起来探索它们。
直接包含在脚本中
您可以通过下载 Vue.js 并在<script>
标签中包含它来使用 Vue.js。实际上,在上一节中我们已经这样做了。因此,如果您已经运行了一个项目并想要使用一些 Vue.js 特性,您可以简单地包含vue.js
文件并使用它。
CDN
如果您不想自己下载和管理 Vue 版本,可以简单地使用 CDN 版本。只需在项目中包含unpkg.com/vue
脚本,您就可以开始了!它将始终与最新的 Vue 版本同步:
<script src="**https://unpkg.com/vue**"></script>
NPM
如果您全身心投入 Node.js 开发,您可以简单地向您的package.json
文件添加一个npm
依赖项。只需在项目的根目录上运行npm install
:
**npm install vue --save**
Vue-cli
Vue 提供了一个漂亮干净的命令行界面,非常适合启动新项目。首先,您必须安装vue-cli:
**npm install --global vue-cli**
现在,您可以使用 Vue 命令行界面开始一个全新的项目。查看vue-cli存储库,获取详细文档:github.com/vuejs/vue-cli
。
如您所见,可以使用不同的模板设置项目——从简单的单个 HTML 页面项目开始,到复杂的 webpack 项目设置。用于搭建 Vue 项目的命令如下:
**vue init <template-name><project-name>**
以下模板可用:
-
webpack:这是一个具有
vue-loader
的功能齐全的 webpack 设置。它支持热重载、linting、测试、各种预处理器等等。 -
webpack-simple:这是一个简单的 webpack 设置,对于快速原型设计非常有用。
-
browserify:这是一个具有 vueify 的功能齐全的 browserify 设置,还支持热重载、linting 和单元测试。
-
browserify-simple:这是一个简单的具有 vueify 的 browserify 设置,可用于快速原型设计。
-
simple:这将生成一个包含 Vue.js 的简单 HTML 页面。非常适合快速功能探索。
还可以创建自定义模板。查看github.com/vuejs/vue-cli#custom-templates
上的文档并尝试一下。
在本书中,我们将使用webpack
模板。我们将包括一些加载器,并使用 linters、单元测试和端到端测试技术。要使用webpack
模板引导项目,只需运行以下代码行:
**vue init webpack my-project**
现在我们知道如何使用vue-cli搭建项目,让我们看看除了我们在上一节中已经探索过的内容之外,Vue 还提供了什么。
Vue 指令
Vue 指令只不过是附加到 HTML 元素的属性。这些指令为您的模板提供了一些额外的功能。
所有这些指令都以前缀v-
开头。为什么?因为这是Vue!您已经在上一节中使用了其中一些。现在,我们将看看存在哪些指令以及您可以用它们做什么。
条件渲染
打开我们的 Hello 页面并删除用户的输入。发生了一些不太美观的事情:
“你好,!”
根据用户输入的不同条件,有可能有趣地渲染你好,名称消息。如果有名称,则渲染它;如果没有名称,则不渲染。
例如,只有在存在名称时才渲染你好,名称消息。指令v-show
和v-if
正是用于条件渲染的。打开此示例的index.html
文件,让我们进行更改。将Hello, <strong>{{name}}</strong>!
部分包装到span
中,并添加一个带有name
值的v-show
属性:
<h1>**<span v-show="name">**Hello, <strong>{{name}}</strong>! **</span>**Let's learn Vue.js</h1>
现在,如果您刷新页面并完全删除输入,消息将只显示让我们学习 Vue.js:
v-show 属性允许条件渲染
尝试用v-show
指令替换v-if
指令。最终结果将是相同的。那么为什么两者都存在呢?查看开发者工具的元素选项卡,尝试添加或删除输入框中的文本。您会发现,在v-show
的情况下,如果条件不满足,条件性 span 将只获得display:none
属性。在v-if
的情况下,元素将完全消失:
使用 v-show 属性操作显示 CSS 属性,而使用 v-if 属性会完全添加/删除一个元素
我们何时使用这两个属性?如果有很多元素应该根据一些数据可见(这些数据真的很动态,所以在运行时会经常发生),我建议使用v-show
属性,因为在 DOM 中添加或删除元素是一个相当昂贵的操作,可能会影响应用程序的性能甚至 DOM 本身。另一方面,如果元素应该有条件地渲染一次,比如在应用程序启动时,请使用v-if
属性。如果某些元素不应出现,它们将不会被渲染。因此,页面上的元素数量将减少。因此,应用程序的计算成本也将减少,因为现在它要经过和计算的元素更少。
文本与 HTML
我相信你已经从上一章中非常了解如何使用胡须语法{{}}
绑定一些数据。
由于这是一本关于编程的技术书籍,我们必须在这里有一只猫!猫很容易渲染。它的 Unicode 是U+1F638
;因此,我们只需在 HTML 中添加😸
代码:
<div>**😸**</div>
当然,我们会有一只猫:
表情猫对我们说你好
这很好,但是如果我们想用狗代替猫,我们将不得不使用谷歌寻找另一个代表狗的 Unicode 并替换它。如果在某个时候我们想用独角兽替换它,我们将不得不运行相同的过程。此外,仅仅通过查看我们的代码,我们将无法说出我们实际渲染的是什么,除非我们知道所有表情符号代码是♥
。将表情符号的名称映射到它们的代码可能是一个好主意。
让我们添加一些它们的地图。打开你的 HTML 文件,将以下代码添加到<script>
区域:
//index.html
<script>
const animalCodes = {
dog: '🐶',
cat: '😸',
monkey: '🐵',
unicorn: '🦄'
}
const data = {
animalCodes
}
new Vue({
el: '#app',
data
})
</script>
现在,您可以将此地图的值绑定到您的 HTML 元素。让我们尝试使用 mustache 注释来做到这一点:
<div>{{animalCodes.cat}}</div>
刷新页面。结果并不完全符合我们的预期,是吗?
代码被呈现,而不是实际的猫表情符号
这是因为 mustache 插值实际上是插值文本。使用 mustache 插值与使用v-text
指令是一样的:
<div **v-text="animalCodes.cat"**></div>
我们实际上想要渲染的不是文本;我们想要渲染的是作为 HTML 呈现的表情符号的 Unicode 值!这在 Vue.js 中也是可能的。只需用v-html
指令替换v-text
指令:
<div **v-html="animalCodes.cat"**></div>
现在,我们将重新获得我们的猫,并且当我们查看代码时,我们确切地知道我们正在渲染什么。
因此,请记住使用v-text
指令或 mustache 注释进行文本插值,使用v-html
指令进行纯 HTML 插值。
循环
在上一节中,我们在页面上放了一只猫。在本节中,我想要整个动物园!想象一下,我们的动物园有一只猫,一只狗,一只猴子,当然还有一只独角兽。我们想要在有序列表中显示我们的动物园。当然,您可以编写一个简单的标记,看起来像这样:
<ol>
<li>😸</li>
<li>🐶</li>
<li>🐵</li>
<li>🦄</li>
</ol>
然而,这会使您的代码难以阅读,如果您想要向您的动物园添加更多动物或删除其中一个,您必须牢记所有这些代码。在上一节中,我们为 emoji 动物 Unicode 添加了一个地图。让我们在我们的标记中使用它。您已经学会了我们必须使用v-html
指令,以便代码被插值为 HTML。因此,我们的标记将如下所示:
<div id="app">
<ol>
<li v-html="**animalCodes.cat**"></li>
<li v-html="**animalCodes.dog**"></li>
<li v-html="**animalCodes.monkey**"></li>
<li v-html="**animalCodes.unicorn**"></li>
</ol>
</div>
看起来更好了,但仍然有一些可以改进的地方。想象一下,如果您想要渲染来自 emoji 世界的所有动物!有很多。对于每种动物,您都必须重复列表项的代码。每当您想重新排序列表,删除一些元素或添加新元素时,您都必须处理这个标记。如果我们只有一个要渲染的动物数组,然后以某种方式迭代它并渲染其中的内容,那不是很好吗?当然,是的!使用v-for
指令是可能的。使用以下代码行创建一个动物数组:
const animals = ['dog', 'cat', 'monkey', 'unicorn']
将其导出到vue data
对象中:
var data = {
name:'Olga',
**animals**,
animalCodes
}
现在,您可以在v-for
指令中使用此数组,并仅用一个替换多个<li>
元素:
<ol>
<h2><span>{{name}}! </span>Here's your Zoo</h2>
<li **v-for="animal in animals"** v-html="animalCodes[**animal**]"></li>
</ol>
结果将会很好:
使用v-for
指令呈现的 Emoji 动物园
绑定数据
在上一节中,我们已经处理了使用 Vue.js 呈现不同数据的许多内容;所以现在,你已经熟悉了不同的绑定方式。你知道如何将数据插值为文本和HTML,你知道如何迭代数据数组。
我们还看到双向数据绑定是通过v-model
指令实现的。我们用它将名称绑定到输入元素:
<input id="name" type="text" **v-model="name"**>
v-model
指令只能与input
、select
和textarea
元素一起使用。它还接受一些修饰符一起使用。修饰符是影响输入的特殊关键字。有三个修饰符可以与此指令一起使用:
-
.lazy
:这将只在更改事件上更新数据(尝试使用我们的输入,你会发现输入的更改只会影响其他部分,当按下Enter按钮时,而不是在每次按键时) -
.number
:这将把你的输入转换为数字 -
.trim
:这将修剪用户的输入
也可以链接修饰符:
<input id="name" type="text"**v-model.lazy.trim="name"**>
所以现在,我们几乎了解了将数据绑定到元素的一切。如果我们想要将一些数据绑定到元素的属性呢?例如,根据某些数据值动态设置图像源属性或类属性的值。我们该怎么做呢?
为此,Vue 提供了一个v-bind
指令。使用这个指令,你可以绑定任何你想要的东西!
例如,当名称未定义时,让我们显示一个悲伤的图片,当名称被定义时,让我们显示一个高兴的图片。为此,我创建了两张图片,glad.png
和sad.png
,并将它们放入我的应用程序的images
文件夹中。我还将它们的路径导出到数据对象中:
//index.html
var data = {
name:'Olga',
animals,
animalCodes,
**sadSrc: 'images/sad.png',**
**gladSrc: 'images/glad.png'**
}
现在,我可以创建一个图像,并使用v-bind:src
绑定其源,我将提供一个 JavaScript 表达式作为值。这个表达式将检查名称的值。如果它被定义,将应用glad
图像,如果没有,将应用sad
图像:
<img width="100%" **v-bind:src="name ? gladSrc : sadSrc"**>
v-bind
指令的快捷方式是:
,所以我们可以写下以下代码行:
<img width="100%" **:src**="name ? gladSrc : sadSrc">
当name
的值被定义时,我们的页面是这样的:
当名称被定义时,快乐的表情图像出现
如果您从输入字段中删除名称,图像将自动更改!打开页面,尝试从输入字段中删除文本,然后再次添加。继续删除和添加,您将看到图像如何快速更改为相应的图像。这是当名称未定义时页面的外观:
一旦输入被清除,图像源立即更改
基本上,您可以对任何属性绑定执行完全相同的操作,例如 class:
<label for="name" **v-bind:class="{green: name, red: !name}"**>What's your name? </label>
您还可以绑定属性以传递给子组件。我们将在有关组件的部分中看到如何执行此操作。
处理事件
除了直接将数据绑定到元素的形式之外,我们还希望处理一些事件,因为这是我们的用户在页面上所做的事情 - 触发一些事件,以便它们发生。他们点击,他们悬停,他们提交表单 - 所有这些事件都必须以某种方式由我们处理。Vue 提供了一种非常好的方法,可以将侦听器附加到任何 DOM 元素上,并提供可以处理这些事件的方法。这些方法的好处是它们可以使用this
关键字直接访问 Vue 数据。通过这种方式,我们可以使用方法来操作数据,而由于这些数据是响应式的,所有更改将立即传播到绑定了这些数据的元素。
要创建一个方法,您只需在 Vue 应用程序的导出部分添加一个methods
对象。为了将此方法附加到任何事件侦听器,请在冒号后使用v-on
指令与相应的事件。这是一个例子:
v-on:sumbit="handleSubmit"
v-on:click="handleClick"
v-on:hover="handleHover"
此指令的快捷方式是@
,因此我们可以将所有这些指令重写如下:
@sumbit="handleSubmit"
@click="handleClick"
@hover="handleHover"
这应该对您来说很熟悉。您还记得我们在第一章中遵循的教程吗,请介绍你自己 - 教程?您还记得我们正在监听消息的submit
方法,添加form
并调用addMessage
吗?看一下。我们的表单及其submit
指令如下所示:
//please-introduce-yourself/src/App.vue
<template>
<form **@submit="addMessage"**>
<...>
</form>
</template>
然后,在methods
部分,我们实际上定义了addMessage
方法:
//please-introduce-yourself/src/App.vue
<script>
<...>
export default {
<...>
methods: {
addMessage (e) {
<...>
},
},
}
</script>
现在开始更有意义了吗?
为了更好地理解,让我们在我们的动物园页面上添加一些方法!如果你能组成自己的动物园,那不是很好吗?让我们添加一个多选元素,它将包含所有可能的选项,你的动物园将从你实际选择的东西中填充!所以,让我们这样做:
-
向我们的
animalCodes
映射添加更多动物 -
添加另一个名为
animalsForZoo
的数组 -
在显示动物园的有序列表中使用这个新数组
-
添加一个由
animalCodes
映射的键组成的多选select
框 -
将一个
@change
监听器附加到这个选择框,它将调用populateAnimalsForZoo
方法 -
创建一个
populateAnimalsForZoo
方法,它将使用从我们的多选元素中选择的选项填充animalsForZoo
数组
听起来很容易吧?当然,是的!让我们开始吧。所以,首先,向我们的animalCodes
映射添加更多动物:
var animalCodes = {
dog : '🐶',
cat : '😸',
monkey : '🐵',
unicorn : '🦄',
tiger : '🐯',
mouse : '🐭',
rabbit : '🐰',
cow : '🐮',
whale : '🐳',
horse : '🐴',
pig : '🐷',
frog : '🐸',
koala : '🐼'
}
让我们重新思考一下我们的animals
数组,并根据我们的映射生成它。这样,每当我们需要添加新的动物时,我们只需将其键值名称-unicode 添加到映射对象中,而不是维护对象和数组。所以,我们的animals
数组将如下所示:
var animals = **Object.keys**(animalCodes)
现在,我们需要另一个空数组。让我们称之为animalsForZoo
,并让我们从这个新数组中填充我们的动物园。因为它是空的,我们的动物园也将是空的。然而,我们即将创建一个填充这个数组的方法。所以,创建一个数组很容易,不要忘记在数据对象中导出它:
<script>
<...>
**var animalsForZoo = []**
var data = {
name:'Olga',
animals,
animalCodes,
**animalsForZoo**,
sadSrc: 'images/sad.png',
gladSrc: 'images/glad.png'
}
new Vue({
el: '#app',
**data**
})
</script>
不要忘记用新的animalsForZoo
数组替换我们动物园展示中对animals
数组的使用:
<ol>
<li v-for="animal in **animalsForZoo**"><span class="animal" v-html="animalCodes[animal]"></span></li>
</ol>
我知道现在你担心你页面上的动物园是空的,但给我们几分钟,我们会照顾好的!
首先,让我们创建一个多选select
元素,它将根据animals
数组进行填充:
<select multiple="multiple" name="animals" id="animals">
<option **v-for="animal in animals"** :value="animal">**{{animal}}**</option>
</select>
现在,最后,我们将给我们的选择框添加一个事件监听器。让我们将监听器附加到 change 事件上。让我们告诉它调用populateAnimalsForZoo
方法。我们的指令将如下所示:
@change="**populateAnimalsForZoo**"
整个select
元素将获得一个新属性:
<select **@change="populateAnimalsForZoo"** multiple="multiple" name="animals" id="animals">
<option v-for="animal in animals" :value="animal">{{animal}}</option>
</select>
太棒了!但是没有populateAnimalsForZoo
这样的方法。但是有我们!让我们创建它。这个方法将只是遍历作为输入选择的动物的选中选项,并将它们推入animalsForZoo
数组中:
new Vue({
el: '#app',
data,
methods: {
**populateAnimalsForZoo(ev) {**
**this.animalsForZoo = []**
**const selected = document.querySelectorAll('#animals option:checked')**
**for (var i = 0; i < selected.length; i++) {**
**this.animalsForZoo.push(selected[i].value)**
**}**
**}**
}
})
查看在chapter2/example1-vue-intro/index.html
文件中所有这些更改后整个 HTML 和 JavaScript 代码的样子。这是我们在更改后的测试页面的样子:
动物园是根据用户的选择进行填充的
页面很混乱,对吧?然而,看看你已经通过使用这个页面学到了多少东西。而且,承认吧,这是一个有趣的学习过程!我们还没有完成。
现在你已经学会了如何添加方法和事件监听器,我将教你如何在没有这个方法和v-bind:change
的情况下完成完全相同的事情。删除我们刚刚添加的所有代码,只需在我们的select
元素中添加v-model
和animalsForZoo
值:
<select **v-model="animalsForZoo"** multiple="multiple" name="animals" id="animals">
<option v-for="animal in animals" :value="animal">{{animal}}</option>
</select>
现在,我们刚刚在方法中所做的一切都被 Vue 自动处理了!是不是很棒?
Vue 组件
我们来到这一章时手头上有一个中等大小的 HTML 页面,其中包含了许多不同的部分。我们可以想到更多的事情,比如为动物园中的每只动物添加互动性,添加喂养动物的可能性,或者在每次你悬停在动物图标上时显示每只动物的有趣事实。在某个时候,让我们面对现实吧,HTML 文件以及它的 JavaScript 将变得难以维护。
你也能看到我们的可视化层(HTML)与我们的逻辑层(JavaScript)一起工作吗?所以,它们有点像形成了块、项目、砖块... 例如,我们有一段代码负责Hello名称部分。我们有另一个包含我们动物园的块。动物园中的每只动物都是另一个项目。
无论你想怎么称呼这些东西,它们无可否认地是结构和逻辑的分离部分,当它们聚集在一起时,形成了整个拼图。如果你用一块独特的材料建造一堵墙,并决定改变墙的一些部分,这将不是一件容易的事情。
所以,想象一下,你建造了这堵墙,并将一些黄色的星星、蓝色的多边形、红色的正方形等等融入其中。然后,你决定你的黄色星星应该是黑色的。你必须改变所有的星星。然后,你决定你的绿色椭圆应该是一个笑脸。现在怎么办?改变所有的椭圆,但首先你必须找到墙上包含这些椭圆的所有位置。这是你的墙,试着找到其中的所有椭圆:
墙是作为一个整体建造的,其中包含了不同颜色和形状的部分
现在,想象每个部分实际上都存在于它们各自的砖块上。你可以随意更改它们,添加它们,以及移除它们。如果你想改变一些墙体元素的外观,你只需要改变这一个砖块,所有包含这个砖块的墙体部分都会改变,因为总的来说,它只是墙上的另一块砖。所以,与其让墙体充满各种奇怪的内嵌部件,你只需要四块砖,然后在需要改变依赖于这块砖的墙体部分时进行更改:
如果你需要改变墙上的一个元素的外观,你只需要改变相应的砖块
墙是由砖块组成的。这些砖块就是我们的组件。如果我们还可以用 HTML、CSS 和 JavaScript 构建组件,并且我们的应用程序可以由这些组件构建呢?我刚刚说“如果”吗?没有“如果”。我们已经有了。Vue.js 支持基于组件的应用程序结构。使用 Vue.js 创建组件非常容易。你需要做的只有三件事:
-
创建一个组件,并给它一个模板、数据、方法,以及你需要给它的任何东西。
-
在 Vue 应用程序中注册它,放在
components
对象下面。 -
在应用程序的模板中使用它。
例如,让我们创建一个简单渲染一个标题元素说Hello的组件。让我们称之为HelloComponent
。它只包含模板字符串:
var HelloComponent = {
template: '<h1>Hello!</h1>'
}
现在,我们可以在 Vue 应用程序初始化代码中注册这个组件:
new Vue({
el: '#app',
components: {
**HelloComponent**
}
})
现在,这个组件实际上可以在 Vue 应用程序元素的 HTML 部分中使用:
<div id="app">
**<hello-component></hello-component>**
</div>
所以,整个部分看起来会是这样的:
<body>
<div id="app">
<hello-component></hello-component>
</div>
<script src="vue.js"></script>
<script>
var HelloComponent = {
template: '<h1>Hello!</h1>'
}
new Vue({
el: '#app',
components: {
HelloComponent
}
})
</script>
</body>
有人可能会问,“这些组件有什么强大之处?”实际上,编写的代码量与我只编写了一个做同样事情的 HTML 代码是一样的。有什么意义呢?是的,当然,但在这个例子中,我们的组件只有一个内部模板。一个只有一行的模板。我们可以在里面放一个巨大的模板,并且我们可以在这个组件中添加一些方法和它自己的数据!比如,让我们给这个组件添加一个输入框用于输入名字,并将名字添加到它的数据对象中:
var HelloComponent = {
template: '<div>' +
'**<input v-model="name" />**' +
'<h1>Hello! **<strong>{{name}}</strong>**</h1>' +
'</div>',
**data() {**
**return {**
**name: ''**
**}**
**}**
}
如果你需要重复使用这个组件,你可以随意多次使用:
<div id="app">
**<hello-component></hello-component>**
**<hello-component></hello-component>**
**<hello-component></hello-component>**
</div>
然后,你将在你的页面上得到三个独立的组件:
使用组件有助于避免重复的代码
这些组件非常好,但仍然有大量的代码写在同一个 JavaScript 代码块中。我们在一个地方声明所有组件,如果组件太多,应用程序将再次变得难以管理。此外,在模板字符串中的 HTML 代码也不是最可维护的东西。
如果你是这样想的,我有一些好消息要告诉你。每个组件都可以存储在自己的文件中,具有自己的 HTML、JavaScript 和 CSS 代码。这些是带有.vue
扩展名的特殊文件。在每个文件中,有一个用于 JavaScript 代码的<script>
部分,一个用于 CSS 代码的<style>
部分,以及一个用于 HTML 代码的<template>
部分。这不是很方便吗?这些组件被称为单文件组件。看看第一章的代码——有一个名为App.vue
的主组件,还有我们创建的MessageCard.vue
组件。是不是很好?
如果你想在你的应用程序中使用单文件组件,你必须使用一些模块化捆绑工具来搭建这个应用程序,例如webpack
。我们已经谈论过vue-cli
以及使用webpack
模板轻松引导 Vue 应用程序的方法。让我们将混乱的动物园页面移植到webpack
捆绑应用程序中。运行初始化和安装脚本:
**vue init webpack zoo**
**cd zoo**
**npm install**
**npm run dev**
现在,打开App.vue
文件,让我们用混乱的动物园应用程序填充它。<script>
部分看起来是这样的:
<script>
<...>
var data = {
name: 'Olga',
animals,
animalCodes,
animalsForZoo,
**sadSrc: '../static/images/sad.png',**
**gladSrc: '../static/images/glad.png'**
}
export default {
name: 'app',
**data () {**
**return data**
**}**
}
</script>
注意高亮显示的区域。我已经将图片复制到static
文件夹中。另一个重要的事情是,组件内部的数据应该被用作返回对象的函数,而不是作为对象本身。由于数据对象仍然会成为多个组件中的一个单一实例,整个数据对象及其属性必须在一个专用函数中组装。
脚本的其余部分完全相同。
组件的模板区域与前面示例中的 HTML 结构基本相同。查看chapter2/example3-components-started
文件夹中的代码。
让我们将一些功能提取到各个单独的组件中。如果我们将动物园提取到单独的组件中,你觉得怎么样?在components
文件夹中创建一个Zoo.vue
文件。将动物列表的模板复制到这个组件的<template>
区域:
//Zoo.vue
<template>
<div v-if="animals.length > 0">
<h2><span v-if="name">{{name}}! </span>Here's your Zoo</h2>
<ol>
<li v-for="animal in animals"><span class="animal"v-html="animalCodes[animal]"></span></li>
</ol>
</div>
</template>
现在,我们应该告诉这个组件,它将从调用以下组件的父组件那里接收animals
、name
和animalCodes
属性:
//Zoo.vue
<script>
export default {
**props: ['animals', 'animalCodes', 'name']**
}
</script>
现在,打开主App.vue
组件,导入Zoo
组件,并在components
对象中导出它:
//App.vue
<script>
**import Zoo from './components/Zoo'**
<...>
export default {
name: 'app',
**components: {**
**Zoo**
**}**
}
</script>
现在,我们可以在模板中使用这个组件了!所以,用以下代码替换包含我们动物园的整个div
标签:
//App.vue
<template>
<...>
**<zoo :animals="animalsForZoo" :animalCodes="animalCodes":name="name"></zoo>**
<...>
</template>
查看页面!一切都像以前一样工作!
练习
将动物提取为单独的组件,并在v-for
指令内部调用它在动物园中。每个动物都必须有一个小功能,点击它的脸(在click
上)时会显示一个小描述。我相信你会很容易解决这个练习。如果你需要帮助,请查看example4-components/zoo
目录中的本章代码。
Vue 路由器
单页应用程序(SPA)很棒。它们让我们的生活变得更加轻松。而且确实如此。通过一点 JavaScript 代码,你可以实现以前必须在服务器端完成的所有功能,而整个页面应该被替换以显示该功能的结果。现在对于 Web 开发人员来说是黄金时代。然而,SPA 试图解决的问题是导航。历史 API 和pushState
方法(developer.mozilla.org/en-US/docs/Web/API/History_API
)已经在解决这个问题,但直到它成为一种成熟的技术,这个过程已经很长时间了。
我们的用户习惯于使用浏览器的导航按钮来控制他们的“我在哪里”和“我想去哪里”。如果整个功能位于同一页上,这些按钮如何帮助导航?你如何使用 Google 分析来检查你的用户更多地访问哪个页面(实际上是相同的)?整个概念完全不同。当然,这些应用程序速度更快,因为请求的数量大大减少,当然,我们的用户对此表示感激,但他们并没有因为我们改变了实现方式而改变他们的网页浏览习惯。他们仍然想要“返回”。他们期望如果他们刷新页面,页面将在刷新按钮之前的确切位置打开。他们期望通过查看页面的 URL 并检查斜杠后面的内容来理解他们在哪里。例如,如果是http://mySite/store
,那么这是一个商店;如果是http://mySite/settings
,那么很可能我在某个地方可以查看我的当前设置并更改它们。
有很多方法可以实现导航,而不必将单页面应用程序转换为多页面应用程序。你可以在应用程序上包含额外的逻辑,并在需要不同 URL 时更改window.location.href
,这将导致页面刷新,这并不好。你也可以使用 HTML5 的history
API。这可能不是最简单的维护方式,但可能有效。
我们都知道好的开发者是懒惰的,对吧?懒惰意味着不解决已经有人解决的问题。导航问题正在被许多框架和库解决。你不仅可以使用一些帮助你处理应用程序中路由的第三方库,还可以使用你选择的框架提供的机制。Vue.js 是提供处理路由的框架之一。你只需将 URL 路径映射到你的组件,一切都会正常工作!查看vue-router
库的官方文档router.vuejs.org/en/
。
为了能够使用vue-router
,你必须为你的项目安装它:
**npm install vue-router –save**
可选地,可以在 Vue 项目初始化时选择使用vue-router
。
现在,你可以在你的应用程序中使用 Vue 路由器。只需告诉 Vue 使用它:
//main.js
import Vue from 'vue'
**import VueRouter from 'vue-router'**
**Vue.use(VueRouter)**
让我们创建一个简单的路由示例。我们将有三个组件,其中一个被视为Home
组件,意味着当有人导航到根路由/
时应该显示它。让我们称第二个为Hello
组件,第三个为Bye
组件。从第二章中打开example5-router-started
代码文件,底层-教程解释。你会在components
目录中找到所有描述的组件:
我们将尝试 Vue 路由的示例应用程序的结构
现在,我们必须创建一个router
实例。构造函数接收options
对象作为参数。这个对象可以包含不同的可配置值。最重要的是routes
数组。这个数组的每个条目都应该包含一个指示路由的path
和其对应component
的对象。
首先,我们将导入所有需要的组件,然后,我们的router
实例将如下所示:
//main.js
**import Home from '@/components/Home'**
**import Hello from '@/components/Hello'**
**import Bye from '@/components/Bye'**
<...>
var router = new Router({
mode: 'history',
routes: [
{
name: 'home',
**component: Home,**
**path: '/'**
},
{
name: 'hello',
**component: Hello,**
**path: '/hello'**
},
{
name: 'bye',
**component: Bye,**
**path: '/bye'**
}
]
})
如果你想更好地理解mode:
history
选项是什么,请查看文档页面router.vuejs.org/en/essentials/history-mode.html
,它以非常好的方式解释了它。现在,我们必须将路由选项传递给我们的 Vue 应用程序。这个选项将指向我们的新router
实例:
//main.js
new Vue({
el: '#app',
template: '<App/>',
components: { App },
**router**
})
现在,整个应用程序都知道我们使用了这个路由。还有一个重要的步骤:我们需要将路由组件包含到主组件的模板中。为此,只需在App.vue
组件的模板中包含<router-view>
标签即可:
//App.vue
<template>
<div id="app">
<img src="./assets/logo.png">
**<router-view></router-view>**
</div>
</template>
在router.vuejs.org/en/api/router-view.html
中更详细地查看router-view
组件。
完成!如果你还没有运行应用程序,请运行:
**npm run dev**
打开页面http://localhost:8080
,检查它是否显示我们的主页组件。然后,在浏览器的地址栏中输入http://localhost:8080/hello
和http://localhost:8080/bye
。检查页面的内容是否根据 URL 路径实际改变:
使用 vue-router 进行基本路由
当然,你已经在考虑如何创建一个简单的菜单,将锚点<a>
元素指向路由器中定义的路径。不要想太多。只需使用一个带有to
属性的<router-link>
组件,指向您选择的路径。例如,为了在我们的路由器示例应用程序中显示一个简单的导航菜单,我们可以写出类似这样的东西:
//App.vue
<template>
<div id="app">
**<router-link to="/">Home</router-link>**
**<router-link to="hello">Hello</router-link>**
**<router-link to="bye">Bye</router-link>**
<router-view></router-view>
</div>
</template>
或者,如果你不想再次编写你的路径,你可以通过名称引用你的路由,并使用v-bind:to
指令或简单地使用:to
:
//App.vue
<template>
<div id="app">
**<router-link :to="{name: 'home'}">Home</router-link>**
**<router-link :to="{name: 'hello'}">Hello</router-link>**
**<router-link :to="{name: 'bye'}">Bye</router-link>**
<router-view></router-view>
</div>
</template>
查看example6-router
文件夹中的代码是什么样子。
打开页面,检查所有链接是否实际上都起作用!多次点击它们,并检查是否在点击浏览器的返回按钮时实际上会返回。这不是很棒吗?
Vuex 状态管理架构
你还记得我们的Zoo
和animal
组件的例子吗?有一些数据必须从主组件传播到子组件的子组件。如果这个孙子组件有可能以某种方式改变数据,这种改变就必须从子组件传播到其父组件,依此类推,直到数据到达主组件。不要认为你可以简单地使用v-model
绑定属性来做到这一点。Vue 在通过props
将数据绑定到子组件方面有一些限制。它是严格的单向的。因此,如果父组件改变了数据,子组件的绑定将受到影响,但反过来永远不会发生。查看 Vue 官方文档关于此的说明:vuejs.org/v2/guide/components.html#One-Way-Data-Flow
。
如果你不相信我,让我们来试试。想象一下,在我们的动物园页面示例中,我们将介绍部分提取到单独的组件中。我在谈论我们混乱的动物园页面的这部分:
如果我们想将这部分提取到单独的组件中,会怎样?
看起来很容易。我们必须声明一个组件,比如Introduction
,告诉它将接收name
属性,并将App.vue
中的 HTML 复制粘贴到这个新组件中。在App.vue
中,我们将导入这个新组件,并在 Vue 实例的components
对象中导出它。当然,我们将用<introduction>
标签替换已经复制到新组件的 HTML,并将name
属性绑定到它。这不是很容易吗?我们的Introduction.vue
文件将如下所示:
//Introduction.vue
<template>
<div>
<label for="name" :class="{green: name, red: !name}">What's your name? </label>
<input id="name" type="text" v-model.trim="name">
</div>
</template>
<script>
export default {
**props: ['name']**
}
</script>
我们的App.vue
文件将导入、导出和调用:
//App.vue
<template>
<div id="app" class="jumbotron">
<...>
**<introduction :name="name"></introduction>**
<...>
</div>
</template>
<script>
<...>
**import Introduction from './components/Introduction'**
<...>
export default {
components: {
Zoo,
**Introduction**
}
<...>
}
</script>
在第二章的代码包中查看此代码,底层-教程解释在example7-events-started/zoo
文件夹中运行npm install
和npm run
:
**cd example7-events-started/zoo**
**npm install**
**npm run dev**
查看页面。它看起来和以前一样。尝试在输入框内更改名称。首先,它不会在应该更改的其他地方更改,其次,我们的开发工具控制台充满了警告和错误:
名称没有在应该更新的地方更新,控制台充满了错误
看起来文档是正确的:我们不能更改作为属性传递给子组件的数据的值。那我们该怎么办呢?我们可以发出事件并将事件监听器附加到组件,并在事件上更改数据。我们该怎么做呢?很简单。首先,让我们将被传递的属性称为不是name
的东西,例如initialName
。然后,打开Introduction
组件并创建一个data
函数,将这个组件的name
对象绑定到initialValueprops
。这样,我们至少告诉 Vue,我们并不打算尝试从子组件更改父级的数据。因此,Introduction.vue
组件的script
将如下所示:
//Introduction.vue
<script>
export default {
**props: ['initialName']**,
data () {
return {
**name: this.initialName**
}
}
}
</script>
我们还必须改变我们在App.vue
中将名称绑定到组件的方式:
//App.vue
<introduction **:initialName="name"**></introduction>
现在,如果你检查页面,你至少会看到 Vue 不再抱怨我们试图做一些非法的事情。然而,如果我们试图更改名称,更改不会传播到父级,这是可以理解的;这些更改只影响组件本身的数据。现在,我们必须将event
附加到input
元素。这个事件将调用一个最终将事件传递给父组件的方法:
//Introduction.vue
<template>
<div>
<...>
<input id="name" type="text" v-model.trim="name"**@input="onInput"**>
</div>
</template>
<script>
export default {
<...>
methods: {
**onInput () {**
**this.$emit('nameChanged', this.name)**
**}**
}
}
</script>
现在,我们唯一需要做的就是将 nameChanged
事件监听器绑定到 <introduction>
组件,并调用会改变 App.vue
数据对象名称的方法:
//App.vue
<template>
<...>
<introduction @nameChanged="onNameChanged" :initialName="name"></introduction>
<...>
</template>
<script>
export default {
<...>
**methods: {**
**onNameChanged (newName) {**
**this.name = newName**
**}**
**}**
}
</script>
检查页面。现在,一切都和以前一样!检查本章的 example7-events/zoo
代码文件夹中的解决方案代码。
嗯,这并不是很困难,但是我们是否想要在每次更新状态时发出所有这些事件?如果我们在组件内部有组件呢?如果我们在这些组件内部有其他组件呢?这会是事件处理的地狱吗?如果我们需要改变一些东西,我们是否需要去所有这些组件?啊!有没有一个集中式存储应用程序数据的地方,可以提供一个简单的 API 来管理数据,然后我们只需要调用这个存储的方法来检索和更新数据?嗯,这正是 Vuex 的用途!Vuex 是受 Redux 启发的集中式状态管理。查看它的官方文档 vuex.vuejs.org/en/
。
现在,简而言之,Vuex store 的三个最重要的部分是 state、getters 和 mutations:
-
State:这是应用程序的初始状态,基本上是应用程序的数据
-
Getters:这正是你所想的,从 store 返回数据的函数
-
Mutations:这些是可以改变 store 上的数据的函数
一个 store 也可以有 actions。这些东西就像是对 mutations 的包装,具有更多的功能。如果你想了解它们是什么,请参考官方文档 vuex.vuejs.org/en/mutations.html
。
让我们将 Vuex store 添加到我们的 Zoo
应用程序中,以检查它的工作原理。首先,我们需要安装 vuex
。打开 第二章 的代码,从 example8-store-started/zoo
文件夹中运行 npm install
:
**cd example8-store-started/zoo**
**npm install vuex --save**
让我们创建我们的 store。首先创建一个名为 store
的文件夹,里面放有 index.js
文件。我们将把所有的 store 数据放在这个文件中。在做这之前,告诉 Vue 我们将使用 Vuex:
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
**Vue.use(Vuex)**
现在,我们可以创建一个新的 Vuex 实例。它应该接收 state
、getters
和 mutations
。让我们定义它们:
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
}
const getters = {
}
const mutations = {
}
export default new Vuex.Store({
state,
getters,
mutations
})
不错!现在,让我们将我们应用程序中的所有数据添加到状态中:
//store/index.js
const animalCodes = {
dog: '🐶',
<...>
koala: '🐼'
}
const animalsDescriptions = {
dog: 'I am a dog, I bark',
<...>
koala: 'I am a koala, I love eucalyptus!'
}
const animals = Object.keys(animalCodes)
const state = {
name: 'Olga',
animals,
animalCodes,
animalsDescriptions,
animalsForZoo: [],
sadSrc: '../static/images/sad.png',
gladSrc: '../static/images/glad.png'
}
现在,如果您在 Vue 应用程序初始化时注入存储,所有组件及其子组件都将访问this.$store
实例。让我们注入它:
//main.js
import Vue from 'vue'
import App from './App'
import store from './store'
new Vue({
el: '#app',
template: '<App/>',
components: { App },
**store**
})
现在,如果我们在App.vue
中用来自存储的计算属性替换所有数据(除了animalsForZoo
,它作为我们动物园的属性绑定),应用程序看起来将基本相同:
//App.vue
<script>
import Zoo from './components/Zoo'
import Introduction from './components/Introduction'
export default {
name: 'app',
components: {
Zoo,
Introduction
},
data () {
return {
animalsForZoo: []
}
},
**computed: {**
**name () {**
**return this.$store.state.name**
**},**
**animals () {**
**return this.$store.state.animals**
**},**
**animalCodes () {**
**return this.$store.state.animalCodes**
**},**
**sadSrc () {**
**return this.$store.state.sadSrc**
**},**
**gladSrc () {**
**return this.$store.state.gladSrc**
**}**
**},**
methods: {
onNameChanged (newName) {
this.name = newName
}
}
}
</script>
如果您打开页面,什么都没有改变。但是,我们的更改名称交互又不起作用了!
让我们添加mutation
来改变名称。Mutations 只是接收状态作为第一个参数以及您调用它们的任何其他参数的方法。因此,让我们称我们的 mutation 为updateName
,并将newName
作为第二个参数传递给它:
//store/index.js
const mutations = {
**updateName (state, newName) {**
**state.name = newName**
**}**
}
现在,我们可以使用此 mutation 来访问负责更新名称的组件Introduction.vue
中的this.$store.mutation
属性。我们只需更改onInput
方法:
//Introduction.vue
methods: {
onInput (ev) {
**this.$store.commit('updateName', ev.currentTarget.value)**
}
}
顺便说一句,我们还可以删除属性并直接从存储中传递名称,就像我们在App.vue
组件中所做的那样。然后,您可以在App.vue
组件的模板中删除绑定到introduction
组件的name
。现在,您可以用来自存储的计算属性替换绑定到 Zoo 组件的属性。看看代码变得多么优雅!例如,看看这行代码:
<introduction></introduction>
它看起来不比以下代码行好:
<introduction @nameChanged="onNameChanged" :initialName="name"></introduction>
在example8-store/zoo
代码文件夹中查看本章的最终代码第二章,Under the Hood – Tutorial Explained。请注意,我们使用了一个非常简化的版本。我们甚至没有使用任何 getters。对于更复杂的用法,我们将创建getters
和actions
,它们将位于它们自己的actions.js
和getters.js
文件中。我们还将使用mapGetters
和mapActions
助手。但是,对于基本的理解,我们所做的就足够了。请参考官方文档以了解有关 Vuex 存储及其使用方法的更多信息。
Bootstrap
既然我们几乎了解了关于 Vue.js 的一切,让我们谈谈 Bootstrap。查看官方 Bootstrap 页面v4-alpha.getbootstrap.com/
。
Bootstrap—响应式项目的框架
简而言之,Bootstrap 为您提供了一组广泛的类,可以以简单轻松的方式构建几乎任何布局。
Bootstrap 为您提供了四个最重要的东西:
-
在
v4-alpha.getbootstrap.com/content/
上有广泛的类来为几乎任何 web 元素设置样式 -
自包含组件,如警报、徽章、模态框等,位于
v4-alpha.getbootstrap.com/components/
-
一些用于样式化图像、图表、定位、样式化和添加边框的实用程序位于
v4-alpha.getbootstrap.com/utilities/
如何安装 Bootstrap?它可以从 CDN 安装:
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
实际上,这正是我们在第一章的PleaseIntroduceYourself
应用程序中所拥有的,请介绍你自己 - 教程,以及在本章中混乱的动物园应用程序中。
Bootstrap 组件
Bootstrap 有很多组件可以直接使用。
本章不会讨论它们所有,因为在本书的过程中我们会有几次机会去发现它们。让我们看一些只是为了有个概念。
让我们看看警报组件。你可能知道,警报是在成功填写某些表单时出现在页面上的漂亮元素。警报也是那些愤怒的红色元素,告诉你做错了什么。你需要在页面上创建一个警报元素,它会在一段时间后消失,或者给用户关闭它的可能性,只需点击x按钮?你可能会创建一个div
,给它添加一些类,并添加一些 JavaScript,它会在一段时间后从 DOM 树中移除元素。使用 Bootstrap,你只需将alert
类添加到你的div
中,并添加另一个类,比如alert-warning
或alert-info
来指定警报的类型:
<div class**="alert alert-success"** role="alert">
<strong>Hello!</strong> You have successfully opened this page!
</div>
<div class**="alert alert-info"** role="alert">
<strong>Hey!</strong> Important information - this alert cannot be closed.
</div>
<div class**="alert alert-warning"** role="alert">
<strong>Warning!</strong> It might be raining tonight, take your umbrella!
</div>
<div class**="alert alert-danger** alert-dismissible fade show" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<strong>Failure!</strong> Since you don't like this failure alert you can simply close it.
</div>
这段代码将产生漂亮的警报框,看起来像这样:
Bootstrap 警报 - 成功、信息、警告和危险
甚至像按钮这样的简单元素也可以使用 Bootstrap 以数百种不同的方式进行样式设置。同样,您可以有表示成功、危险区域、信息性或只是灰色的按钮。还有可能将按钮分组并使其看起来像链接。代码非常简单:
<button type="**button**" class="**btn btn-primary**">Primary</button>
<button type="**button**" class="**btn btn-secondary**">Secondary</button>
<button type="**button**" class="**btn btn-success**">Success</button>
<button type="**button**" class="**btn btn-info**">Info</button>
<button type="**button**" class="**btn btn-link**">Link</button>
<button type="**button**" class="**btn btn-primary btn-sm**">Small button</button>
此代码将生成如下所示的按钮:
Bootstrap 按钮
v4-alpha.getbootstrap.com/components/buttons/
上的官方文档页面。
关于 Bootstrap 我最喜欢的一点是,您可能有一个微不足道的元素,但是当您为其添加一些 Bootstrap 的类时,它突然变得干净而漂亮。例如,创建一个带有一些<h1>
和<p>
元素的简单页面:
<div>
<h1>Jumbotron</h1>
<p>
Lorem ipsum dolor sit amet…
</p>
</div>
它看起来正常,简单。现在,将container
类添加到父div
中。是不是好多了?还可以将jumbotron
类添加到其中。
页面之前看起来是这样的:
在添加 Bootstrap 类之前 div 中的内容
突然间,同一个页面看起来是这样的:
在添加 Bootstra 之后 div 中的内容
实际上,如果您检查我们从第一章中的PleaseIntroduceYourself
示例,Please Introduce Yourself - Tutorial (chapter1/please-introuce-yourself/src/App.vue
), 您会发现这个确切的类被用于父元素。
有很多不同的组件:弹出框,工具提示,模态框等等。我们将在本书的过程中使用所有这些组件。
Bootstrap 实用程序
您想要具有响应式浮动(向左或向右流动的元素)吗?只需将float-left
和float-right
类添加到您的元素中,您就不必再担心了:
<div class="**float-left**">Float left on all viewport sizes</div><br>
<div class="**float-right**">Float right on all viewport sizes</div><br>
<div class="float-none">Don't float on all viewport sizes</div><br>
将此代码插入到您的 HTML 页面中(或者只需查看example11-responsive-floats
文件夹中的index.html
文件),打开它,调整窗口大小。
您可以使用简单的类轻松控制大小和间距。查看v4-alpha.getbootstrap.com/utilities/sizing/
和v4-alpha.getbootstrap.com/utilities/spacing/.
甚至可以通过将d-flex
类添加到容器来启用 flex-box 行为。 d来自display。通过将更多类附加到您的 flex 元素,您可以控制 flex-box 的对齐和方向。在v4-alpha.getbootstrap.com/utilities/flexbox/
中查看。
还有很多其他实用程序可以探索,我们将在我们的旅程中了解大部分。
Bootstrap 布局
使用 Bootstrap,很容易控制系统的布局:
Bootstrap 包括几个组件和选项,用于布置您的项目,包括包装容器,强大的 flexbox 网格系统,灵活的媒体对象和响应式实用程序类。 - (v4-alpha.getbootstrap.com/layout/overview/ ) |
||
---|---|---|
--来自 Bootstrap |
Bootstrap 的网格系统非常强大且易于理解。它只是由列组成的行。一切都由具有相当自我描述性名称的类来控制,比如row
和col
。如果你只给你的列col
类,row
元素内的每一列都会有相同的大小。如果你想要不同大小的列,可以玩一下行可以由 12 列组成这个事实。所以,如果你想让一些列,比方说你的一半行,给它一个类col-6:
<div class="row">
<div class="**col**">this is a column with class col</div>
<div class="**col-6**">this is a column with class col-6</div>
<div class="**col-2**">this is a column with class col-2</div>
</div>
这段代码将产生类似于这样的结果:
网格布局系统结合了行和列类
有趣的是,如果你调整窗口大小,你的布局不会破坏。它会相应地调整大小。你不必实现任何 CSS 黑魔法来实现这一点!这就是为什么 Bootstrap 是一个大的。
结合 Vue.js 和 Bootstrap
当我们谈论 Vue 时,我们专门讨论了它的组件。当我们谈论 Bootstrap 时,我们也谈论了组件。这不是同一个概念吗?也许我们可以从 Bootstrap 组件创建 Vue 组件?也许我们可以!实际上,我们已经做到了!打开第一章的PleaseIntroduceYourself
应用程序的代码。查看我们在components
文件夹中有什么。有一样东西我们称之为MessageCard.vue
。实际上,这是一个实现了 Bootstrap 组件的 Vue 组件(v4-alpha.getbootstrap.com/components/card/
)!
打开example13-vue-bootstrap-components-started/components
文件夹。让我们将此项目用作基于 Bootstrap 警报组件创建 Vue 组件的游乐场。运行npm install
和run
:
**cd example13-vue-bootstrap-components-started/components**
**npm install**
**npm run dev**
让我们创建一个名为Alert
的 Vue 组件。该组件将包含必要的代码来模拟 Bootstrap 的警报组件行为。
在components
文件夹内创建一个名为Alert.vue
的文件,并添加template
标签。我们的警报肯定会有alert
类。但是,它的附加类(alert-danger
,alert-info
等)应该是可配置的。此外,它的标题和文本应该是从父组件传递的绑定属性。因此,警报组件的模板将如下所示:
//Alert.vue
<template>
<div class="alert"**:class="additionalClass"** role="alert">
<strong**>{{title}}**</strong>**{{text}}**
</div>
</template>
让我们将additionalClass
属性实现为一个计算属性,该属性将根据父组件传递的type
属性进行计算。因此,Alert
组件的脚本将如下所示:
//Alert.vue
<script>
export default {
**props: ['type', 'title', 'text']**,
computed: {
**additionalClass () {**
**if (!this.type) {**
**return 'alert-success'**
**}**
**return 'alert-' + this.type**
**}**
},
name: 'alert'
}
</script>
然后,我们可以从我们的主App.vue
组件中调用它:
//App.vue
<template>
<div id="app" class="container">
<img src="./assets/logo.png">
**<alert :title="title" :text="text"></alert>**
</div>
</template>
<script>
**import Alert from './components/Alert'**
export default {
data () {
return {
**title: 'Vue Bootstrap Component',**
**text: 'Isn\'t it easy?'**
}
},
name: 'app',
components: {
**Alert**
}
}
</script>
您将在页面上看到一个漂亮的警报:
我们刚刚创建了我们的 Alert Vue Bootstrap 组件
练习
为警报组件的标题启用默认值。因此,如果未传递title
,它将默认为Success。还应该在App.vue
父组件内创建组件时将type
属性绑定到组件上。根据一些任意值将此属性导出为计算属性。例如,基于一些随机数,如果它可以被3
整除,类型应为danger;如果它可以被5
整除,类型应为info;等等。
自己去看看。转到example13-vue-bootstrap-components/components
文件夹,特别是App.vue
和components/Alert.vue
组件。
继续结合 Vue.js 和 Bootstrap
因此,我们知道如何基于 Bootstrap 组件创建 Vue 组件。现在感觉好像很棒,可以将所有 Bootstrap 组件创建为 Vue 组件,并在我们的 Vue 应用程序中使用它们,而无需考虑 Bootstrap 类。想象一下 Vue 组件,例如<button-success></button-success>
或<button :type="success"></button>
。我们甚至可以基于 Bootstrap 创建一个完整的 Vue 组件库!问题是,如果已经存在,我们应该这样做吗?是的,已经有人为我们做了所有的工作。这些人已经完成了这项工作:
Bootstrap-vue 的核心团队
这些可爱的人们开发了一个叫做 Bootstrap-Vue 的东西,它确实做到了你所想的——它包含了作为 Vue.js 组件实现的完整的 Bootstrap 组件集。在bootstrap-vue.github.io/
上查看它。
让我们来看看,例如,警报组件是如何在bootstrap-vue.github.io/docs/components/alert
实现的。它比我们的警报详细一点。数据是通过组件的标签传递的,而不是作为属性,就像我们的情况一样,这也使它更灵活。在整本书的开发过程中,我们将经常使用它。
什么是 Firebase?
要了解什么是 Firebase,让我们打开它的网站firebase.google.com/
。这是我们看到的:
Google Firebase 首页
对于 Google 来说,Firebase 只是另一个云服务,就像 AWS 是亚马逊的,Azure 是微软的一样,不过简单一些,因为 Google 已经有了庞大的 Google 云平台。
如果你觉得你想在 Firebase 和 AWS 之间做出选择,不要忘记你很可能会去谷歌搜索。无论如何,已经有人为你做过了,所以在 Quora 上有这个问题:www.quora.com/Which-is-better-cloud-server-Amazon-AWS-or-Firebase
。
我会说它更类似于 Heroku——它允许您轻松部署您的应用程序并将其与分析工具集成。如果您已经阅读了《Learning Vue.js 2》一书(www.packtpub.com/web-development/learning-vuejs-2
),那么您已经知道我有多么喜欢 Heroku 了。我甚至有 Heroku 袜子!
我美丽的 Heroku 袜子
然而,我觉得 Google Firebase 控制台也非常好用和简单。它还提供后端作为服务。这个后端为您的 Web 和移动应用程序共享,这在开发跨平台和跨设备应用程序时非常有帮助。Firebase 提供以下服务:
-
身份验证:这使用 Firebase API 来使用不同的提供者(Facebook、Google、电子邮件等)对用户进行身份验证。
-
数据库:这使用 Firebase 数据库 API 来存储和检索数据。无需在不同的数据库提供商之间进行选择,也无需建立连接。只需直接使用 API。
-
托管:这使用简单的 shell 命令托管和部署您的应用程序。
-
存储:这使用简单的 API 托管静态文件。
再次强调,如果您在考虑如何将您的 Vue 应用程序与 Firebase API 集成,那么请停止思考,因为已经有人为您完成了这项工作。在使用 Firebase 控制台创建项目后,您可以简单地使用 Firebase 的vuefire
包装器来连接到您的数据库并获取数据。请访问github.com/vuejs/vuefire
查看。实际上,这正是我们在第一章的PleaseIntroduceYourself
应用程序中所做的。查看位于App.vue
组件内的代码:
//PleaseIntroduceYourself/src/App.vue
<script>
**import Firebase from 'firebase'**
let **config** = {
apiKey: '... ',
...
messagingSenderId: '...'
}
let app = **Firebase.initializeApp**(config)
let db = **app.database()**
let messagesRef = **db.ref('messages')**
export default {
...
**firebase: {**
**messages: messagesRef.limitToLast(100)**
**}**
}
</script>
Firebase 对象中导出的所有内容都可以通过this
关键字访问,就像我们访问data
或computed
属性一样。我们将在整本书中开发的应用程序中使用vuefire
来更好地理解它的工作原理。
总结
在本章中,我们熟悉了 Vue.js、Bootstrap 和 Firebase。我们还分析了将 Vue.js 与 Bootstrap 以及 Vue.js 与 Firebase 集成的工具。
因此,现在,我们熟悉了使用单文件组件、Bootstrap 的网格系统、组件和 CSS 辅助工具来使我们的生活更轻松,并利用 Google Firebase 控制台的可能性来构建 Vue.js 应用程序。
此外,我们知道如何初始化 Vue.js 项目,并使用 Vue 指令、组件、存储和路由。
您还学会了如何利用 Bootstrap 的网格系统来实现应用程序布局的响应性。
最后但同样重要的是,您学会了如何在 Vue 应用程序中使用 Firebase API,使用vuefire
绑定。
随着本章的结束,我们旅程的第一部分也告一段落。
在下一章中,我们将深入了解实现细节。就像潜水氧气瓶一样,我们将带走您迄今为止学到的一切!
因此,我们将开始开发整本书中将构建的应用程序,直到准备部署。我们将:
-
定义应用程序将要做什么及其要求
-
定义我们为谁构建应用程序
-
为应用程序构建基本的模型
-
使用 Vue 命令行界面搭建应用程序
你和我一样兴奋吗?那么,让我们进入下一章吧!
第三章:让我们开始吧
在上一章中,我们讨论了本书中将使用的三种主要技术,以构建我们的应用程序。我们深入探讨了 Vue.js 的许多内容;我们介绍了 Bootstrap 的一些功能,并检查了使用 Google Firebase 控制台可以实现什么。我们知道如何使用 Vue.js 从头开始创建应用程序。我们知道如何在 Bootstrap 的帮助下使其美观,也知道如何使用 Google Firebase 将其部署到实时环境中!这意味着我们已经百分之百准备好开始开发我们的应用程序了!
编写应用程序是一个有趣、具有挑战性和令人兴奋的过程……只要我们知道我们要编写什么,对吧?为了知道我们将要编写什么,我们必须定义应用程序的概念、其要求和目标用户。在本书中,我们不会完全涉及设计构建的整个过程,因为对此,您有很多其他书籍,因为这是一个大科学。
在本书中,特别是在本章中,在进行实施之前,我们至少会定义一组角色和用户故事。因此,在本章中,我们将执行以下操作:
-
阐述我们将用应用程序解决的问题
-
定义一些角色和用户故事
-
从用户故事中提取名词和动词
-
绘制将定义我们应用程序的主要屏幕和区域的模拟图
陈述问题
世界上有许多时间管理技术。一些大师和专业人士已经就如何有效管理时间,以便您高效并且所有的 KPI 值都高于任何可能的生产力基准进行了大量演讲。其中一些演讲真的很棒。当涉及到时间管理演讲时,我总是建议观看 Randy Pausch 在youtu.be/oTugjssqOT0
上的演讲。
说到时间管理技术,有一种我特别喜欢的流行技术,我觉得非常简单易用。它被称为番茄钟(en.wikipedia.org/wiki/Pomodoro_Technique
)。这种技术包括以下原则:
-
在一定时间内工作,没有任何干扰。这段时间可以是 20 到 25 分钟,被称为番茄钟
-
在工作的番茄钟后,您有 5 分钟的休息时间。在这个休息时间里,您可以做任何您想做的事情——查看电子邮件、社交网络等等
-
在完成四个番茄钟后,你有权享受一个持续 10 到 15 分钟的较长休息时间。
番茄钟有许多实现方式。其中一些允许你配置工作番茄钟和短暂和长暂休息的时间。有些在工作番茄钟期间阻止社交网络页面;有些会发出噪音。在《学习 Vue.js 2》一书中,我们还构建了一个简单的番茄钟,它在工作期间发出棕色噪音,并在短暂休息期间显示随机小猫。
如果你正在阅读这本书,那么很可能你是一名开发人员,你一天中的大部分时间都是坐着,或者可能是站着,因为站立式办公桌如今非常流行。你在工作日(或夜晚)中改变姿势的频率有多高?你是否有背部问题?你去健身房吗?你喜欢慢跑吗?你多久在家锻炼一次?作为一名开发人员需要高度集中注意力,我们很容易忘记一点关于自己的事情。
在这本书中,我们将再次构建一个番茄钟。这次,它不仅会尝试解决时间管理问题,还会解决健身管理问题。它不会让你在休息期间做任何你想做的事情或者显示一些随机小猫,而是会告诉你做简单的锻炼。锻炼的种类从非常简单的头部旋转练习到俯卧撑和弹跳。用户可以根据他们所在办公室的类型选择一组自己喜欢的锻炼。用户还可以添加新的锻炼。锻炼也可以被评分。
因此,我们将实现的番茄钟的主要原则如下:
-
不间断地工作。专注于你正在做的事情。
-
在休息期间进行锻炼。
-
合作并添加新的令人兴奋的锻炼,可以被你和应用程序的其他用户使用。
需求收集
现在我们知道要构建什么,让我们为应用程序定义一系列要求。该应用程序主要用于显示计时器和展示锻炼。因此,让我们定义它必须具备的功能。以下是我的功能要求列表:
-
该应用程序应该显示倒计时计时器。
-
倒计时计时器可以从 25 到 0 分钟,从 5 到 0 分钟,或者从 10 到 0 分钟。
-
在应用程序的任何时刻都可以启动、暂停和停止倒计时计时器。
-
当时间到达 0 并且下一个休息时间或工作番茄开始时,应用程序应该发出一些声音。
-
该应用程序应该在短暂和长时间休息期间显示一个锻炼项目。可以跳过当前的锻炼项目并切换到下一个。也可以在休息期间完全跳过锻炼项目,只是盯着小猫。也可以标记给定的锻炼项目为已完成。
-
该应用程序必须提供认证机制。经过认证的用户可以配置番茄定时器,向系统添加新的锻炼项目,并查看他们的统计数据。
-
认证用户的统计数据显示每天、每周和每月完成的锻炼项目数量。
-
经过认证的用户可以配置番茄工作法定时器,如下所示:
-
为长时间工作的番茄定时器选择一个在 15 到 30 之间的值
-
为短暂休息定时器选择一个在 5 到 10 之间的值
-
为长休息定时器选择一个在 10 到 15 之间的值
-
经过认证的用户可以配置他们喜欢的锻炼项目集进行显示。
-
经过认证的用户可以创建新的锻炼项目并将其添加到系统中。
-
每个锻炼项目包括四个部分:标题、描述、图片和评分。
我还有一个非功能需求的基本清单,包括两个项目:
-
该应用程序应该使用持久存储来存储其数据——在我们的情况下是 Firebase 的实时数据库
-
该应用程序应该是响应式的,并且可以在多个平台和设备上运行
我想这已经足够支持我们的番茄工作法的功能了。或者,既然涉及健身,也许我们可以称之为 PoFIToro?或者,也许,既然我们的身体得到了一些好处,让我们称之为ProFitOro。
人物角色
通常,在开发应用程序之前,我们必须定义其目标用户。为此,我们要与应用程序的潜在用户进行多次问卷调查。问卷调查通常包括关于用户个人数据的问题,如年龄、性别等。还应该有关于用户使用模式的问题——操作系统、桌面或移动设备等。当然,还应该有关于应用程序本身的问题。例如,对于 ProFitOro 应用程序,我们可以问以下问题:
-
你每天在办公室花费多少小时?
-
你在工作日里在办公室坐多久?
-
你多久进行一次像慢跑、健身锻炼等体育活动?
-
你是在办公室工作还是在家工作?
-
你的工作场所有没有可以做俯卧撑的地方?
-
你有背部问题吗?
收集完所有问卷后,用户根据相似的模式和个人数据被分成不同的类别。然后,每个用户的类别形成一个单一的角色。我将在这里为 ProFitOro 应用程序留下四个角色。
让我们从一个叫做 Alex Bright 的虚构角色开始:
Alex Bright
年龄:32 岁
性别:男性
教育:硕士
职业:软件工程师,全职
使用模式:在办公室工作,使用运行 Ubuntu 的笔记本电脑和 iPhone。
最喜欢的浏览器:Google Chrome
健康和健身:每个月跑 5 公里。偶尔感到背部疼痛
让我们继续我们下一个虚构的角色—Anna Kuznetsova。
Anna Kuznetsova
年龄:22 岁
性别:女性
教育:学士学位
职业:学生
使用模式:大部分时间在家使用运行 Windows 的台式机和安卓手机。
最喜欢的浏览器:Mozilla Firefox
健康和健身:每周去健身房三次。没有任何健康问题
在写这本书的时候,我的一个朋友刚刚来我们家做客。他叫 Duarte,但我们取笑他叫 Dwart。他一露面,下一个角色就诞生了(请注意,我们的朋友 Duarte 离 45 岁还很远):
Dwart Azevedo
年龄:45 岁
性别:男性
教育:博士
职业:副总工程师,全职
使用模式:在办公室工作,经常在共享工作空间和家里工作。使用 MacBook Pro 和 iPhone,并且在工作时花费大量时间坐着。
健康和健身:定期在家做锻炼。有时感到背部疼痛。
我丈夫 Rui 在一家名为 Gymondo 的在线健身公司工作。那里有一个名叫 Steve 的出色健身教练。他会把你推到极限。每次我和这个家伙一起做锻炼,之后我甚至都无法走路。这就是下一个角色诞生的原因:
Steve Wilson
年龄:35 岁
性别:男性
职业:健身教练,全职
使用模式:家里的 Windows 台式机
健康和健身:从不感到疼痛,每天每小时都训练
我们可以看到,我们的用户共同之处在于他们都花了一些时间保持相同的姿势(坐着),他们的工作需要一些专注力和可能需要时间管理技巧,他们有时需要改变姿势以防止背部问题。
用户故事
在定义了我们的用户之后,让我们写一些用户故事。当涉及编写用户故事时,我只需闭上眼睛,想象自己是这个人。让我们从德瓦特·阿塞韦多开始尝试这种心灵锻炼:
德瓦特·阿塞韦多
德瓦特的工作日包括会议、电话、视频会议和文书工作。今天,他非常忙碌,有访谈和会议。最后,他有几个小时可以处理整整一周等待他的文书工作。德瓦特希望能够以最有效率的方式度过这几个小时。他打开 ProFitOro 应用,点击“开始”,然后开始工作。在完成文书工作后,他点击“停止”,在 ProFitOro 中检查自己的统计数据,并感到高兴。尽管他的工作时间只有两个小时,但他能够完成他计划完成的一切。
因此,我们可以提出一个正式的用户故事,如下所示:
作为一个经过验证的用户,我想要在 ProFitOro 上查看我的统计页面,以便了解我的工作日的完整性。
让我们继续介绍我们的健身教练史蒂夫·威尔逊。
史蒂夫·威尔逊
史蒂夫是一名健身教练。他对人体、营养知识以及如何正确进行锻炼了如指掌。他有很多朋友——都是使用 ProFitOro 应用的程序员。他在工作结束后回到家,登录并打开 ProFitOro 应用,点击“锻炼”部分,并添加了新的背部锻炼。
因此,一个新的正式用户故事可以是这样的:
作为一名健身教练,我希望能够轻松添加新的锻炼,以丰富 ProFitOro 应用的锻炼内容。
让我们继续介绍我们的学生安娜·库兹涅佐娃。
安娜·库兹涅佐娃
安娜是一名学生。目前,她正在经历考试期。她每天都需要为考试学习。在夏天,当所有朋友都出去玩时,要专心看书并不容易。有人告诉她 ProFitOro 应用程序可以帮助她集中注意力,所以她开始在没有注册的情况下使用它。过了一会儿,她意识到这实际上有助于她集中注意力。使用了一段时间后,她想要检查自己工作了多少时间,做了多少练习。然而,这些信息对非注册用户不可用。因此,她点击应用程序首页的注册按钮,用她的电子邮件注册,现在她可以访问她的统计数据了。
因此,又出现了另一个用户故事:
作为非注册用户,我希望能够注册自己,以便能够登录应用程序并访问我的统计数据。
检索名词和动词
从用户故事中检索名词和动词是一项非常有趣的任务,它可以帮助你意识到你的应用程序由哪些部分组成。对于那些喜欢统一建模语言(UML)的人来说,当你从用户故事中检索名词和动词后,你几乎已经完成了类和实体关系图!不要低估要检索的名词和动词的数量。把它们都写下来——真的!之后可以删除那些没有意义的词。所以,让我们开始吧。
名词
我能从我的用户故事中检索到的名词如下:
-
工作日
-
会议
-
电话
-
面试
-
小时
-
天
-
周
-
应用程序
-
统计
-
工作时间
-
计划
-
健身
-
教练
-
人体
-
营养
-
锻炼
-
部分
-
练习
-
电子邮件
-
数据
-
页面
-
注册
动词
我能从用户故事中检索到的动词如下:
-
包括
-
忙碌
-
打开
-
花时间
-
开始
-
暂停
-
停止
-
检查
-
完成
-
计划
-
添加
-
创建
-
注册
-
认证
-
登录
-
集中
我们有注册、登录和认证等动词,以及电子邮件和注册等名词,这意味着该应用程序可能会有注册和非注册两种使用方式。这意味着第一页可能会包含登录和注册区域,并且以某种方式,它还应该包含一个链接到可以在任何身份验证之前使用的应用程序。
然后,我们有动词如开始,暂停和停止。这些是适用于我们的番茄钟的主要动作。我们可以启动应用程序,我们可以暂停它,当然,我们可以在工作日的任何时候停止它。顺便说一句,工作日是我们检索到的名词之一。这意味着我们的应用程序的主页面将包含倒计时计时器,可以启动、暂停和停止。
我们有很多与健身相关的名词——健身本身,人体,锻炼,训练等等。这实际上是我们试图通过这个应用程序实现的目标——在番茄休息时训练我们的身体。因此,在工作休息时进行锻炼。注意还有动词如检查和完成。因此,锻炼可以被完成,并且某事可以被检查,表明用户已经完成了锻炼。这就是为什么,这个番茄间隔表示应该包含一个复选框。它还应该包含一个链接,指向下一个锻炼,以防你在当前锻炼上花费的时间较少。它还可能有一个跳过按钮,以防你在这个间隔期间完全不喜欢这个锻炼。
查看名词统计。这并不意味着我们必须讨论平均数、抽样、人口和其他一些年前在学校学到的东西。在我们的语境中,名词统计意味着用户应该能够访问他们在一天、一周或一个月内进行的锻炼的统计数据(注意名词列表中实际上有天和周这两个名词)。因此,将会有另一个屏幕显示用户的统计数据。
计划和工作时间。一些事情可以被计划并可能被配置。这是有道理的——一些用户可能觉得对于他们来说,工作时间应该是 30 分钟而不是 25 分钟。有些人可能需要更短的工作间隔,比如 15 或 20 分钟。这些数值应该是可配置的。因此,我们又来到了另一个屏幕——配置。在这个屏幕上,用户将能够重设他们的密码并配置他们的番茄钟工作时间,以及短时和长时休息时间。
查看动词创建和添加与名词锻炼相结合。我们已经讨论过番茄钟休息期间出现的锻炼是应用程序用户协作工作的结果。因此,应该有一个部分(检查名词列表中是否也包含部分这个词),允许可视化现有的锻炼和创建新的锻炼。
因此,根据先前的分析,我们将涉及 ProFitOro 应用程序的六个重要领域:
-
用户可以注册或登录的第一页。此页面还允许用户在未经身份验证的情况下开始使用应用程序。
-
番茄钟计时器所在的主页面。
-
主页面上显示番茄钟休息时间并显示在此休息期间要进行的锻炼的计时器。
-
可以更改用户设置,如用户名和个人资料图片,并配置番茄钟计时器的区域。
-
可以观察每天、每周或每月进行的锻炼的统计数据的区域。
-
显示所有现有锻炼并允许用户添加新锻炼的部分。
现在我们已经有了如何概述我们的应用程序的想法,我们可以开始考虑创建一些模型,以便更好地了解它,并尽早预见可能的问题。
模型
现在我们已经有了所有的名词和动词,我们可以开始在应用程序的所有部分之间建立联系。我们实际上可以开始准备一些模型。坐下来和某人讨论,解释你的想法,并收集反馈。提出问题。回答问题。使用白板,使用便条。使用纸张:绘制,丢弃,然后重新绘制。
我有一个名叫 Safura 的好朋友。她是一名在柏林学习计算机科学的在职学生,我们在同一个团队一起工作。她对 UI/UX 话题很感兴趣。实际上,她将在人机交互(HCI)领域撰写她的硕士论文。所以,我们坐在一起,我向她解释了 ProFitOro 的想法。你无法想象她提出的问题数量。然后,我们开始绘制。然后重新绘制。“如果……?”再次重绘。
这是纸上的第一批模型的样子:
ProFitOro 应用程序的纸上的第一批模型
在所有头脑风暴、绘图和重绘之后,Safura 为我准备了一些不错的模型。她使用WireframeSketcher进行此操作(wireframesketcher.com/
)。
第一页-登录和注册
用户看到的第一页是允许他们登录、注册或开始使用 ProFitOro 而无需注册的页面。页面如下所示:
ProFitOro 应用程序的登录页面
措辞、颜色和图形尚未最终确定。模型的最重要部分是元素的定位。您仍将与设计师合作,并且仍然必须使用您喜爱的编程语言(对我们来说是 JavaScript/HTML/CSS)来实现这一点。模型有助于您记住应用程序的重要细节。
显示番茄钟的主页面
应用程序的下一个模型显示了番茄钟启动时的情况:
应用程序的主屏幕——工作计时器已启动
正如您所看到的,我们的目标是拥有简单清晰的界面。标题区域有四个链接。它们如下:
-
链接到设置页面:它将打开用户的个人设置。用户可以更改个人数据,如密码、个人资料照片和番茄钟设置。
-
链接到统计页面:它将打开包含用户统计数据的弹出窗口。
-
链接到锻炼:这将打开包含所有可用锻炼的页面。该页面还将提供添加新锻炼的可能性。
-
注销链接
这些链接仅对已注册和经过身份验证的用户启用。对于匿名用户,这些链接将被禁用。
休息时锻炼
当工作的番茄钟结束时,将开始为期五分钟的小休息。在这段休息时间内,用户可以选择进行简单的小型锻炼:
在短暂的休息时间内,用户有可能进行小型锻炼
正如您所看到的,锻炼区域提供以下内容:
-
首先,您可以完成锻炼并单击完成。此操作将将您的锻炼存储到您的统计数据中。
-
如果出于某种原因,您不想做建议的锻炼,但仍想做些事情,那么您可以单击下一个。这将为您提供一个新的随机选择的锻炼。
-
如果出于某种原因,您感到疲倦,根本不想锻炼,那么您可以点击给我看小猫!按钮,它将呈现一个区域,其中有随机的小猫,您可以盯着它们直到休息时间结束。
设置
如果用户想要更改他们的个人设置或番茄工作法的时间间隔,用户必须前往设置区域。这个区域看起来像这样:
ProFitOro 的设置区域
正如您所看到的,设置区域允许我们更改用户的个人数据并配置番茄工作法的时间。
统计
如果用户想要查看他们的统计数据并点击统计菜单按钮,将打开一个弹出窗口,其中显示了用户每天、每周和每月完成的锻炼的图表:
统计数据弹出窗口
锻炼
最后,如果您觉得您可能有一个在应用程序中不存在的锻炼想法,您可以随时打开锻炼部分并添加新的锻炼:
锻炼部分
正如您所看到的,在锻炼部分,用户可以查看整个锻炼列表,搜索它们,并编制自己的锻炼列表。默认情况下,应用程序中列出的所有锻炼将形成您的日常锻炼计划。然而,在这个区域,可以切换它们的选择。配置将为每个用户存储。
还可以创建新的锻炼。添加新的锻炼包括提供标题、描述和图片。
这些模型并不决定应用程序的最终外观。它们只是帮助我们定义首要任务和如何放置元素。在过程中,最终的位置和外观可能会发生很大变化。尽管如此,我们有严格的指导方针,这是项目管理和开发这个阶段最重要的成果。
标志
您可能已经注意到所有屏幕都包含一个漂亮的标志。这个标志是由我的一个非常好的朋友、一位名叫 Carina 的优秀平面设计师设计的。我已经在学习 Vue.js 2书中提到过这个标志,但我很乐意再次提到它。就在这里:
ProFitOro 的标志是由我的朋友 Carina 设计的
是不是很好?它是否反映了我们的应用程序将允许我们做的事情——结合番茄工作法和小锻炼?我们甚至定义了 ProFitOro 的座右铭:
工作期间休息。休息期间锻炼。
总结
在本章中,我们应用了设计应用程序用户界面的基本原则。我们进行了头脑风暴,定义了我们的角色并编写了用户故事,从这些故事中提取了名词和动词,并最终为我们的应用程序设计了一些不错的模型。
在下一章中,我们将开始实现我们的 ProFitOro。我们将使用 Vue.js 来搭建应用程序并将其拆分为重要的组件。因此,在下一章中我们将做以下事情:
-
使用
webpack
模板使用 vue-cli 搭建 ProFitOro 应用程序 -
将应用程序拆分为组件并为应用程序创建所有必要的组件
-
使用 Vue.js 和 Bootstrap 实现一个基本的番茄钟定时器
第四章:让它成为番茄钟!
上一章以一组ProFitOro应用程序的模拟图结束。我们之前已经定义了应用程序应该做什么;我们还确定了一个平均用户配置文件,并且准备好实现它。在这一章中,我们将最终开始编码。因此,在这一章中,我们将做以下事情:
-
使用
webpack
模板使用 vue-cli 搭建ProFitOro -
定义所有需要的应用程序组件
-
为所有组件创建占位符
-
实现一个组件,负责使用 Vue.js 和 Bootstrap 渲染番茄钟计时器
-
重新审视三角函数的基础知识(你没想到会有这个吧?)
创建应用程序的骨架
在一切之前,让我们确保我们至少在节点版本上是一致的。我使用的 Node.js 版本是6.11.1。
让我们从为我们的应用程序创建一个骨架开始。我们将使用webpack
模板的 vue-cli。如果你不记得vue-cli是什么以及它来自哪里,请查看官方 Vue 文档,网址为github.com/vuejs/vue-cli
。如果由于某种原因你还没有安装它,请继续安装:
**npm install -g vue-cli**
现在,让我们引导我们的应用程序。我相信你记得,为了使用vue-cli
初始化应用程序,你必须运行vue init
命令,后面跟着要使用的模板名称和项目本身的名称。我们将使用webpack
模板,我们的应用程序名称是profitoro
。所以,让我们初始化它:
**vue init webpack profitoro**
在初始化过程中,会有一些问题需要回答。只需一直按Enter键回答默认的Yes
即可;因为对于这个应用程序,我们需要一切:代码检查、vue-router、单元测试、端到端测试,全部都需要。这将会很庞大!
你的控制台输出应该几乎和我的一样:
应用程序初始化时的控制台输出
现在,在新创建的profitoro
目录中运行npm install
:
**cd profitoro**
**npm install**
让我们安装sass
加载器,因为我们将使用sass
预处理器来为我们的应用程序添加样式:
**npm install sass-loader node-sass --save-dev**
最后,我们准备运行它:
**npm run dev**
您的新 Vue 应用程序已准备就绪。为了让我们的 ProFitOro 有一个干净的工作环境,删除与默认安装过程一起安装的Hello
组件相关的一切。作为替代方案,只需打开第四章 让它番茄钟!的代码文件,并从chapter4/1/profitoro
文件夹中获取样板代码。
定义 ProFitOro 组件
我们的应用程序由两个主要屏幕组成。
其中一个屏幕是所谓的登陆页面;该页面由以下部分组成:
-
一个标志
-
一个标语
-
一个认证部分
-
一个可供未注册用户使用的应用程序链接
从图表上看,这是我们组件在屏幕上的位置:
包含标志、标语、认证部分和应用程序链接的登陆页面
第二个屏幕是主应用程序屏幕。该屏幕包含三个部分:
-
一个页眉
-
一个页脚
-
内容
内容部分包含番茄钟计时器。如果用户已经认证,它将包含设置、锻炼和统计信息:
包含页眉、页脚和内容的主应用程序屏幕
让我们创建一个名为components
的文件夹,以及名为main
、landing
和common
的子文件夹,用于相应的子组件。
登陆页面和主页面的组件将存放在components
文件夹中;其余的 11 个组件将分布在相应的子文件夹中。
对于每个定义的组件文件,添加template
、script
和style
部分。在style
标签中添加lang="sass"
属性,因为正如我之前提到的,我们将使用sass
预处理器来为我们的组件添加样式。因此,例如,HeaderComponent.vue
将如下所示:
//HeaderComponent.vue
<template>
<div>**Header**</div>
</template>
<script>
export default {
}
</script>
<style scoped **lang="sass"**>
</style>
因此,我们有 13 个准备好填充必要数据的组件占位符。这些组件将被使用和重复使用。这是因为 Vue 组件是可重用组件,这就是它们如此强大的原因。在开发过程中,我们将不可避免地添加更多组件和子组件,但这是我们的基础:
ProFitOro 的 13 个基础组件
检查我们在chapter4/2/profitoro
文件夹中的基础组件。
让我们也通过填充所需的子组件来准备我们的LandingPage
和MainContent
组件。在此之前,为每个子文件夹添加一个index.js
文件,并在其中导出相应子文件夹的内容。这将使之后的导入更容易。因此,从common
文件夹开始,并添加以下内容的index.js
文件:
//common/index.js
export {default as Logo} from './Logo'
对sections
,main
和landing
文件夹重复相同的操作。
现在我们可以组合我们的登陆页面和主要内容组件。让我们从LandingPage.vue
开始。这个组件包括一个标志,一个认证部分,一个指向应用程序的链接和一个标语。导入所有这些组件,将它们导出到components
对象中,并在template
中使用它们!我们在index.js
文件中导出这些组件的事实使我们可以像下面这样导入它们:
//LandingPage.vue
import {Authentication, GoToAppLink, Tagline} from './landing'
import {Logo} from './common'
现在我们可以在LandingPage
组件的components
对象中使用这些导入的组件。顺便说一句,你有没有见过同一个短语中有这么多组件这个词?"组件,组件,组件",导出的对象看起来如下:
//LandingPage.vue
export default {
components: {
Logo,
Authentication,
GoToAppLink,
Tagline
}
}
在components
对象中导出后,所有这些组件都可以在模板中使用。请注意,所有驼峰命名的内容在模板中都会变成短横线命名。因此,我们的GoToAppLink
看起来会像go-to-app-link
。因此,我们模板中的组件将如下所示:
<logo></logo>
<tagline></tagline>
<authentication></authentication>
<go-to-app-link></go-to-app-link>
因此,我们整个LandingPage
组件现在将有以下代码:
//LandingPage.vue
<template>
<div>
**<logo></logo>**
**<tagline></tagline>**
**<authentication></authentication>**
**<go-to-app-link></go-to-app-link>**
</div>
</template>
<script>
import {**Authentication, GoToAppLink, Tagline**} from './landing'
import {**Logo**} from **'./common'**
export default {
components: {
**Logo,**
**Authentication,**
**GoToAppLink,**
**Tagline**
}
}
</script>
<style scoped lang="sass">
</style>
让我们告诉App.vue
来渲染这个组件:
//App.vue
<template>
<div id="app">
<h1>Welcome to Profitoro</h1>
**<landing-page></landing-page>**
</div>
</template>
<script>
**import LandingPage from './components/LandingPage'**
export default {
name: 'app',
components: {
LandingPage
}
}
</script>
检查页面。你能看到你的组件吗?我相信你可以:
LandingPage 组件
现在,我们只需实现相应的组件,我们的登陆页面就准备好了!
练习
对于MainContent
组件也要做同样的操作——导入和导出所有必要的子组件,并将它们添加到模板中。之后,在App.vue
中调用MainContent
组件,就像我们刚刚在LandingPage
组件中所做的那样。如果有疑问,请检查chapter4/3/profitoro
文件夹中的代码。
实现番茄钟计时器
我们应用程序中最重要的组件之一,毫无疑问,就是番茄钟计时器。它执行应用程序的主要功能。因此,首先实现它可能是一个好主意。
我在想一种圆形计时器。类似这样的:
圆形计时器将被实现为番茄钟
随着时间的推移,突出显示的扇形将逆时针移动,时间也将倒计时。为了实现这种结构,我考虑了三个组件:
-
SvgCircleSector:此组件将只接收一个角度作为属性,并着色 SVG 圆的相应扇形。
-
CountDownTimer:此组件将接收要倒计时的秒数,实现计时器并在每次计时器更新时计算要传递给
SvgCircularComponent
的角度。 -
PomodoroTimer:我们已经引导了这个组件。此组件将负责使用初始时间调用
CountDownTimer
组件,并根据当前工作的番茄钟或休息间隔更新到相应的秒数。
SVG 和三角函数
让我们首先定义我们的SvgCircleSector
组件。这个组件将接收angle
和text
作为属性,并绘制一个具有给定角度突出显示扇形的 SVG 圆。在components/main/sections
文件夹内创建一个名为timer
的文件夹,然后在其中创建一个SvgCircleSector.vue
文件。定义template
、script
和style
所需的部分。您还可以导出props
,其中包括此组件将从其父级接收的angle
和text
属性:
//SvgCircleSector.vue
<template>
<div>
</div>
</template>
<script>
export default {
**props: ['angle', 'text']**
}
</script>
<style scoped lang="scss">
</style>
那么,我们如何使用 SVG 绘制圆并突出显示其扇形?首先,让我们绘制两个圆:一个在另一个内部。让我们将较大的圆半径设为100px
,较小的圆半径设为90px
。基本上,我们必须提供中心、x和y坐标、半径(r
)和fill
属性。查看 SVG 中关于圆的文档,网址为developer.mozilla.org/en-US/docs/Web/SVG/Element/circle
。我们最终会得到类似于这样的东西:
<svg width="200" height="200" >
<circle r="100" cx="100" cy="100" fill="gray"></circle>
<circle r="90" cx="100" cy="100" fill="lightgray"></circle>
</svg>
因此,我们得到了两个圆,一个在另一个内部。
使用 SVG 圆元素绘制的两个圆
现在,为了绘制圆的突出显示扇形,我们将使用path SVG 元素(developer.mozilla.org/en-US/docs/Web/SVG/Element/path
)。
使用 SVG 路径元素,您可以绘制任何您想要的东西。它的主要属性称为d
,基本上是一种使用 SVG 特定领域语言编程路径的方式。例如,这是如何在我们的圆内绘制一个三角形:
<path d="M100,100 V0 L0,100 H0 z"></path>
这些代码代表什么? M
代表移动,L
代表线,V
代表垂直线,H
代表水平线,z
代表在此停止路径。因此,我们告诉我们的路径首先移动到100
,100
(圆心),然后画一条垂直线直到达到y轴的0
点,然后画一条线到0
,100
x,y坐标,然后画一条水平线直到达到100
x坐标,然后停止。我们的二维坐标区由x和y轴组成,其中x从左到右从0
开始,直到200
,y从上到下从0
开始,直到200
。
这是我们小圆坐标系的中心和极端点的(x, y)坐标的样子:
标记的点代表 SVG 圆的(x,y)坐标,圆心在(100,100)
因此,如果我们从(100
,100
)开始,画一条垂直线到(100
,0
),然后画一条线到(0
, 100
),然后画一条水平线直到(100
,100
),我们最终得到一个在我们的圆的左上象限内绘制的直角三角形:
路径在圆内绘制一个三角形
这只是对路径 SVG 元素的一个小介绍,以及它可以实现的内容。然而,我们仍然需要绘制一个圆形扇区,而不仅仅是一个三角形。为了使用路径绘制扇区,我们可以在d
属性内部使用A
命令。 A
代表弧。这可能是路径中最复杂的命令。它接收以下信息:rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y。
在我们的情况下,前四个属性始终可以是100
,100
,0
,0
。如果您想了解原因,请查看 w3c 关于弧路径属性的文档www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
。
对我们来说,最重要的属性是最后三个。sweep-flag表示弧的方向;它可以是0
或1
,分别表示顺时针和逆时针方向。在我们的情况下,它将始终为0,因为这是我们希望弧线绘制的方式(逆时针)。至于最后的x和y值,这些值决定了弧线的停止位置。因此,例如,如果我们想在90度处绘制左上方的扇形,我们将在(0
, 100
)坐标处停止弧线—x为0
,y为100
—因此我们的d
属性将如下所示:
d="M100,100 L100,0 **A**100,100 0 0,0 **0,100** z"
包含两个圆和扇形的整个 SVG 元素将如下所示:
<svg width="200" height="200" >
<circle r="100" cx="100" cy="100" fill="gray"></circle>
<circle r="90" cx="100" cy="100" fill="lightgray"></circle>
<path id="sector" fill="darkgray" opacity="0.6" **d="M100,100 L100,0 A100,100 0 0,0 0, 100 z"**></path>
</svg>
这段代码产生了以下结果:
用 SVG 元素的路径绘制的 90 度扇形
我们实际上必须将这个d
属性定义为一个动态属性,其计算值将取决于。为了表示这一点,我们必须使用v-bind
,后面跟着一个分号和属性:v-bind:d
,或者简单地写为:d
。让我们给相应的属性路径命名,并将其添加到我们组件的导出对象computed
中:
//SvgCircleSector.vue
<template>
<div>
<svg class="timer" width="200" height="200" >
<...>
<path class="segment" **:d="path"**></path>
</svg>
</div>
</template>
<script>
function **calcPath** (angle) {
let d
**d = "M100,100 L100,0 A100,100 0 0,0 0, 100 z"**
return d
}
export default {
props: ['angle', 'text'],
computed: {
path () {
**return calcPath(this.angle)**
}
}
}
</script>
我引入了一个名为calcPath
的函数,它将确定我们的路径字符串。目前,它返回的路径将突出显示90度的区域。
我们几乎完成了。我们实际上可以绘制一个段,但缺少的是能够为任何角度绘制一个段的能力。我们的SvgCircleSector
组件将接收一个角度作为属性。这个角度不总是等于90度。我们应该想出一个公式,根据angle
来计算结束的x和y坐标。如果你对重新学习基本的三角函数不感兴趣,可以跳过这部分,继续阅读本节的结尾。
这是我计算小于 180 度角的x,y坐标的方法:
要计算角度α的(x,y)坐标,我们需要计算直角三角形的 a 和 b 边。
从图中我们可以看到:
x = 100 – b
y = 100 – a
因此,我们只需要计算a
和b
。这是一项简单的任务。我们可以通过知道角度和斜边来计算直角三角形的两条腿。斜边c
等于圆的半径(在我们的例子中为100
)。与角度相邻的腿a
等于c * cosα
,而与角度相对的腿b
等于c * sin
α
。因此:
x = 100 – 100 * sinα
y = 100 – 100 * cosα
对于大于 180 度的角度,我们有以下方案:
对于大于 180°的角度,我们还必须计算右三角形的两边
我可以告诉你一个秘密吗?我真的很不擅长画这种图。我尝试过从纸上的草图到使用 Gimp 进行绘画。一切看起来都很丑。幸运的是,我有我的哥哥伊利亚,他用 Sketch 在五分钟内创建了这些图形。非常感谢你,伊鲁什卡!
回到我们的例子。在这种情况下,右三角形的角度等于270° -
α
。我们的x
等于100 + b
,y
等于100 + a
。以下是简单的计算:
a = c * sin (270 - α)
a = c * sin (180 + (90 - α))
a = -c * sin (90 - α)
a = -c * cosα
b = c * cos (270 - α)
b = c * cos (180 + (90 - α))
b = -c * cos (90 - α)
b = -c * sinα
因此:
x = 100 + (-100 * sinα) = 100 – 100*sinα
y = 100 + (-100 * cosα) = 100 – 100*cosα
这与小于180度的角度完全相同!
这是用于计算x,y坐标的 JavaScript 代码:
function calcEndPoint (angle) {
let x, y
**x = 100 - 100 * Math.sin(Math.PI * angle / 180)**
**y = 100 - 100 * Math.cos(Math.PI * angle / 180)**
return {
x, y
}
}
现在,我们终于可以定义一个函数,根据角度确定路径元素的d
字符串属性。这个函数将调用calcEndPoint
函数,并返回一个包含最终d
属性的string
:
function calcPath (angle) {
let d
let {x, y} = calcEndPoint(angle)
if (angle <= 180) {
d = `M100,100 L100, 0 A100,100 0 0,0 ${x}, ${y} z`
} else {
d = `M100,100 L100, 0 A100,100 0 0,0 100, 200 A100,100 0 0,0 ${x}, ${y} z`
}
return d
}
为了完成我们的组件,让我们引入一个文本 SVG 元素,它将只渲染传递给组件的文本属性。也应该可以绘制一个没有任何文本的圆;因此,让我们使用v-if
指令来实现这一点:
//SvgCircleSector.vue
<template>
<div>
<svg class="timer" width="200" height="200" >
<...>
<text **v-if="text != ''"** class="text" x="100" y="100">
**{{text}}**
</text>
</svg>
</div>
</template>
让我们还提取大圆和小圆的样式,以及路径和文本的样式到style
部分。让我们定义有意义的类,这样我们的模板将如下所示:
//SvgCircleSector.vue
<template>
<div>
<svg class="timer" width="200" height="200" >
<circle class="**bigCircle**" r="100" cx="100" cy="100"></circle>
<circle class="**smallCircle**" r="90" cx="100" cy="100"></circle>
<path class="**segment**" :d="path"></path>
<text v-if="text != ''" class="**text**" x="100" y="100">
{{text}}
</text>
</svg>
</div>
</template>
在style
标签内,让我们定义颜色变量,并将它们用于我们的圆。将颜色提取到变量中将有助于我们在将来轻松地更改它们,如果我们决定更改应用程序的颜色方案。因此,我们的 SVG 组件的样式将如下所示:
//SvgCircleSector.vue
<style scoped lang="scss">
**$big-circle-color: gray;**
**$small-circle-color: lightgray;**
**$segment-color: darkgray;**
**$text-color: black;**
.bigCircle {
fill: $big-circle-color;
}
.smallCircle {
fill: $small-circle-color;
}
.segment {
fill: $segment-color;opacity: 0.6;
}
.text {
font-size: 1em;
stroke-width: 0;
opacity: .9;
fill: $text-color;
}
</style>
练习
到目前为止,我们一直在使用绝对大小的圆;它的半径始终为100
像素。使用viewBox
和preserveAspectRatio
属性应用于svg
元素,使我们的圆响应式。试着玩一下;在PomodoroTimer
组件中调用这个组件,使用不同的角度属性来看看它是如何工作的。我能想出这样疯狂的页面:
由许多 SVG 圆组成的疯狂页面,其扇形由给定角度定义
检查chapter4/4/profitoro
文件夹中的代码。特别注意components/sections/timer
文件夹中的SvgCircleSector.vue
组件,以及调用圆形组件多次并使用不同的角度属性的PomodoroTimer.vue
组件。
实现倒计时计时器组件
现在我们有一个完全功能的组件,它可以根据给定的角度渲染一个带有高亮区域的圆形,我们将实现CountDownTimer
组件。这个组件将接收一个倒计时的秒数作为属性。它将包含控件元素:一组按钮,允许你开始、暂停和停止计时器。一旦计时器启动,秒数将被倒计时,并相应地重新计算角度。这个重新计算的角度被传递给SvgCircleSector
组件,以及计算出的文本。文本将包含计时器结束时剩余的分钟和秒数。
首先,在components/main/sections/timer
文件夹中创建一个CountDownTimer.vue
文件。让我们从这个组件中调用SvgCircleSector
组件,并为angle
和text
属性设置一些任意值:
**//CountDownTimer.vue**
<template>
<div class="container">
<div>
<**svg-circle-sector** **:angle="30"** **:text="'Hello'"**></**svg-circle-sector**>
</div>
</div>
</template>
<script>
**import SvgCircleSector from './SvgCircleSector'**
export default {
components: {
**SvgCircleSector**
}
}
</script>
<style scoped lang="scss">
</style>
打开页面。有点太大了。甚至不适合我的屏幕:
我们的组件不适合我的屏幕
然而,如果我在手机上打开它,它会渲染得很好,实际上看起来很好:
我们的组件在移动屏幕上实际上非常合适
这是因为我们的圆是响应式的。如果你尝试调整浏览器的大小,你会发现圆形会相应地调整大小。它的宽度始终是浏览器的100%。当页面的高度大于宽度时(这是移动浏览器的情况),它看起来很好,但当宽度大于高度时(如在桌面屏幕的情况下),它看起来非常大和丑陋。所以,我们的圆是响应式的,但并不是真正适应性的。但我们正在使用 Bootstrap!Bootstrap 在响应性和适应性方面是一个很好的朋友。
使用 Bootstrap 实现倒计时计时器的响应性和适应性
为了实现对任何设备的适应性,我们将使用 Bootstrap 网格系统来构建我们的布局,网址为v4-alpha.getbootstrap.com/layout/grid/
。
注意
请注意,此 URL 是用于 alpha 版本的,下一个版本将在官方网站上提供。
此系统基于十二列行布局。row
和col
类包括不同的层级,每个媒体查询一个。因此,相同的元素可以根据设备大小具有不同的相对大小。这些类的名称是不言自明的。包装行类名为row
。然后,每列可能有一个名为col
的类。例如,这是一个具有相等大小的四列的简单行:
<div class="**row**">
<div class="**col**">Column 1</div>
<div class="**col**">Column 2</div>
<div class="**col**">Column 3</div>
<div class="**col**">Column 4</div>
</div>
此代码将产生以下结果:
具有四个相等大小列的 Bootstrap 行
类col
可以与您要为列指定的大小相结合:
<div class="**col-***">Column 1</div>
在这里,*
可以是从1
到12
的任何内容,因为每行最多可以包含十二列。以下是具有四个不同大小列的行的示例:
<div class="row">
<div class="**col-6**">Column 1</div>
<div class="**col-3**">Column 2</div>
<div class="**col-2**">Column 3</div>
<div class="**col-1**">Column 4</div>
</div>
因此,第一列将占据一半的行,第二列将占据四分之一的行,第三列将占据六分之一的行,最后一列将占据十二分之一的行。这是它的样子:
具有不同大小列的 Bootstrap 行
不要在意黑色边框;我添加它们是为了使列宽更加明显。Bootstrap 将在没有任何边框的情况下绘制您的布局,除非您告诉它包括它们。
Bootstrap 还提供了一种偏移列的技术,可以在v4-alpha.getbootstrap.com/layout/grid/#offsetting-columns
上找到。
注意
请注意,此 URL 是用于 alpha 版本的,下一个版本将在官方网站上提供。
例如,我们如何制作两列,其中一列的大小为6
,另一列的大小为2
,偏移量为4
:
<div class="**row**">
<div class="**col-6**">Column 1</div>
<div class="**col-2 offset-4**">Column 2</div>
</div>
这是它的样子:
具有两列的行,其中一列显示偏移量为 4。
您甚至可以通过使用push-*
和pull-*
类来玩转列并更改它们的顺序。有关更多信息,请访问v4-alpha.getbootstrap.com/layout/grid/#push-and-pull
。
注意
请注意,此 URL 是用于 alpha 版本的,下一个版本将在官方网站上提供
这些类几乎扮演了offset-*
类的相同角色;它们为您的列提供了更多的灵活性。例如,如果我们想要呈现大小为3
的列和大小为9
的列并更改它们的顺序,我们将需要将大小为3
的列推送到9
的位置,并将大小为9
的列拉到3
的位置:
<div class="row">
<div class="**col-3 push-9**">Column 1</div>
<div class="**col-9 pull-3**">Column 2</div>
</div>
此代码将产生以下布局:
使用 push-和 pull-类更改列的顺序
尝试所有这些示例,并检查无论如何调整页面大小,布局的比例都将始终相同。这是 Bootstrap 布局的一个强大功能;您甚至不必费心使您的布局响应。我在本节的第一段中提到的不同设备怎么样?到目前为止,我们一直在探索称为col-*
、offset-*
、push-*
和pull-*
的类。Bootstrap 还为每种媒体查询提供了这组类。
Bootstrap 中有五种设备类型:
xs | 超小设备 | 竖屏手机(<544px) |
---|---|---|
sm | 小设备 | 横屏手机(≥544px - <768px) |
md | 中等设备 | 平板电脑(≥768px - <992px) |
lg | 大设备 | 桌面电脑(≥992px - <1200px) |
xl | 超大设备 | 桌面电脑(≥1200px) |
为了指示在给定设备上的期望行为,您只需在类名和其大小之间传递设备指定。因此,例如,如果您希望大小分别为8
和4
的两列在移动设备上转换为两个堆叠的列,您可以执行以下操作:
<div class="row">
<div class="col-sm-12 col-md-8">Column 1</div>
<div class="col-sm-12 col-md-4">Column 2</div>
</div>
如果您在浏览器中打开此代码并尝试调整页面大小,您会发现一旦大小小于544
像素,列将堆叠:
两列布局在小屏幕上变成了堆叠的等大小列布局
那么我们应该怎么处理我们的计时器?我会说它可以在小设备上占据整个宽度(100%),在中等宽度设备上占据宽度的 2/3,在大设备上变为宽度的一半,在超大设备上为宽度的 1/3。因此,它将需要以下类:
-
col-sm-12 用于小设备
-
col-md-8 用于中等宽度设备
-
col-lg-6 用于大设备
-
col-xl-4 用于超大设备
我还希望我的圆圈出现在屏幕中央。为此,我将应用justify-content-center
类到行中:
<div class="row **justify-content-center**">
<svg-circle-sector class="**col-sm-12 col-md-8 col-lg-6 col-xl-4**" :angle="30" :text="'Hello'"></svg-circle-sector>
</div>
打开页面并尝试调整大小,模拟不同的设备,测试纵向和横向视图。我们的圆圈会相应地调整大小。检查chapter4/5/profitoro
文件夹中的代码;特别注意components/CountDownTimer.vue
组件。
倒计时计时器组件- 让我们倒计时!
我们已经实现了倒计时计时器组件的响应性。让我们最终将其变成一个真正的倒计时计时器组件。让我们首先添加控件:开始、暂停和停止按钮。现在,我会让它们看起来像链接。为此,我将使用 Bootstrap 的btn-link
类在v4-alpha.getbootstrap.com/components/buttons/
。
注意
请注意,此 URL 是用于 alpha 版本的,下一个版本将在官方网站上提供。
我还将使用 Vue 的v-on
指令在每个按钮点击时绑定一个方法在vuejs.org/v2/api/#v-on
:
<button **v-on:click="start">Start</button>**
或者,我们可以简单地使用:
<button **@click="start"**>Start</button>
因此,按钮的代码将如下所示:
<div class="controls">
<div class="btn-group" role="group">
<button **@click="start"** type="button" class="**btn btn-link**">Start</button>
<button **@click="pause"** type="button" class="**btn btn-link**">Pause</button>
<button **@click="stop"** type="button" class="**btn btn-link**">Stop</button>
</div>
</div>
将text-center
类添加到包装容器div
中,以便按钮居中对齐。现在,有了控制按钮,我们的计时器看起来像这样:
带控制按钮的倒计时计时器
当我们开始讨论这个组件时,我们说它将从其父组件接收以秒为单位的倒计时时间。让我们添加一个名为time
的属性,并让我们从父组件传递这个属性:
//CountDownTimer.vue
<script>
<...>
export default {
**props: ['time']**
<...>
}
</script>
现在,让我们将这个属性作为计算的硬编码属性导出到PomodorTimer
组件中,并将其绑定到CountDownTimer
组件。让我们将其硬编码为25
分钟,或25 * 60
秒:
//PomodoroTimer.vue
<template>
<div>
<count-down-timer **:time="time"**></count-down-timer>
</div>
</template>
<script>
import CountDownTimer from './timer/CountDownTimer'
export default {
**computed: {**
**time () {**
**return 25 * 60**
**}**
**}**,
components: {
CountDownTimer
}
}
</script>
好的,所以我们的倒计时组件接收以秒为单位的时间。它将如何更新角度
和文本
?由于我们无法更改父级的属性(时间
),我们需要引入属于该组件的值,然后我们将能够在组件内部更改它,并根据该值计算角度和文本值。让我们引入这个新值并称之为时间戳
。将其放在倒计时组件的数据函数中:
//CountDownTimer.vue
data () {
return {
**timestamp: this.time**
}
},
现在让我们为angle
添加一个计算值。我们如何根据时间戳(以秒为单位)计算角度?如果我们知道每秒的角度值,那么我们只需将该值乘以所需秒数即可:
angle = DegreesPerSecond * this.timestamp
知道初始时间(以秒为单位),很容易计算每秒的度数。由于整个周长为360 度,我们只需将360除以初始时间即可:
DegreesPerSecond = 360/this.time
最后但同样重要的是,由于我们的计时器是逆时针计时器,我们需要将逆角度传递给SvgCircleSector
组件,因此我们的最终计算角度值将如下所示:
computed: {
**angle** () {
**return 360 - (360 / this.time * this.timestamp)**
}
}
通过角度的值替换模板中的硬编码角度绑定:
<svg-circle-sector **:angle="angle"**></svg-circle-sector>
调整timestamp
的值;尝试将其从0 * 60
设置为25 * 60
。您将看到高亮区域如何相应地更改:
圆圈的高亮区域随着给定的时间戳而相应地变化
我不确定你,但我已经厌倦了看到这个 Hello。让我们做点什么。计时器的文本应显示剩余时间直到倒计时结束的分钟数和秒数;它对应于计时器圆圈的未高亮区域。这是一个非常简单的计算。如果我们将时间戳除以60
并获得除法的整数部分,我们将得到当前分钟数。如果我们获得这个除法的余数,我们将得到当前秒数。文本应该显示分钟和秒数除以冒号(:
)。因此,让我们添加这三个计算值:
//CountDownTimer.vue
computed: {
angle () {
return 360 - (360 / this.time * this.timestamp)
},
**minutes** () {
return **Math.floor(this.timestamp / 60)**
},
**seconds** () {
return **this.timestamp % 60**
},
**text** () {
return **`${this.minutes}:${this.seconds}`**
}
},
请注意,我们在计算文本时使用了ES6
模板(developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals
)。
最后,用文本值替换属性绑定中的硬编码字符串Hello
:
<svg-circle-sector :angle="angle" **:text="text"**></svg-circle-sector>
现在好多了吧?
计时器的文本根据剩余时间而变化
现在唯一缺少的是实际启动计时器并进行倒计时。我们已经在每个相应的按钮点击上调用了start
、pause
和stop
方法。让我们创建这些方法:
//CountDownTimer.vue
methods: {
**start** () {
},
**pause** () {
},
**stop** () {
}
},
这些方法内部应该发生什么?start
方法应该设置一个间隔,每秒减少一秒的计时器。pause
方法应该暂停这个间隔,stop
方法应该清除这个间隔并重置时间戳。在组件的数据函数中引入一个名为interval
的新变量,并添加所需的方法:
//CountDownTimer.vue
data () {
return {
timestamp: this.time,
**interval**: null
}
},
<...>
methods: {
**start** () {
this.interval = **setInterval**(() => {
**this.timestamp--**
if (this.timestamp === 0) {
this.timestamp = this.time
}
}, 1000)
},
**pause** () {
**clearInterval**(this.interval)
},
**stop** () {
**clearInterval**(this.interval)
this.timestamp = this.time
}
}
然后...我们完成了!打开页面,点击控制按钮,尝试不同的初始时间值,并检查它的工作情况!检查chapter4/6/profitoro
文件夹中CountDownTimer
组件的代码。
练习
我们的倒计时器看起来很不错,但仍然存在一些问题。首先,文本看起来不太好。当分钟或秒数少于9
时,它会显示相应的文本,而不带有尾随的0
,例如,5 分钟 5 秒显示为5:5。这看起来并不像时间。引入一个方法,让我们称之为leftpad
,它将为这种情况添加一个额外的0
。请尽量不要破坏互联网!(www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/
)
我们的计时器的另一个问题是我们可以随时点击任何按钮。如果你频繁点击启动按钮,结果会出乎意料地难看。引入三个数据变量——isStarted
,isPaused
和isStopped
——它们将根据每个方法进行切换。将disabled
类绑定到控制按钮。这个类应该根据提到的变量的值来激活。所以,行为应该是以下的:
-
如果计时器已经启动并且没有暂停,启动按钮应该被禁用。
-
如果计时器没有启动,暂停和停止按钮应该被禁用。如果计时器已经暂停或停止,它们也应该被禁用。
要有条件地绑定类,使用v-bind:className={expression}
,或者简单地使用:className={expression}
表示法。例如:
<button **:class="{disabled: isStarted}"**>Start</button>
要自己检查一下,请查看chapter4/7/profitoro
目录,特别是components/CountDownTimer.vue
组件。
番茄钟计时器
因此,我们已经有了一个完全功能的倒计时计时器。我们离应用程序的最终目的——能够倒计时任何给定的时间——已经非常接近了。我们只需要基于它实现一个番茄钟计时器。我们的番茄钟计时器必须使用工作番茄钟时间初始化倒计时组件,并在番茄钟结束后将其重置为休息时间。休息结束后,它必须再次将其重置为工作番茄钟时间。依此类推。不要忘记,三个常规番茄钟后的休息时间略长于通常的休息时间。
让我们创建一个config
文件,其中包含这些值,这样我们就可以在需要用不同的时间测试应用程序时轻松更改它。因此,我们需要指定workingPomodoro
、shortBreak
和longBreak
的值。我们还需要指定到长休息之前工作的番茄钟数量。默认情况下,这个值将是三,但是如果你是一个工作狂,你可以在23485个常规番茄钟后指定更长的番茄钟休息(不要这样做,我还需要你!)。因此,我们的配置文件是一个常规的.js
文件,其内容如下:
//src/config.js
export default {
**workingPomodoro**: 25,
**shortBreak**: 5,
**longBreak**: 10,
**pomodorosTillLongBreak**: 3
}
在PomodoroTimer
组件中导入这个文件。让我们还为这个组件定义必要的数据。因此,番茄钟计时器有三种主要状态;它要么处于工作状态,要么处于短休息状态,要么处于长休息状态。它还应该计算到长休息之前的番茄钟数量。因此,我们的PomodoroTimer
组件的数据将如下所示:
//PomodoroTimer.vue
data () {
return {
isWorking: true,
isShortBreak: false,
isLongBreak: false,
pomodoros: 0
}
}
现在,我们可以根据番茄钟计时器的当前状态计算time
的值。为此,我们只需要将当前间隔对应的分钟数乘以60
。我们需要定义哪个间隔是正确的分钟数,并根据应用程序的当前状态做出决定。下面是我们漂亮的计算值的if-else
构造:
//PomodoroTimer.vue
computed: {
time () {
let minutes
if (this.**isWorking**) {
minutes = config.**workingPomodoro**
} else if (this.**isShortBreak**) {
minutes = config.**shortBreak**
} else if (this.**isLongBreak**) {
minutes = config.**longBreak**
}
return minutes * 60
}
}
这比较清楚,对吧?现在,我们必须编写代码,以在工作的番茄钟、短休息和长休息之间切换。让我们称这个方法为togglePomodoro
。这个方法应该做什么?首先,isWorking
状态应该根据先前的值设置为true
或false
(this.isWorking = !this.isWorking
)。然后,我们应该重置isShortBreak
和isLongBreak
的值。然后我们必须检查isWorking
的状态是否为false
,这意味着我们目前正在休息。如果是的话,我们必须增加到目前为止完成的番茄数量。然后根据番茄数量,我们需要将其中一个休息状态设置为true
。这是这个方法:
//PomodoroTimer.vue
methods: {
togglePomodoro () {
// toggle the working state
**this.isWorking = !this.isWorking**
// reset break states
**this.isShortBreak = this.isLongBreak = false**
// we have switched to the working state, just return
if (this.isWorking) {
return
}
// we have switched to the break state, increase the number of pomodoros and choose between long and short break
**this.pomodoros ++**
this.isLongBreak = **this.pomodoros % config.pomodorosTillLongBreak === 0**
this.isShortBreak = **!this.isLongBreak**
}
}
现在,我们只需要找到一种调用这个方法的方式。它应该在什么时候被调用?很明显,每当倒计时器达到零时,应该调用这个方法,但我们如何能意识到这一点呢?某种程度上,倒计时器组件必须向其父组件通知它已经停在零上。幸运的是,使用 Vue.js,组件可以使用this.$emit
方法发出事件。因此,我们将从倒计时组件触发此事件,并将其处理程序绑定到从PomodoroTimer
调用的组件上。让我们称这个事件为finished
。打开CountDownTimer.vue
组件,并找到一个地方,我们在那里检查减少的时间戳是否达到了零值。在这一点上,我们必须大喊“嘿,父组件!我完成了我的任务!给我另一个”。这是一个简单的代码:
// CountDownTimer.vue
<...>
if (this.timestamp <= 0) {
**this.$emit('finished')**
this.timestamp = this.time
}
绑定这个事件非常简单。就像任何其他事件一样;只需在PomodoroTimer
模板内使用@
后跟附加到组件的事件名称。
<count-down-timer **@finished="togglePomodoro"** :time="time"></count-down-timer>
现在检查应用程序的页面。尝试在配置文件中玩弄时间值。检查一切是否正常工作。
锻炼
你已经开始使用新的番茄钟来安排你的日常生活了吗?如果是的话,我相信当计时器在工作时,你会非常愉快地浏览其他标签并做其他事情。你有没有注意到时间比应该的时间长?我们的浏览器真的很聪明;为了不影响你的 CPU,它们在非活动的标签中保持相当空闲。这实际上是完全合理的。如果你不看它们,为什么非活动的标签要执行复杂的计算或者基于setIntervals
和setTimeout
函数运行一些疯狂的动画呢?虽然从性能方面来说这是完全合理的,但对我们的应用程序来说并没有太多意义。
不管怎样,它都应该倒数 25 分钟。为了这个练习,改进我们的倒计时器,使其始终倒计时准确的秒数,即使它在隐藏或非活动的浏览器标签中打开。谷歌一下,你会看到整个互联网上关于Stackoverflow的结果:
在非活动标签中使用 setInerval 的奇怪行为充斥着互联网
我还希望你在这个练习中为CountDownTimer
组件的time
属性添加一个监视器,以便重新启动计时器。这将使我们能够更精确地在PomodoroTimer
组件中更改时间时重置计时器。在这方面,请查看 Vue 文档,网址为vuejs.org/v2/guide/computed.html#Watchers
。
对于这两个任务,请查看chapter4/8/profitoro
应用程序文件夹,自行检查。唯一应用更改的组件是CountDownTimer.vue
组件。注意setInterval
函数以及如何更新timestamp
。
引入锻炼
我写这一章时非常热情,计算正弦、余弦,绘制 SVG,实现计时器,并照顾非活动标签等等,以至于我几乎忘记了做锻炼!我喜欢做平板支撑和俯卧撑,你呢?顺便说一句,你难道也忘了锻炼是我们应用程序的一部分吗?在休息时间,我们应该做简单的锻炼,而不仅仅是查看社交网络!
我们将在接下来的章节中实现完整的锻炼和管理;现在,让我们为锻炼留下一个漂亮的占位符,并在这个占位符中硬编码一个锻炼(我投票支持俯卧撑,因为这本书是我的,但你可以添加你自己喜欢的锻炼或者锻炼)。打开PomodoroTimer.vue
组件,并将倒计时组件包装在一个带有row
类的div
中。我们将使这一行包含两列,其中一列将是倒计时器,另一列是一个有条件渲染的包含锻炼的元素。为什么有条件呢?因为我们只需要在番茄钟休息时显示这个元素。我们将使用v-show
指令,以便包含元素始终存在,只有display
属性会改变。因此,标记看起来像下面这样:
//PomodoroTimer.vue
<div class="container">
<div class="**row**">
<div **v-show="!isWorking"** class="**col-sm-4**">
WORKOUT TIME!
</div>
<count-down-timer class="**col-sm-8**" @finished="togglePomodoro" :time="time"></count-down-timer>
</div>
</div>
请注意col-sm-4
和col-sm-8
。再次强调,我希望在更大的设备上列看起来不同,在小设备上堆叠!
我们应该使用什么元素来显示我们的锻炼?出于某种原因,我非常喜欢 Bootstrap 的jumbotrons
(v4-alpha.getbootstrap.com/components/jumbotron/
),所以我将使用一个包含锻炼标题的标题元素,锻炼描述的引导元素,以及一个图像元素来显示锻炼图像的jumbotron
。
注意
请注意,Bootstrap 的 Jumbotron 组件的 URL 是 alpha 版本的,下一个版本将在官方网站上提供
因此,我用于显示锻炼的标记结构如下:
//PomodoroTimer.vue
<div class="jumbotron">
<div class="container">
<img class="img-fluid rounded" src="IMAGE_SOURCE" alt="">
<h2>Push-ups</h2>
<lead>
Description: lorem ipsum
</lead>
</div>
</div>
在这一部分,随意添加另一个适合你的好锻炼,这样你就能在读完这本书之前锻炼了。检查section4/9/profitoro
文件夹中的此部分的代码。
这是我的笔记本电脑屏幕上的番茄钟的样子:
笔记本电脑屏幕上的番茄钟
这是它在手机屏幕上的样子:
手机屏幕上的番茄钟
当然,它并不那么美观,但它是响应式和自适应的,我们没有为它做任何 CSS 黑魔法!
总结
在本章中,我们做了很多事情。我们实现了我们的番茄钟计时器的主要功能,现在它是完全功能的、可配置的、可用的和响应的。我们启动了我们的 ProFitOro 应用程序,将其分成组件,为每个定义的组件创建了一个骨架,并完全实现了其中的一个。我们甚至重新学习了一些三角学,因为数学无处不在。我们实现了我们的计时器,并让它在隐藏和非活动标签上也能工作。我们使用强大的 Bootstrap 布局类使应用程序对不同设备尺寸具有响应性和适应性。我们的应用程序是功能性的,但离美观还有很大差距。不过,暂时不要在意这些灰色调;让我们暂时坚持它们。在本书的最后,你将得到你漂亮的 ProFitOro 样式,我向你保证!
我们准备继续在技术世界中的旅程。在下一章中,我们将学习如何配置我们的番茄钟,以及如何使用 Firebase 存储配置和使用统计数据。因此,在下一章中我们将:
-
回到 Vuex 集中式状态管理架构,并将其与 Google Firebase 存储系统结合起来,以存储应用程序的关键数据,如配置和统计信息。
-
实现 ProFitOro 的配置
-
实现 ProFitOro 使用统计数据的存储、检索和显示
第五章:配置您的番茄钟
在上一章中,我们实现了 ProFitOro 应用程序的主要功能-番茄钟计时器。我们甚至添加了一个硬编码的锻炼,这样我们就可以在休息时间锻炼。实际上,我已经开始使用 ProFitOro。当我写下这些文字时,番茄钟正在倒计时-滴答滴答滴答滴答。
在这一章中,我们将探索Firebase 实时数据库的可能性及其 API。我们将管理存储、检索和更新应用程序的使用统计和配置。我们将使用 Vuex 存储将应用程序的数据从数据库传递到前端应用程序。
为了将这种可能性带到 UI 中,我们将使用 Vue 的响应性结合 Bootstrap 的强大之处。因此,在这一章中,我们将使用以下内容来实现 ProFitOro 的统计和设置组件:
-
Firebase 实时数据库
-
Vue.js 的响应式数据绑定和 Vuex 状态管理
-
Bootstrap 的强大之处在于使事物具有响应性
设置 Vuex 存储
在开始使用数据库中的真实数据之前,让我们为我们的 ProFitOro 设置 Vuex 存储。我们将使用它来管理番茄钟计时器的配置,用户设置(如用户名)以及个人资料图片的 URL。我们还将使用它来存储和检索应用程序的使用统计。
从第二章 Hello User Explained,您已经知道了 Vuex 存储的工作原理。我们必须定义代表应用程序状态的数据,然后我们必须提供所有需要的 getter 来获取数据和所有需要的 mutation 来更新数据。一旦所有这些都设置好了,我们就能够从组件中访问这些数据。
应用程序的存储准备就绪并设置好后,我们可以将其连接到实时数据库,并稍微调整 getter 和 mutation 以操作真实数据。
首先,我们需要告诉我们的应用程序将使用 Vuex 存储。为此,让我们为vuex
添加npm
依赖项:
**npm install vuex --save**
现在,我们需要定义我们存储的基本结构。我们的 Vuex 存储将包含以下内容:
-
State:应用程序数据的初始状态。
-
Getters:检索状态属性的方法。
-
Mutations:提供改变状态的方法。
-
操作:可以调度以调用突变的方法。操作和突变之间唯一的区别是操作可以是异步的,我们可能需要它们用于我们的应用程序。
听起来很简单,对吧?只需创建一个名为store
的文件夹,并为我们刚刚指定的所有内容创建 JavaScript 文件。还要创建index.js
文件,该文件将使用所有这些内容实例化一个带有所有这些内容的 Vuex 存储。以下是您的结构:
存储文件夹的结构
当我们在第二章中首次提到 Vuex 存储时,Hello User Explained,我们简化了结构,并在同一个文件中介绍了所有存储的组件。现在,我们将遵循良好的模块化结构,并让所有内容都放在自己的位置上。我们甚至可以进一步将状态分离到模块中(一个用于配置,另一个用于设置,依此类推),但对于 ProFitOro 的复杂级别来说,这可能会有些过度。但是,如果您想了解如何将存储分离为逻辑模块,请查看关于 Vuex 的这篇出色文档中有关模块的部分:vuex.vuejs.org/en/
。
尽管如此,让我们继续使用我们的存储。在创建了结构之后,将所有存储组件导入index.js
并创建一个 Vuex 实例,将所有这些组件作为参数传递。不要忘记导入 Vuex 并告诉 Vue 使用它!因此,我们的存储入口点将如下所示:
//store/index.js
**import Vue from 'vue'**
**import Vuex from 'vuex'**
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
**Vue.use(Vuex)**
export default new Vuex.Store({
**state,**
**getters,**
**mutations,**
**actions**
})
现在唯一重要的事情,以便我们的设置完全完成,就是让我们的应用程序知道它现在正在使用这个存储。这样,存储将在所有组件中可用。要使其成为可能的唯一事情就是在应用程序的入口点(main.js
)中导入我们的存储,并将其传递给 Vue 实例:
//main.js
import Vue from 'vue'
import App from './App'
**import store from './store'**
new Vue({
el: '#app',
template: '<App/>',
components: { App },
**store**
})
现在,我们已经完全准备好开始使用存储进行魔术了。您是否一直在思念编码?好了,现在可以了!让我们首先用存储的状态和获取器替换我们已经创建的config
文件,该文件用作番茄钟定时属性的容器。只需将config
文件的所有配置元素复制到我们的状态中,并为其创建一个获取器:
//store/state.js
const config = {
workingPomodoro: 25,
shortBreak: 5,
longBreak: 10,
pomodorosTillLongBreak: 3
}
export default {
**config**
}
让我们现在转向 getter。 Getter 不仅仅是普通的函数。在幕后,它们接收状态作为参数,因此您可以访问应用程序状态的数据,而无需进行任何依赖注入的努力,因为 Vuex 已经为您管理了。因此,只需创建一个接收状态作为参数并返回任何状态数据的函数!如果需要,在 getter 内部,您可以对数据执行任何操作。因此,config
文件的 getter 可能看起来像这样:
//store/getters.js
function getConfig (state) {
return state.config
}
由于我们使用的是 ES6,可以以更简洁和优雅的方式重写:
//store/getters.js
var getConfig = (state) => state.config
然后,它可以被导出:
//store/getters.js
export default {
getConfig: getConfig
}
或者,我们可以简单地使用:
//store/getter.js
export default {
getConfig
}
整个事情实际上可以写成:
//store/getters.js
export default {
**getConfig: state => state.config**
}
多么惊人简单啊?当我开始使用 JavaScript 时(不要问我什么时候,我不想让自己感觉老),我几乎无法想象这样的语法会有可能。
现在,您可以在任何应用程序组件中使用您的新 getter。如何?您还记得使用this.$store.state
属性轻松访问状态有多容易吗?同样,在计算数据函数内部,您可以访问您的“getter”:
**computed**: {
config () {
return **this.$store.getters.getConfig**
}
},
从现在开始,this.config
可以在组件的所有计算值和方法中使用。现在想象一下,在同一个组件内,我们需要使用多个 getter。例如,假设我们为每个 config 的值创建 getter。因此,对于每个值,您都必须重复这种繁琐的代码:this.$store.getters.bla-bla-bla
。啊!一定有更简单的方法...而且确实有。Vuex 很友好地为我们提供了一个名为mapGetters
的辅助对象。如果您简单地将此对象导入到组件中,就可以使用 ES6 扩展运算符使用mapGetters
调用您的 getter:
import { **mapGetters** } from 'vuex'
export default {
computed: {
**...mapGetters**([
'getConfig'
])
}
}
或者,如果您想将 getter 方法映射到其他名称,只需使用一个对象:
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters({
**config: 'getConfig'**
})
}
}
所以,这就是我们要做的。我们将在PomodoroTimer
组件内部使用mapGetters
助手,并删除对导入的config
文件的引用(还要记得删除文件本身;我们不希望代码库中有死代码)。我们将用this.config
替换所有对config
的引用。因此,我们的PomodoroTimer
脚本部分将如下所示:
//PomodoroTimer.vue
<script>
// ...
**import { mapGetters } from 'vuex'**
// ...
export default {
data () {
// ...
},
computed: {
**...mapGetters({**
**config: 'getConfig'**
**})**,
time () {
let minutes
if (this.isWorking) {
minutes = **this.config**.workingPomodoro
} else if (this.isShortBreak) {
minutes = **this.config**.shortBreak
} else if (this.isLongBreak) {
minutes = **this.config**.longBreak
}
return minutes * 60
}
},
// ...
methods: {
togglePomodoro () {
// ...
this.isLongBreak = this.pomodoros % **this.config**.pomodorosTillLongBreak === 0
}
}
}
</script>
检查你的页面,一切都应该和以前一样。这种新方法的优势是什么?——有人可能会问,我们已经在这里花了半章的时间设置这个商店和它的方法、获取器、操作,等等…最后,我们得到了完全相同的行为。为什么?嗯,你还记得这一章的整个目的是能够配置和重新配置番茄工作法的定时设置,并将它们存储在数据库中吗?如果我们不得不在我们的组件中引入数据库引用和检索和存储数据的所有操作,我们的生活会更加艰难。想象一下,如果某个时候 Firebase 不符合你的需求,你希望切换到另一个数据源,甚至是另一种技术,比如 Elasticsearch 或者 MongoDB。你将不得不改变你的组件和它的方法,以及它的计算值。维护所有这些不是听起来像地狱吗?
让你的数据驻留在存储中,并且让你的获取器负责检索它们,将使你只需要改变你的获取器,如果你决定改变底层数据源。你的组件将永远不会被触及!这是你的应用程序的数据和逻辑层的抽象。在软件工程领域,抽象是一件非常酷的事情。
让我们为 Settings.vue
组件定义一个基本的标记。检查我们的模拟。
这个组件将包含两个主要区域:
-
个人设置配置区域
-
番茄工作法定时设置配置区域
同样,我将使用 Bootstrap 栅格类来帮助我构建一个漂亮的、响应式的布局。我希望在小设备上制作两个堆叠列,在中等大小的设备上制作两个相同大小的列,在大设备上制作两个不同大小的列。因此,我将使用 row
类来包装 div
和相应的 col-*-*
类来包装我们 Settings
组件的两个主要区域。
// Settings.vue
<div class="**row justify-content-center**">
<div class="**col-sm-12 col-md-6 col-lg-4**">
<div class="container">
<h2>Account settings</h2>
account settings
</div>
</div>
<div class="**col-sm-12 col-md-6 col-lg-8**">
<div class="container">
<h2>Set your pomodoro timer</h2>
pomodoro timer configuration
</div>
</div>
</div>
现在让我们只集中在番茄工作法定时设置配置上。我创建了一个名为 SetTimer.vue
的组件。这个组件只包含一个数字类型的输入,并在其值发生变化时发出一个方法。在番茄工作法设置容器中,我将使用从导入的 mapGetters
助手中获取的不同值,将这个组件渲染三次:
//Settings.vue
<template>
<...>
<div class="row justify-content-center align-items-center">
<div class="col-md-5 col-sm-10">
**<set-timer :value="config.workingPomodoro"></set-timer>**
<div class="figure-caption">Pomodoro</div>
</div>
<div class="col-md-4 col-sm-10">
**<set-timer :value="config.longBreak"></set-timer>**
<div class="figure-caption">Long break</div>
</div>
<div class="col-md-3 col-sm-10">
**<set-timer :value="config.shortBreak"></set-timer>**
<div class="figure-caption">Short break</div>
</div>
</div>
<...>
</template>
通过一些 CSS 魔法,我能够在 SetTimer
组件中渲染三个输入圆圈,如下所示:
输入允许我们为不同的番茄钟间隔设置定时器的球
您可以在chapter5/1/profitoro
文件夹中找到相应的代码。特别是检查components/main/sections/timer
文件夹中的SetTimer.vue
组件以及在Settings.vue
组件中如何使用相应的值调用它。
定义操作和突变
很棒,我们的组件现在可以从存储中获取数据,但如果我们的组件也能够更改存储中的数据,那可能会更有趣。另一方面,我们都知道我们不能直接修改存储的状态。
状态不应该被任何组件触摸。然而,您还记得我们关于 Vuex 存储的章节中有一些特殊函数可以改变存储。它们甚至被称为mutations
。这些函数可以对 Vuex 存储数据做任何它们/你想做的事情。这些突变可以使用应用于存储的commit
方法来调用。在底层,它们实质上接收两个参数 - 状态和值。
我将定义三个突变 - 分别用于定时器的每个定义。这些突变将使用新值更新config
对象的相应属性。因此,我的突变如下:
//store/mutations.js
export default {
**setWorkingPomodoro** (state, workingPomodoro) {
state.config.workingPomodoro = workingPomodoro
},
**setShortBreak** (state, shortBreak) {
state.config.shortBreak = shortBreak
},
**setLongBreak** (state, longBreak) {
state.config.longBreak = longBreak
}
}
现在我们可以定义操作。操作基本上会调用我们的突变,因此可以被视为重复的工作。然而,请记住操作和突变之间的区别在于操作实际上可以是异步的,因此当我们将操作连接到数据库时可能会派上用场。现在,让我们告诉操作在提交之前验证接收到的值。actions
方法接收存储和一个新值。由于存储为我们提供了一个名为commit
的基本方法,该方法将调用所需的突变的名称,因此我们可以定义每个操作如下:
actionName (**{commit}**, newValue) {
**commit**('mutationName', newValue)
}
提示
我们可以将{commit}
写为参数,并立即使用commit
函数,因为我们使用的是 ES6 和对象解构对我们来说非常完美(developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
)。
因此,我的操作看起来是这样的:
//store/actions.js
export default {
**setWorkingPomodoro** ({commit}, workingPomodoro) {
if (workingPomodoro) {
commit('setWorkingPomodoro', parseInt(workingPomodoro, 10))
}
},
**setShortBreak** ({commit}, shortBreak) {
if (shortBreak) {
commit('setShortBreak', parseInt(shortBreak, 10))
}
},
**setLongBreak** ({commit}, longBreak) {
if (longBreak) {
commit('setLongBreak', parseInt(longBreak, 10))
}
}
}
现在,让我们回到Settings.vue
组件。这个组件应该导入操作并在需要时调用它们,对吧?我们如何导入操作?你还记得mapGetters
助手吗?有一个类似的助手用于操作,叫做mapActions
。所以,我们可以和mapGetters
助手一起导入它,并在methods
对象内使用扩展操作符(…
):
//Settings.vue
<script>
import {mapGetters, **mapActions**} from 'vuex'
<...>
export default {
<...>
methods: {
**...mapActions(['setWorkingPomodoro', 'setShortBreak', 'setLongBreak'])**
}
}
</script>
现在,我们必须在set-timer
输入框的值发生变化时调用所需的操作。在前一段中,我们讨论了SetTimer
组件发出changeValue
事件。所以,我们现在唯一需要做的就是将这个事件绑定到所有三个set-timer
组件上,并调用相应的方法:
<div class="col-md-5 col-sm-10">
**<set-timer :value="config.workingPomodoro" @valueChanged="setWorkingPomodoro"></set-timer>**
<div class="figure-caption">Pomodoro</div>
</div>
<div class="col-md-4 col-sm-10">
**<set-timer :value="config.longBreak" @valueChanged="setLongBreak"></set-timer>**
<div class="figure-caption">Long break</div>
</div>
<div class="col-md-3 col-sm-10">
**<set-timer :value="config.shortBreak" @valueChanged="setShortBreak"></set-timer>**
<div class="figure-caption">Short break</div>
</div>
打开页面,尝试更改每个计时器设置的值。
如果你正在使用 Chrome 浏览器,但还没有安装 Vue 开发者工具,请安装它。你会看到它是多么方便和可爱!只需按照这个链接:goo.gl/22khXD
。
安装了 Vue devtools 扩展后,你会立即看到这些值在 Vuex 存储中是如何变化的:
一旦输入框中的值发生变化,它们就会立即在 Vuex 存储中发生变化
检查chapter5/2/profitoro
文件夹中的本节最终代码。注意存储文件夹内的actions.js
和mutations.js
文件以及Settings.vue
组件。
建立一个 Firebase 项目
我希望你还记得如何从本书的前几章中设置 Firebase 项目。在console.firebase.google.com
打开你的 Firebase 控制台,点击添加项目按钮,命名它,并选择你的国家。Firebase 项目已准备好。是不是很容易?现在让我们准备我们的数据库。以下数据将存储在其中:
-
配置: 我们的 Pomodoro 计时器值的配置
-
统计: Pomodoro 使用的统计数据
每个这些对象将通过一个特殊的键来访问,该键将对应于用户的 ID;这是因为在下一章中,我们将实现一个身份验证机制。
配置对象将包含值-workingPomodoro
,longBreak
和shortBreak
-这些值对我们来说已经很熟悉了。
让我们向我们的数据库添加一个带有一些虚假数据的配置对象:
{
"configuration": {
"test": {
"workingPomodoro": 25,
"shortBreak": 5,
"longBreak": 10
}
}
}
你甚至可以将其创建为一个简单的 JSON 文件并导入到你的数据库中:
将 JSON 文件导入到您的实时 Firebase 数据库
恭喜,您的实时数据库已准备就绪!请记住,默认情况下,安全规则不允许您从外部访问数据,除非您经过身份验证。现在,让我们暂时删除这些规则。一旦我们实现了身份验证机制,我们将稍后添加它们。单击RULES选项卡,并用以下对象替换现有规则:
{
"rules": {
".read": true,
".write": true
}
}
现在我们可以从我们的 Vue 应用程序访问我们的实时数据库。
将 Vuex 存储连接到 Firebase 数据库
现在,我们必须将我们的 Vuex 存储连接到 Firebase 数据库。我们可以使用本机 Firebase API 将状态数据绑定到数据库数据,但是如果有人已经为我们做了这些事情,为什么我们要处理承诺和其他东西呢?这个人叫 Eduardo,他创建了 Vuexfire - Vuex 的 Firebase 绑定(github.com/posva/vuexfire
)。如果您在Wroclaw的vueconf2017 大会上,您可能还记得这个家伙:
Eduardo 在 Vue 大会期间谈到 Vue 和 Firebase
Vuexfire 带有 Firebase 的 mutations 和 actions,这将为您执行所有幕后工作,而您只需在 mutations 和 actions 对象中导出它们。因此,首先安装firebase
和vuexfire
:
**npm install vue firebase vuexfire –save**
在您的存储的index.js
入口点中导入firebase
和firebaseMutations
:
//store/index.js
import firebase from 'firebase'
import { firebaseMutations } from 'vuexfire'
现在,我们需要获取对 Firebase 应用程序的引用。Firebase 带有一个初始化方法initializeApp
,它接收由许多应用程序设置数据组成的对象 - 应用程序 ID,身份验证域等。现在,我们至少必须提供数据库 URL。要获取数据库 URL,只需转到您的 Firebase 项目设置,然后单击将 Firebase 添加到您的 Web 应用按钮:
单击“将 Firebase 添加到您的 Web 应用”按钮
复制数据库 URL,甚至整个配置对象,并将其粘贴到您的存储的index.js
文件中:
//store/index.js
let app = firebase.initializeApp({
databaseURL: **'https://profitoro-ad0f0.firebaseio.com'**
})
您现在可以获取配置对象的引用。一旦我们实现了身份验证机制,我们将使用经过身份验证的用户 ID 从数据库中获取当前用户的配置。现在,让我们使用我们硬编码的 ID test
:
let configRef = app.database().ref('**/configuration/test**')
我将使用扩展运算符在状态对象中导出configRef
引用。因此,这个引用可以被动作访问:
//store/index.js
export default new Vuex.Store({
**state: {**
**...state,**
**configRef**
**}**
})
为了使整个 Vuexfire 魔术生效,我们还必须在mutations
对象中导出firebaseMutations
:
//store/index.js
export default new Vuex.Store({
mutations: {
...mutations,
**...firebaseMutations**
},
actions
})
因此,我们整个store/index.js
现在看起来像下面这样:
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
import firebase from 'firebase'
import { firebaseMutations } from 'vuexfire'
Vue.use(Vuex)
// Initialize Firebase
let config = {
databaseURL: 'https://profitoro-ad0f0.firebaseio.com'
}
let app = firebase.initializeApp(config)
let configRef = app.database().ref('/configuration/test')
export default new Vuex.Store({
state: {
...state,
configRef
},
getters,
mutations: {
...mutations,
...firebaseMutations
},
actions
})
现在让我们去我们的动作。非常重要的是,在做任何其他事情之前,我们要将我们的数据库引用绑定到相应的状态属性上。在我们的情况下,我们必须将状态的config
对象绑定到它对应的引用configRef
上。为此,我们的朋友 Eduardo 为我们提供了一个叫做firebaseAction
的动作增强器,它实现了bindFirebaseRef
方法。只需调用这个方法,你就不必担心承诺和它们的回调。
打开action.js
并导入firebaseAction
增强器:
//store/actions.js
import { **firebaseAction** } from 'vuexfire'
现在让我们创建一个名为bindConfig
的动作,我们将使用bindFirebaseRef
方法实际绑定两个东西在一起:
//store/actions.js
**bindConfig**: firebaseAction(({bindFirebaseRef, state}) => {
**bindFirebaseRef('config', state.configRef)**
})
这个动作应该在什么时候派发呢?可能是在Settings.vue
组件创建时,因为这个组件负责渲染config
状态。因此,在Settings.vue
内部,我们绑定了created
组件的状态,并在其中调用了bindConfig
动作:
//Settings.vue
export default {
//...
methods: {
...mapActions(['setWorkingPomodoro', 'setShortBreak', 'setLongBreak', **'bindConfig'**])
},
**created () {**
**this.bindConfig()**
**}**
}
如果你现在打开页面,你会发现一切都保持不变。唯一的区别是,现在我们使用的数据来自我们的实时数据库,而不是硬编码的config
对象。您可以通过完全删除状态存储对象内config
对象的内容并确保一切仍然正常工作来进行检查。
如果你尝试更改输入值,然后刷新页面,你会发现应用的更改没有保存。这是因为我们没有更新数据库引用。所以让我们更新它!好处是我们不需要在组件内部改变任何东西;我们只需要稍微改变我们的动作。我们将在引用上调用update
方法。请查看 Firebase 实时数据库文档以了解读取和写入数据:firebase.google.com/docs/database/web/read-and-write
。
因此,我们将state
对象传递给每个动作,并在state.configRef
上调用update
方法,将相应的更改属性传递给它。因此,它可能看起来就像以下代码片段一样简单:
//store/actions.js
setWorkingPomodoro ({commit, **state**}, workingPomodoro) {
**state.configRef.update({workingPomodoro})**
},
不要忘记执行所需的检查,将更新的属性解析为整数,并检查configRef
是否可用。如果不可用,只需使用相应的 mutation 名称调用commit
方法。检查chapter5/3/profitoro
文件夹中此部分的最终代码。特别注意store/index.js
和store/actions.js
文件以及Settings.vue
组件。
如果您打开页面并更改番茄钟计时器的值,并继续查看 Firebase 控制台数据库选项卡,您将立即看到差异!
应用于番茄钟计时器配置框的更改立即传播到实时数据库
如果直接在数据库中更改值,您还将看到更改立即传播到您的视图。
练习
您已经学会了如何将实时 Firebase 数据库连接到您的 Vue 应用程序,并利用这些知识来更新番茄钟计时器的配置。现在,将您的知识应用到统计领域。为了简单起见,只显示自用户开始使用该应用以来执行的番茄钟总数。为此,您需要执行以下操作:
-
在您的 Firebase 数据库中添加另一个名为
statistics
的对象,其中包含初始等于0
的totalPomodoros
属性。 -
在存储的
state
中创建一个条目来保存统计数据。 -
使用
firebaseAction
增强器和bindFirebaseRef
方法将统计状态对象的totalPomodoros
映射到 Firebase 引用。 -
创建一个动作,将更新
totalPomodoros
的引用。 -
每当必须在
PomodoroTimer
组件内调用此动作时调用此动作。 -
在
Statistics.vue
组件内显示此值。
尝试自己做。这不应该很困难。遵循我们在Settings.vue
组件中应用的相同逻辑。如果有疑问,请查看chapter5/4/profitoro
文件夹,特别是存储的文件 - index.js
,state.js
和actions.js
。然后查看相应的动作如何在PomodoroTimer
组件内使用,以及它如何在Statistics
组件中呈现。祝你好运!
总结
在本章中,您学会了如何在 Vue 应用程序中使用实时 Firebase 数据库。您学会了如何使用 Vuexfire 及其方法,将我们的 Vuex 存储状态正确地绑定到数据库引用。我们不仅能够从数据库中读取和渲染数据,还能够更新数据。因此,在本章中,我们看到了 Vuex、Firebase 和 Vuexfire 的实际应用。我想我们应该为自己感到自豪。
然而,让我们不要忘记,我们已经在获取用户数据时使用了一个硬编码的用户 ID。此外,我们还不得不通过更改安全规则来向世界公开我们的数据库,这似乎也不太对。看来是时候启用认证机制了!
在下一章中我们将完成这个任务!在下一章中,我们将学习如何使用 Firebase 认证框架来设置认证机制。我们将学习如何在我们的应用程序中使用它,使用 Vuefire(Vue 的 Firebase 绑定:github.com/vuejs/vuefire
)。我们还将实现我们应用程序的初始视图,负责提供注册和登录的方式。我们将使用 Bootstrap 表单元素,以使屏幕对所有屏幕尺寸响应和适应。所以,让我们继续下一章吧!不要忘记先做一些俯卧撑!
第六章:请进行身份验证!
在上一章中,我们将 ProFitOro 应用程序连接到了实时数据库。每当用户更新番茄钟计时器设置时,这些设置都会存储在数据库中,并立即在使用它们的组件之间传播。由于我们没有身份验证机制,我们不得不使用一个虚假用户来测试我们的更改。在本章中,我们将拥有真正的用户!
在这方面,我们将使用 Firebase 身份验证 API。因此,在本章中,我们将做以下事情:
-
讨论 AAA 的含义以及身份验证和授权之间的区别
-
探索 Firebase 身份验证 API
-
创建一个登录页面,并将其与 Firebase 身份验证 API 连接
-
将用户的设置与用户的身份验证连接起来
解释 AAA
AAA代表身份验证、授权和计费。最初,这个术语是用来描述安全网络协议的,然而,它可以很容易地应用于任何系统、网络资源或站点。
那么,AAA 是什么意思,为什么我们要关心呢?
身份验证是唯一识别系统用户的过程。经过身份验证的用户是被授予对系统访问权限的用户。通常,身份验证是通过一些用户名和密码来完成的。当您必须提供用户名和密码来打开您的 Facebook 页面时,您正在进行身份验证。
您的护照是在机场验证自己身份的一种方式。护照控制人员会看着你的脸,然后检查你的护照。因此,任何允许您“通过”的东西都是您身份验证的一部分。它可以是一个只有您和系统知道的特殊词(密码),也可以是您随身携带的可以帮助系统唯一识别您的东西(护照)。
授权是一种控制每个用户有权(权限)访问哪些资源的方式。如果您正在开发 Facebook 应用程序,您可以访问开发者页面,而普通用户无法访问此页面。
计费衡量为每个用户分配的资源。如果您拥有 Dropbox 商业标准帐户,您可以使用高达 2TB 的存储空间,而拥有普通免费 Dropbox 帐户只能获得 2GB 的空间。
对于我们的应用程序,我们应该关注 Triple-A 的前两个部分——身份验证和授权。在计算机科学中,我们经常使用术语auth,指的是身份验证或授权,甚至同时指两者。因此,我们将实现 auth,其中 auth 同时指身份验证和授权。在我们的 ProFitOro 应用程序的上下文中,这两个术语有什么区别呢?嗯,身份验证将允许用户登录到系统中,所以这很容易。授权呢?
您还记得我们决定只有经过身份验证的用户才能访问番茄工作法设置和统计数据吗?这就是授权。以后,我们可能会进一步实现一个特殊的角色——健身教练。拥有这个角色的用户将能够访问锻炼区域并能够添加新的锻炼。
在本章中,我们将使用 Firebase 身份验证机制,以添加登录和登陆到我们的应用程序的可能性,并控制用户可以访问的内容。
Firebase 如何进行身份验证?
在上一章中,您学习了如何使用 Firebase API 创建 Firebase 应用程序实例,并通过应用程序使用它。我们能够访问数据库,读取它,并在其中存储数据。
您使用 Firebase 身份验证 API 的方式非常相似。您创建一个 Firebase 实例,向其提供一个config
对象,并使用firebase.auth()
方法来访问与身份验证相关的不同方法。检查您的 Firebase 控制台的身份验证选项卡:
现在还没有用户,但我们将在一分钟内解决这个问题!
Firebase SDK 提供了几种用户身份验证的方式:
-
基于电子邮件和密码的身份验证:对用户进行身份验证的经典方式。Firebase 提供了一种使用电子邮件/密码登录用户并将其登录的方法。它还提供了重置用户密码的方法。
-
联合实体提供者身份验证:与外部实体提供者(如 Google、Facebook、Twitter 或 GitHub)对用户进行身份验证的方式。
-
电话号码身份验证:通过向用户发送包含验证码的短信来对用户进行身份验证,用户需要输入验证码以确认其身份。
-
自定义身份验证系统集成:将已经存在的身份验证解决方案与 Firebase 身份验证 API 集成的方式。
-
匿名用户身份验证:提供 Firebase 功能(例如访问 Firebase 数据库)而无需进行身份验证的方式。例如,我们可以使用此匿名帐户来提供对数据库中存储的默认配置的访问权限。
对于我们的应用程序,我们将使用第一个和最后一个方法,因此我们将允许用户使用其电子邮件和密码组合进行登录和登录,并且我们将允许匿名用户使用应用程序的基本功能。
您应该在 Firebase 控制台中明确激活这两种方法。只需打开 Firebase 项目的身份验证选项卡,单击登录方法链接,然后启用这两种方法:
明确启用电子邮件/密码和匿名登录方法
使用 Firebase 身份验证 API 的工作流程如下:
-
创建所有必要的方法进行登录和登录。
-
为您的身份验证实现所有必要的 UI。
-
将 UI 的更改连接到身份验证方法。
在第 3 步中发现了什么有趣的东西吗?将 UI 的更改连接到身份验证方法。您还记得我们正在处理一种响应式数据绑定框架,对吧?所以这将会很有趣!
如何将 Firebase 身份验证 API 连接到 Web 应用程序
为了将您的应用程序连接到 Firebase 身份验证 API,您应该首先创建一个 Firebase 应用程序实例:
let config = {
apiKey: 'YourAPIKey',
databaseURL: 'YourDBURL',
authDomain: 'YourAuthDomain'
}
let app = firebase.initializeApp(config)
您可以在弹出窗口中找到必要的密钥和 URL,如果单击Web 设置按钮将打开该窗口:
在 Web 应用程序中使用 Firebase 的设置配置
现在您可以使用应用程序实例来访问auth()
对象及其方法。查看有关身份验证 API 的官方 Firebase 文档:firebase.google.com/docs/auth/users
。
对我们来说 API 最重要的部分是创建和登录用户的方法,以及监听身份验证状态变化的方法:
app.auth().**createUserWithEmailAndPassword**(email, password)
或者:
app.auth().**signInWithEmailAndPassword**(email, password)
监听应用程序身份验证状态变化的方法称为onAuthStateChanged
。您可以在此方法中设置重要属性,考虑应用程序根据用户是否已登录需要具有的状态:
app.auth().**onAuthStateChanged**((user) => {
if (user) {
// user is logged in
} else {
// user is logged out
}
})
就是这样!在我们的应用程序中,我们只需要提供一种可视方式将用户名和密码传递给 API。
认证到 ProFitOro 应用程序
现在让我们让 ProFitOro 应用程序的登录和注销成为可能!首先,我们必须设置 Firebase 实例,并找出应该将所有与身份验证相关的方法放在哪里。Firebase 应用程序初始化已经在 store/index.js 文件中完成。如果您仍然没有在 config 中包含它们,请添加 apiKey 和 authDomain 配置条目:
// store/index.js
let config = {
apiKey: 'YourAPIKey',
databaseURL: 'https://profitoro-ad0f0.firebaseio.com',
authDomain: 'profitoro-ad0f0.firebaseapp.com'
}
let firebaseApp = firebase.initializeApp(config)
我还将使用扩展运算符在 store 的 state 属性中导出 firebaseApp:
//store/index.js
export default new Vuex.Store({
state: {
...state,
firebaseApp
},
<...>
})
我还将向我们的状态添加一个用户属性,以便我们可以在 onAuthStateChanged 监听器的处理程序上重置它:
// store/state.js
export default {
config,
statistics,
**user,**
**isAnonymous: false**
}
让我们还创建一个小的变异,将用户对象的值重置为给定值:
// store/mutations.js
export default {
<...>
**setUser (state, value) {**
**state.user = value**
**}**
}
现在我们已经完全准备好创建所需的操作。我将创建四个对我们的应用程序至关重要的操作:
-
createUser:此操作将调用 Firebase auth 的 createUserWithEmailAndPassword 方法,使用给定的电子邮件和密码
-
authenticate:此操作将调用 Firebase auth 的 signInWithEmailAndPassword 方法以使用给定的电子邮件和密码登录用户
-
注销:此操作将调用 Firebase auth 的 signOut 方法
-
bindAuth:此操作将设置 onAuthStateChanged 回调并提交 setUser 变异
首先,让我们以一种非常简单的方式实现这些操作,而不附加任何回调。因此,它们将如下所示:
// store/actions.js
**createUser** ({state}, {email, password}) {
state.firebaseApp.auth().**createUserWithEmailAndPassword**(email, password).catch(error => {
console.log(error.code, error.message)
})
},
**authenticate** ({state}, {email, password}) {
state.firebaseApp.auth().**signInWithEmailAndPassword**(email, password)
},
**logout** ({state}) {
state.firebaseApp.auth().**signOut**()
},
**bindAuth** ({commit, state}) {
state.firebaseApp.auth().**onAuthStateChanged**((user) => {
commit('setUser', user)
})
},
太棒了!现在让我们将 bindAuth 操作附加到主 App.vue 组件的 created 方法上:
// App.vue
methods: {
...mapActions(['bindStatistics', 'bindConfig', **'bindAuth'**])
},
created () {
**this.bindAuth()**
this.bindConfig()
this.bindStatistics()
}
现在,一旦应用程序被创建,身份验证状态的监听器将立即绑定。我们可以做什么?现在,App.vue 组件立即显示的唯一组件是主内容组件。但是,如果用户没有登录,我们实际上应该显示着陆页组件,以提供给用户登录或注册的可能性。我们可以很容易地使用绑定到用户属性的 v-if 指令来实现。如果用户已定义,让我们显示主内容组件;否则,让我们显示着陆页组件。多么简单?我们的 App.vue 组件的模板将如下所示:
// App.vue
<template>
<div id="app">
<landing-page **v-if="!user"**></landing-page>
<main-content **v-if="user"**></main-content>
</div>
</template>
如果您现在打开页面,您将看到显示着陆页:
当应用程序启动时,会显示登陆页面,因为用户没有登录
所有相关的代码到这部分都在chapter6/1/profitoro
文件夹中。特别注意商店的文件(index.js, actions.js, mutations.js, state.js
)和App.vue
组件。
现在我们卡在了显示一些占位文本的登陆页面上,没有办法进入应用程序,因为我们无法登录!
好吧,这很容易解决:让我们在Authentication.vue
组件中创建一个简单的注册和登录表单,并将其与我们的操作连接起来。
所以我将添加组件数据,用于保存注册的电子邮件、登录的电子邮件和相应的密码:
// Authentication.vue
export default {
**data** () {
return {
**registerEmail: '',**
**registerPassword: '',**
**loginEmail: '',**
**loginPassword: ''**
}
}
}
我还将添加一个非常简单的标记,用于显示相应数据的输入:
<template>
<div>
<h1>Register</h1>
<input **v-model="registerEmail"** type="text" placeholder="email">
<input **v-model="registerPassword"** type="password" placeholder="password">
<button>Register!</button>
<h1>Login</h1>
<input **v-model="loginEmail"** type="text" placeholder="email">
<input **v-model="loginPassword"** type="password" placeholder="password">
<button>Log in!</button>
</div>
</template>
现在让我们导入必要的操作(authenticate
和createUser
)并创建将调用这些操作的方法:
// Authentication.vue
<script>
**import {mapActions} from 'vuex'**
export default {
<...>
methods: {
**...mapActions(['createUser', 'authenticate'])**,
**onRegisterClick** () {
this.**createUser**({email: **this.registerEmail**, password: **this.registerPassword**})
},
**onLoginClick** () {
this.**authenticate**({email: **this.loginEmail**, password: **this.loginPassword**})
}
}
}
</script>
现在我们只需要将事件绑定到相应的按钮上,使用v-on:click
指令:
// Authentication.vue
<template>
<div>
<h1>Register</h1>
<input v-model="registerEmail" type="text" placeholder="email">
<input v-model="registerPassword" type="password" placeholder="password">
<button **@click="onRegisterClick"**>Register!</button>
<h1>Login</h1>
<input v-model="loginEmail" type="text" placeholder="email">
<input v-model="loginPassword" type="password" placeholder="password">
<button **@click="onLoginClick"**>Log in!</button>
</div>
</template>
让我们还在HeaderComponent.vue
组件中添加一个按钮。这个按钮应该允许用户注销。这很容易;我们甚至不需要创建任何方法,我们只需要将事件绑定到实际的操作。因此整个标记和所需的脚本看起来就像这样简单:
// HeaderComponent.vue
<template>
<div>
<button **@click="logout"**>Logout</button>
</div>
</template>
<script>
**import {mapActions} from 'vuex'**
export default {
methods: {
**...mapActions(['logout'])**
}
}
</script>
就是这样!打开页面并尝试在你的应用程序中注册!它起作用了!一旦你登录,你不仅会看到番茄钟,还可以看到注销按钮。点击它,检查你是否真的被踢出应用程序到登陆页面。尝试重新登录。一切都像魅力一样运行。
不要忘记打开你的 Firebase 控制台并检查认证选项卡。你应该在那里看到所有注册的用户:
通过 Firebase 控制台的认证选项卡监视你的注册用户
恭喜!你刚刚使用 Firebase 认证 API 为你的应用程序实现了完整的认证机制。你可以在chapter6/2/profitoro
文件夹中找到相应的代码。特别注意Authentication.vue
和HeaderComponent.vue
组件。
使认证界面再次变得伟大
我们刚刚为我们的 ProFitOro 应用程序实现了认证机制。这很棒,但是我们的认证页面的 UI 看起来好像我们使用了时光机,回到了互联网的早期。让我们使用我们强大的朋友-Bootstrap 来解决这个问题。
首先,我想将我的登陆页面布局为两列网格布局,因此整个登录属于左列,而将用户引导到应用程序而不注册的按钮位于右侧。但是,我希望这两列在移动设备上堆叠。
这对您来说并不新鲜;我想您应该还记得如何使用 Bootstrap 的网格布局来实现这种行为:v4-alpha.getbootstrap.com/layout/grid/
。因此,在我们的LandingPage
组件中,我将把认证和go-to-app-link
组件包装到带有row
类的div
中,并为这些组件添加相应的col-*
类:
// LandingPage.vue
<template>
<div>
<...>
<div class="**container row justify-content-center**">
<div class="**col-sm-12 col-md-6 col-lg-6**">
<authentication></authentication>
</div>
<div class="**col-sm-12 col-md-6 col-lg-6**">
<go-to-app-link></go-to-app-link>
</div>
</div>
</div>
</template>
就是这样!现在您有一个漂亮的两列布局,在小型设备上会转换为单列布局:
这是我们在桌面设备上的布局
如您所见,在桌面设备上,我们有一个漂亮的两列布局。如果将浏览器调整到移动设备的大小,右列将跳到左列后面:
这是我们在移动设备上的布局
现在让我们来看看我们的Authentication.vue
组件。为了使其比 20 年前的网页更美观,让我们对其应用 Bootstrap 的魔法。为此,我们将使用 Bootstrap 表单的类:v4-alpha.getbootstrap.com/components/forms/
。
我们将整个表单包装到<form>
标签中,将每个输入都包装到带有form-group
类的div
中。我们还将为每个输入添加form-control
类。因此,例如,电子邮件输入将如下所示:
<div class="**form-group**">
<input class="**form-control**" v-model="email" type="email" placeholder="email">
</div>
作为一个小练习,做以下事情:
-
只需创建一个表单,其中有一个按钮可以在登录和注册表单之间切换
-
只需创建一个方法,根据表单当前的状态调用其中一个动作
-
探索 Bootstrap 的实用程序类,以除去所有边框,除了底部边框,并从中删除圆角:
v4-alpha.getbootstrap.com/utilities/borders/
最后,您的表单应该如下所示:
这就是最终两种形式应该看起来的样子。它们应该通过底部按钮进行切换
尝试自己实现。要检查您的工作,请查看chapter6/3/profitoro
文件夹。特别是,检查Authentication.vue
组件的代码。它非常不同!
管理匿名用户
ProFitOro 允许未注册用户使用该应用程序。唯一的区别是,这些未注册用户也不被允许配置他们的设置,因为他们无法访问他们的统计数据。他们也无法管理锻炼。这就是我们遇到三 A 定义的第二个 A - 授权。我们如何管理这些用户?如果我们只允许我们的用户注册和登录,他们如何进入应用程序?好吧,出于某种原因,我们已经准备好了“转到应用程序”的部分。让我提醒您在模型中的外观:
在初始模型中的无需注册!按钮
幸运的是,Firebase 身份验证 API 提供了一种方法来登录匿名用户。返回的用户对象包含isAnonymous
属性,这将允许我们管理可以或不可以访问此匿名用户的资源。因此,让我们添加名为authenticateAnonymous
的操作,并在其中调用相应的 Firebase auth
方法:
// store/actions.js
**authenticateAnonymous** ({state}) {
state.firebaseApp.auth().**signInAnonymously**().catch(error => {
console.log(error.code, error.message)
})
},
这就是我们!现在让我们稍微修改一个设置用户和isAnonymous状态属性的变化,使其与用户对象中的相应属性相对应:
// store/mutations.js
setUser (state, value) {
state.user = value
**state.isAnonymous = value.isAnonymous**
}
让我们还修改绑定配置和统计操作,并仅在用户设置且用户不是匿名用户时执行实际绑定:
// state/actions.js
bindConfig: firebaseAction(({bindFirebaseRef, state}) => {
if (state.user **&& !state.isAnonymous**) {
bindFirebaseRef('config', state.configRef)
}
}),
bindStatistics: firebaseAction(({bindFirebaseRef, state}) => {
if (state.user **&& !state.isAnonymous**) {
bindFirebaseRef('statistics', state.statisticsRef)
}
})
我们已经完成了后端!现在让我们实现这个按钮!只需三个步骤即可实现。打开GoToAppLink.vue
组件,导入mapActions
助手,添加按钮,并使用v-on:click
指令将事件侦听器绑定到它,该事件侦听器将调用相应的操作:
// GoToAppLink.vue
<template>
<div>
**<button @click="authenticateAnonymous">**
**START WITHOUT REGISTRATION**
**</button>**
</div>
</template>
<script>
**import {mapActions} from 'vuex'**
export default {
methods: {
**...mapActions(['authenticateAnonymous'])**
}
}
</script>
这有多简单?现在,作为一个小练习,借助 Bootstrap,尝试使事物看起来像下面这样:
使用相应的 Bootstrap 类使我们的按钮看起来像这样,并垂直对齐列。
检查 Bootstrap 的对齐类:v4-alpha.getbootstrap.com/layout/grid/#alignment
。还要检查辅助类以去除圆角。通过查看chapter6/4/profitoro
文件夹中的代码来检查自己。特别注意GoToAppLink.vue
组件和存储组件,如action.js
和mutations.js
。
个性化番茄钟
现在,我们已经可以注册新用户并登录现有用户,可能我们应该考虑利用我们的身份验证机制,因为现在我们实际上没有利用它。我们只是注册和登录。是的,我们还可以根据用户的身份验证隐藏或显示一些内容,但这还不够。所有这一切努力的重点是能够存储和检索用户的自定义番茄钟配置和用户的统计数据。
到目前为止,我们一直在使用硬编码的数据库对象,其中包含键test
,以便访问用户的数据,但现在,由于我们已经有了真正的用户,是时候用真正的用户数据填充数据库并在我们的应用程序中使用它了。实际上,我们唯一需要做的就是用实际用户的 ID 替换这个硬编码的值。因此,例如,我们绑定config
引用的代码看起来像这样:
// store/actions.js
bindConfig: firebaseAction(({bindFirebaseRef, state}) => {
if (state.user && !state.isAnonymous) {
bindFirebaseRef('config', **state.configRef**)
}
}),
在这里,引用state.configRef
已经在存储的入口点index.js
中定义:
// store/actions.js
let firebaseApp = firebase.initializeApp(config)
let db = firebaseApp.database()
**let configRef = db.ref('/configuration/test')**
现在,我们实际上不能在存储的入口点实例化我们的数据库引用,因为在这一点上(无意冒犯),我们仍然不知道我们的用户是否已经通过身份验证。因此,最好的做法是将此代码传递给实际的bindConfig
函数,并用真实用户的uid替换这个test
:
// store/action.js
bindConfig: firebaseAction(({bindFirebaseRef, state}) => {
if (state.user && !state.isAnonymous) {
let db = firebaseApp.database()
bindFirebaseRef('config', **db.ref(`/configuration/${state.user.uid}`)**)
}
}),
现在,我亲爱的细心用户,我知道你在惊叹“但是用户的uid配置是如何存储的?”非常注意到:它没有。我们仍然需要在用户首次注册时将其存储。实际上,我们需要存储配置和统计数据。
Firebase 数据库提供了一种写入新数据到数据库的方法,称为set
。因此,您基本上获取引用(就像读取数据的情况一样)并设置您需要写入的数据:
firebaseApp.database().ref(**`/configuration/${state.user.uid}`**).set(
state.config
);
这将在我们的配置表中为给定的用户 ID 创建一个新条目,并设置默认状态的config
数据。因此,我们将不得不在新用户创建时调用此方法。我们仍然需要将数据库引用绑定到我们的状态对象。为了减少代码量,我创建了一个方法bindFirebaseReference
,它接收引用和表示应将其绑定到的状态键的字符串。该方法将分析数据库中是否已存在给定引用的条目,并在需要时创建它。为此,Firebase 提供了一个几乎可以应用于任何东西的好方法 - 这个方法称为once
,它接收一个回调和一个快照。因此,在此回调中,我们可以分析此快照是否具有给定名称的子项,甚至是否具有值或为null
。如果值已设置,我们将将我们的状态绑定到它。如果没有,我们将创建一个新条目。在这方面查看官方 Firebase 文档:firebase.google.com/docs/database/web/read-and-write
。这就是once
方法及其回调的外观:
如何使用once
方法检查数据库中是否存在数据
不考虑数据的存在与否,我们的绑定引用方法应调用 Firebase 绑定。因此,它将如下所示:
// store/actions.js
bindFirebaseReference: firebaseAction(({bindFirebaseRef, state}, {reference, toBind}) => {
return reference.once('value').then(snapshot => {
if (!snapshot.val()) {
**reference.set(state[toBind])**
}
**bindFirebaseRef(toBind, reference)**
})
}),
我还用一个方法替换了绑定config
和statistics
的两种方法:
// store/actions.js
bindFirebaseReferences: firebaseAction(({bindFirebaseRef, state, commit, dispatch}, user) => {
let db = state.firebaseApp.database()
let **configRef** = db.ref(**`/configuration/${user.uid}`**)
let **statisticsRef** = db.ref(**`/statistics/${user.uid}`**)
dispatch('bindFirebaseReference', {reference: configRef, toBind: 'config'}).then(() => {
**commit('setConfigRef', configRef)**
})
dispatch('bindFirebaseReference', {reference: statisticsRef, toBind: 'statistics'}).then(() => {
**commit('setStatisticsRef', statisticsRef)**
})
}),
这个方法是从bindAuth
方法中调用的。因此,现在我们可以从App.vue
的created
方法中删除绑定config
和statistics
的调用。我们还不需要在store/index.js
中实例化引用,因为这两个引用都是在这个新方法中实例化的。我们还必须添加两个将引用设置为状态的 mutations,这样我们就不需要更改我们的 Pomodoro 配置设置 actions,因为它们正在使用这两个引用来更新数据。
检查chapter6/5/profitoro
文件夹中代码的外观。查看App.vue
组件中的轻微更改,并查看存储文件现在的外观(index.js
,mutations.js
,state.js
,特别是actions.js
)。
玩一下你的应用程序。注册、登录、更改番茄钟定时器配置、退出登录,然后检查它是否有效。检查你的 Firebase 控制台 - 实时数据库选项卡和身份验证选项卡。检查无论你改变什么,你的数据都是一致的 - 在你的数据库中,在你的身份验证选项卡中,最重要的是在你的应用程序中(因为应用程序是你的用户将要看到的,对吧?):
检查数据在各处是否一致
现在我们可以注册新用户,以现有用户身份登录,以匿名用户身份登录。我们为经过身份验证的用户提供了一个不错的价值 - 能够配置他们的番茄钟定时器并检查他们的统计数据。当然,我们的应用程序还远远不完美 - 我们没有验证输入,接受番茄钟配置区域中的任何值,这是不对的,而且我们也没有在启动页面上显示更改密码的可能性。但是我们有一个坚实的框架,它使我们能够在其基础上构建一个坚实而不错的应用程序。所以让我们继续前进!
更新用户的个人资料
如果我们能够通过显示欢迎消息来欢迎我们的用户,比如欢迎 Olga,那不是很有趣吗?但是我们的用户没有名字;他们只有电子邮件和密码 - 这两个在注册过程中传递的基本认证组件。那么,我们该怎么做呢?如果你仔细阅读了 Firebase 关于身份验证的文档(firebase.google.com/docs/auth/web/manage-users
),你可能会发现这些不错的方法:
用于更新用户个人资料和电子邮件地址的 Firebase 方法
让我们使用这些方法来更新我们用户的个人资料和用户的个人资料图片!
我们将定义三个新的操作 - 一个将通过调用 Firebase 的updateProfile
方法来更新用户的显示名称,另一个将通过调用相同的方法来更新用户的个人资料图片 URL,还有一个将调用updateEmail
方法。然后我们将在Settings.vue
组件中创建必要的标记,将这些操作绑定到相应输入的更新上。听起来很容易,对吧?相信我,实际实现起来就像听起来的那么容易。
因此,让我们定义我们的操作。它们将如下所示:
// store/actions.js
**updateUserName** ({state, commit}, displayName) {
state.user.**updateProfile**({
displayName
})
},
**updatePhotoURL** ({state}, photoURL) {
state.user.**updateProfile**({
photoURL
})
},
**updateUserEmail** ({state}, email) {
state.user.**updateEmail**(email).then(() => {
// Update successful.
}, error => {
console.log(error)
})
},
太棒了!现在让我们切换到我们的Settings.vue
组件,它将负责渲染所需的数据以更改帐户设置,并在需要时调用所需的操作来更新这些数据。所以首先,我将向数据函数添加三个条目,这些条目将在组件被created
时设置为当前用户对象的相应属性:
// Settings.vue
data () {
return {
**displayName**: '',
**email**: '',
**photoURL**: 'static/tomato.png'
}
},
computed: {
...mapGetters({**user**: 'getUser'})
},
created () {
**this.displayName** = this.user.displayName
**this.email** = this.user.email
**this.photoURL** = this.user.photoURL ? this.user.photoURL : this.photoURL
}
现在这些数据可以在相应的操作中使用。所以,让我们导入所需的操作并创建相应的方法:
// Settings.vue
methods: {
**...mapActions(['updateUserName', 'updateUserEmail', 'updatePhotoURL'])**,
onChangeUserName () {
this.**updateUserName**(this.**displayName**)
},
onChangeUserEmail () {
this.**updateUserEmail**(this.**email**)
},
**onProfilePicChanged** () {
this.**updatePhotoURL**(this.**photoURL**)
}
}
现在我们可以添加所需的标记,其中包含了我们将使用v-model
数据绑定指令绑定数据的输入框!我们还将在每个输入框的更新上调用相应的方法:
// Settings.vue
<form>
<div class="form-group">
<figure class="figure">
<img **:src="photoURL"** alt="Avatar">
<input type="text" **v-model="photoURL"** **@change="onProfilePicChanged"**>
</figure>
</div>
<div class="form-group">
<input **@change="onChangeUserName"** **v-model="displayName"** type="text" placeholder="Change your username">
</div>
<div class="form-group">
<input **@change="onChangeUserEmail"** **v-model="email"** type="text" placeholder="Change your username">
</div>
</form>
然后...我们完成了!
作为一个小练习,做以下操作:在我们的图像后面添加一个标题,说更改个人资料图片。新图片 URL 的输入框应该只在用户点击这个标题时可见。一旦 URL 更新完成,输入框应该再次变得不可见。
结果应该如下所示:
用户点击更改个人资料图片标题之前的外观如下
最初,它包含默认用户图片。
用户点击标题后,更改图片 URL 的输入框出现:
用户点击标题后,输入框出现
用户更改个人资料图片 URL 后,输入框再次隐藏:
用户更改个人资料图片的 URL 后,输入框消失了
我的建议:向Settings.vue
组件的数据添加一个额外的属性,当用户点击标题时将其设置为true
,并在输入框内的值改变时将其重置为false
。
还有,不要忘记我们这一部分的初始目标 - 在Header.vue
组件内添加一个欢迎消息。这个欢迎消息应该包含用户的显示名称。它应该看起来像这样:
欢迎消息提到用户的名字
请注意,如果您决定更改您的电子邮件,您将不得不注销并再次登录;否则,您将在控制台中收到一些 Firebase 安全错误。
本章的最终代码可以在chapter6/6/profitoro
文件夹中找到。请注意,我将帐户设置和番茄钟设置分成了两个单独的组件(AccountSettings.vue
和PomodoroTimerSettings.vue
)。这样做可以更容易地进行维护。也要注意存储组件。查看Header.vue
组件以及它如何显示欢迎消息。
总结
在本章中,我们学习了如何结合 Firebase 实时数据库和认证 API 来更新用户的设置。我们已经构建了一个用户界面,允许用户更新其个人资料设置。在短短几分钟内,我们就完成了应用程序的完整认证和授权部分。我不知道你们,但我对此感到非常惊讶。
在下一章中,我们将最终摆脱包含应用程序所有部分的庞大页面 - 番茄钟计时器本身、统计数据和设置配置视图。我们将探索 Vue 的一个非常好的重要功能 - vue-router
。我们将把它与 Bootstrap 的导航系统结合起来,以实现流畅的导航。我们还将探讨代码拆分这样一个热门话题,以实现应用程序的延迟加载。所以,让我们开始吧!
第七章:使用 vue-router 和 Nuxt.js 添加菜单和路由功能
在上一章中,我们为我们的应用程序添加了一个非常重要的功能 - 身份验证。现在,我们的用户可以注册、登录应用程序,并在登录后管理他们的资源。因此,他们现在可以管理番茄钟计时器的配置和他们账户的设置。一旦登录,他们还可以访问他们的统计数据。我们已经学会了如何使用 Firebase 的身份验证 API 并将 Vue 应用程序连接到它。我必须说,上一章在学习上非常广泛,而且非常偏向后端。我非常喜欢它,希望你也喜欢。
尽管我们的应用程序具有身份验证和授权的复杂功能,但仍然缺乏导航。出于简单起见,我们目前在主页上显示应用程序的所有部分。这很丑陋:
承认吧,这很丑陋
在本章中,我们不打算让事情变得美丽。我们要做的是使事情可导航,以便通过导航访问应用程序的所有部分。我们将应用vue-router
机制,以实现自然的浏览器导航,并且我们将使用 Bootstrap 的navbar
来轻松导航到每个部分。因此,在本章中,我们将:
-
再次探索
vue-router
以实现 ProFitOro 应用程序的导航 -
使用 Bootstrap 的
navbar
来渲染导航栏 -
探索代码拆分技术,仅在需要时加载应用程序的每个部分
-
最后,我们将探索 Nuxt.js 模板,使用它重建我们的应用程序,并以不显眼和愉快的方式实现路由
使用 vue-router 添加导航
希望你还记得第二章中vue-router
是什么,它是做什么的,以及它是如何工作的。只是提醒一下:
Vue-router 是 Vue.js 的官方路由器。它与 Vue.js 核心深度集成,使使用 Vue.js 构建单页面应用程序变得轻而易举。
-(来自 vue-router 的官方文档)
vue-router
非常容易使用,我们不需要安装任何东西 - 它已经与 Vue 应用程序的默认脚手架和 webpack 模板一起提供。简而言之,如果我们有应该表示路由的 Vue 组件,这就是我们要做的事情:
-
告诉 Vue 使用
vue-router
-
创建一个路由实例并将每个组件映射到其路径
-
将此实例传递给 Vue 实例或组件的选项
-
使用
router-view
组件进行渲染
注意
查看官方vue-router
文档:router.vuejs.org
创建路由时,应将路由数组传递给它。每个数组项表示给定组件与某个路径的映射:
{
name: 'home',
component: HomeComponent,
path: '/'
}
ProFitOro 只有四个可能的路由 - 番茄钟计时器本身,我们可以将其视为主页,带有设置和统计信息的视图,以及协作锻炼的视图。因此,我们的路由看起来非常简单易懂:
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import {PomodoroTimer, Settings, Statistics, Workouts} from '@/components/main/sections'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
name: **'home'**,
component: **PomodoroTimer**,
path: '**/**'
},
{
name: **'settings'**,
component: **Settings**,
path: '**/settings'**
},
{
name: **'statistics'**,
component: **Statistics**,
path: '**/statistics**'
},
{
name: **'workouts'**,
component: **Workouts**,
path: '**/workouts**'
}
]
})
现在,如果您在ContentComponent
视图中导入创建的路由,将其传递给组件的选项并渲染router-view
组件,您将能够看到 Vue 路由的实际效果!您还可以删除所有组件导入,因为ContentComponent
现在实际上应该导入的唯一事物是负责其他一切的路由。因此,ContentComponent
将如下所示:
// ContentComponent.vue
<template>
<div class="container">
**<router-view></router-view>**
</div>
</template>
<script>
**import router from '@/router'**
export default {
**router**
}
</script>
打开页面,在浏览器地址栏中输入localhost:8080/settings
,localhost:8080/statistics
,localhost:8080/workouts
,您将看到视图根据您实际尝试访问的内容而出现。您必须承认,这真的很容易。
现在让我们添加链接,因为我们希望通过单击某些按钮进行导航,而不是在浏览器地址栏中输入导航 URL,对吧?
使用vue-router
添加导航链接非常容易。使用提供的router-link
组件,带有指向所需路径的to
属性的链接:
<router-link to="/">Home</router-link>
让我们在我们的Header
组件中添加这些链接。这个组件应该负责导航表示。因此,在我们的HeaderComponent.vue
的template
部分中,添加以下内容:
// HeaderComponent.vue
<template>
<router-link to="/">Home </router-link>
<router-link to="statistics">Statistics </router-link>
<router-link to="workouts">Workouts </router-link>
<router-link to="settings">Settings </router-link>
</template>
不要忘记在组件选项中导入路由并导出它:
// HeaderComponent.vue
<script>
//...
**import router from '@/router'**
export default {
//
**router**
}
</script>
通过一点 Bootstrap 类的调整,我们可以得到如下结果:
使用 vue-router 导航 ProFitOro
这就是用vue-router
及其组件进行路由和导航的基础知识。您可以在chapter7/1/profitoro
文件夹中找到此部分的最终代码。特别注意路由器本身(router/index.js
)、ContentComponent.vue
和HeaderComponent.vue
文件。
练习 - 根据身份验证限制导航
不要忘记,我们必须根据用户的身份验证状态限制导航链接。如果用户是匿名身份验证的,我们不应该显示导航链接。然而,应该有一个按钮,让用户返回到主页。因此,这个按钮应该调用注销功能,并显示不同的文本,比如返回到起始页。您已经知道如何有条件地渲染 Vue 组件。我们的路由链接不过是普通组件,所以根据用户的值和其属性isAnonymous
应用条件渲染机制。
在chapter7/2/profitoro
文件夹中检查此练习的解决方案。注意HeaderComponent
组件。
使用 Bootstrap 导航栏进行导航链接
我们当前的导航栏很棒 - 它很实用,但不是响应式的。幸运的是,Bootstrap 有一个navbar
组件,为我们实现了响应性和适应性。我们只需用一些 Bootstrap 类包装我们的导航元素,然后坐下来检查我们美丽的导航栏,在移动设备上折叠,在桌面设备上展开。查看 Bootstrap 关于navbar
组件的文档:v4-alpha.getbootstrap.com/components/navbar/
。
注意
请记住,此 URL 是用于 alpha 版本。下一个稳定版本 4 将在官方网站上提供。
这些是我们将使用的类,将我们简单的导航栏转换为由 Bootstrap 管理的响应式导航栏:
-
导航栏:这个包裹整个导航栏元素
-
navbar-toggleable-*
:这也应该包裹整个导航栏元素,并告诉它何时在展开/折叠状态之间切换(例如,navbar-toggleable-md
会使导航栏在中等大小设备上折叠) -
navbar-toggler
:这是一个用于在小型设备上打开折叠菜单的按钮类 -
navbar-toggler-*
:这告诉toggler
元素应该被放置在哪里,例如,navbar-toggler-right
-
navbar-brand
:这是代表品牌的导航栏元素的类(可以是标志和/或文本) -
collapse navbar-collapse
:这些类将包裹应该在小设备上折叠的导航栏元素 -
nav-item
:这是每个导航栏项的类 -
nav-link
:这是nav-item
项的嵌套元素的类;最终这将是一个将您带到给定链接的锚点
还有许多其他类来定义导航栏的颜色方案,以及其定位、对齐等。查看文档并尝试它们。我将只改变Header
组件的标记。因此,它将看起来像下面这样:
// HeaderComponent.vue
<template>
<div>
<nav class="**navbar navbar-toggleable-md navbar-light**">
<button class="**navbar-toggler navbar-toggler-right**" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="**navbar-brand**">
<logo></logo>
</div>
<div class="collapse navbar-collapse" id="navbarHeader">
<ul class="navbar-nav ml-auto">
<li class="**nav-item**">
<router-link class="**nav-link**" to="/">Home </router-link>
</li>
<li class="nav-item">
<router-link class="**nav-link**" to="settings">Settings </router-link>
</li>
<li class="**nav-item**">
<router-link class="**nav-link**" to="statistics">Statistics </router-link>
</li>
<li class="**nav-item**">
<router-link class="**nav-link**" to="workouts">Workouts </router-link>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<button class="btn btn-secondary" @click="onLogout">Logout</button>
</form>
</div>
</nav>
</div>
</template>
您可能已经注意到,我在导航项中使用了我们的router-link
元素和nav-link
类。事实证明它们非常好地配合在一起。因此,我们将 Vue 路由机制与 Bootstrap 的导航栏混合在一起,在我们的 Vue 应用程序中实现了一个优雅的响应式路由解决方案。现在,我们的页眉看起来就像这样:
ProFitOro 在桌面设备上的导航栏
如果我们在移动设备上打开 ProFitOro,我们将看到一个漂亮的切换按钮而不是菜单:
这是 ProFitOro 在移动设备上的菜单样子
如果我们在移动设备上点击切换按钮,菜单将垂直展开:
这是在移动设备上扩展的 ProFitOro 菜单的样子
提示
这在 Bootstrap 4 的 alpha 版本中运行良好,但是如果您使用 Bootstrap 4 Beta,您将看到一些不一致之处。一些类被删除,一些类被添加。为了使它看起来完全相同,做如下操作:
-
用
navbar-expand-lg
替换navbar-tooglable-md
类 -
将
btn-secondary
按钮的类替换为button-outline-secondary
,交换切换按钮和品牌元素
基于身份验证的条件渲染功能已被删除。我将重新添加它,但是不再在用户匿名时隐藏元素,而是将它们禁用。这将为应用程序带来额外的价值-未注册用户将不断被提醒,如果他注册,就可以使用一些不错的功能。因此,我将把disabled
Bootstrap 类绑定到router-link
元素上。如果用户是匿名的,这个类将被激活。因此,我们的每个路由链接将如下所示:
// HeaderComponent.vue
<router-link class="nav-link" **:class="{disabled:user.isAnonymous}"** to="settings">Settings </router-link>
如果你现在打开页面并以匿名用户的身份进入应用程序,你会发现链接显示为禁用状态:
对于未经授权的用户,链接将显示为禁用状态
但是,我们的用户很聪明,我们都知道,对吧?我们的用户将做与你现在考虑做的事情完全相同(如果你还没有做过的话)-打开开发者工具控制台,转到元素选项卡,编辑标记并删除disabled
类。Ba-dum-tsss,现在你可以点击导航链接了!
因此,我们还需要在路由器端保护它。幸运的是,vue-router
实例提供了一个名为beforeEach
的钩子方法。这个方法接收下一个和上一个路由,并且在其中你可以评估它们并调用next()
方法,这将根据条件继续到下一个路由或替换被调用的路径。
此外,每个路由项都可以包括元属性,我们可以在其中传递一个条件,该条件决定是否可以调用该路由。在这方面,请查看vue-router
文档:router.vuejs.org/en/advanced/meta.html
。
因此,让我们为每个三个路由项添加一个requiresAuth
的元属性,并像这样使用beforeEach
方法:
// router/index.js
router.beforeEach((**to, from, next**) => {
if (to.matched.some(record => **record.meta.requiresAuth**)) {
if **(!store.state.user || store.state.user.isAnonymous)** {
next({
**path: '/'**
})
} else {
**next()**
}
} else {
**next()**
}
})
Et voilá,从现在开始,即使你在未经授权的情况下在浏览器地址栏中明确输入了其中一个有条件的路由 URL,你也会被重定向到主页!
查看chapter7/3/profitoro
文件夹中此部分的最终代码。特别注意路由器本身(router/index.js
)和Header
组件。
代码拆分或延迟加载
当我们构建应用程序以部署到生产环境时,所有 JavaScript 都被捆绑到一个唯一的 JavaScript 文件中。这非常方便,因为一旦浏览器加载了这个文件,整个应用程序已经在客户端上了,没有人担心加载更多的东西。当然,这仅适用于单页应用程序。
我们的 ProFitOro 应用程序(至少在这个阶段)受益于这种捆绑行为-它很小,只有一个请求,一切就位,我们不需要为任何 JavaScript 文件从服务器请求任何内容。
然而,这种捆绑可能会有一些缺点。我非常确定您已经构建过或已经看到过庞大的 JavaScript 应用程序。总会有一些时候,加载庞大的捆绑包将变得难以忍受地慢,特别是当我们希望这些应用程序在桌面和移动环境下运行时。
这个问题的一个明显解决方案是以一种方式拆分代码,只有在需要时才加载不同的代码块。这对于单页应用程序来说是一个相当大的挑战,这就是为什么我们现在有一个庞大的社区致力于网页开发。
目前,在网页开发领域已经存在一些简单的技术,可以用来拆分 webpack 应用程序中的代码。查看官方 webpack 文档以了解更多信息:webpack.js.org/guides/code-splitting/
。
为了在 Vue.js 应用程序中使用代码拆分,您不需要做任何复杂的事情。无需重新配置您的 webpack 配置文件,也无需重写您的组件。查看有关延迟加载路由的文档条目:router.vuejs.org/en/advanced/lazy-loading.html
。
提示
TL;DR:为了延迟加载您的路由,您只需要改变导入它们的方式。因此,请考虑以下代码:import PomodoroTimer from '@/components/main/sections/PomodoroTimer'
要惰性加载您的路由,您应该写成以下形式:const PomodoroTimer = () => import('@/components/main/sections/PomodoroTimer')
其余的代码保持完全不变!
因此,我们只需改变在路由器中导入组件的方式:
// router/index.js
const PomodoroTimer = () => import('@/components/main/sections/PomodoroTimer')
const Settings = () => import('@/components/main/sections/Settings')
const Statistics = () => import('@/components/main/sections/Statistics')
const Workouts = () => import('@/components/main/sections/Workouts')
就是这样!检查页面,确保一切仍然按预期工作。检查网络面板。您会看到现在将为不同的路由视图请求不同的 JavaScript 包!
如果您将网络请求与以前的版本进行比较,您将看到现在实际上有四个请求-0.js
,1.js
,2.js
和3.js
-与以前的单个app.js
请求相比:
代码分割或延迟加载之前的 app.js 包的单个请求
在代码分割之后,如果我们通过应用程序的导航链接导航,我们将看到以下内容:
每个路由都会请求相当小的 JavaScript 块
注意一下块的大小。您不认为对于大型项目,代码分割技术实际上可能会增加应用程序的性能吗?检查chapter7/3.1/profitoro
文件夹中的路由器代码。
服务器端渲染
服务器端渲染(SSR)最近成为了 Web 开发世界中又一个流行的缩写词。它与代码分割技术一起使用,有助于提高 Web 应用的性能。它还对 SEO 产生积极影响,因为所有内容一次性加载,爬虫能够立即看到,而不是在初始请求后在浏览器中构建内容的情况。
我找到了一篇关于 SSR 的好文章,比较了服务器端和客户端渲染(尽管它是 2012 年的)。看看这个链接:openmymind.net/2012/5/30/Client-Side-vs-Server-Side-Rendering/
。
将服务器端渲染引入 Vue 应用程序非常容易-在这方面查看官方文档:ssr.vuejs.org
。
我们的应用程序性能很重要;SEO 的工作也很重要。然而,重要的是不滥用工具,不引入实现开销和过度。我们的 ProFitOro 应用程序需要 SSR 吗?要回答这个问题,让我们考虑一下我们的内容。如果有大量的内容被带到页面上,并被用作初始渲染的基础,那么答案可能是肯定的。好吧,这不适用于我们的应用程序。我们有一个简单的登录页面,我们的 ProFitOro 计时器,以及一些配置设置。目前可能有意义的唯一视图是包含锻炼的视图。但现在,让我们不要把事情搞得太复杂。您可以尝试使用我们的 ProFitOro 进行 Vue 应用程序的服务器端渲染技术,但请记住,这不是应该始终使用的东西。还要了解服务器端渲染和预渲染之间的区别(github.com/chrisvfritz/prerender-spa-plugin
),并检查我们的应用程序实际上如何从这两种技术中受益。
Nuxt.js
在我们忙于定义路由器对象、路由器链接、代码拆分和学习有关服务器端渲染的知识时,有人实现了一种开发 Vue.js 应用程序的方式,而不必担心所有这些事情。只需编写您的代码。所有诸如路由、代码拆分甚至服务器端渲染的事情都将在幕后为您处理!如果你想知道这到底是什么,让我向你介绍 Nuxt.js:nuxtjs.org
。
那么,Nuxt.js 是什么?
Nuxt.js 是用于创建通用 Vue.js 应用程序的框架。
它的主要范围是 UI 渲染,同时抽象出客户端/服务器分发。
它有什么了不起的地方?Nuxt.js 引入了页面的概念 - 基本上,页面也是 Vue 组件,但每个页面代表一个路由。一旦您在pages
文件夹中定义了您的组件,它们就会成为路由,无需任何额外的配置。
在本章中,我们将完全将我们的 ProFitOro 迁移到 Nuxt 架构。所以,做好准备;我们将进行大量的更改!在本章结束时,我们的努力将得到一段漂亮、优雅的代码。
Nuxt 应用有一个单独的config
文件,你可以在其中定义必要的 webpack 配置,以及meta
、links
和额外的scripts
用于你的index.html
文件。这是因为 Nuxt 会在构建过程中自动生成你的index.html
,所以你不必在应用的根目录中拥有它。在这个配置文件中,你还可以定义每个路由变化时应该发生的过渡效果。
创建 Nuxt 应用的方式与创建任何 Vue 应用非常相似 - 所有 Nuxt.js 功能都内置在nuxt-starter
模板中:github.com/nuxt-community/starter-template
。因此,使用 Nuxt 模板创建 Vue.js 应用只是:
**vue init nuxt/starter <project-name>**
让我们创建一个profitoro-nuxt
项目并看看它是如何工作的。运行以下命令:
**vue init nuxt/starter profitoro-nuxt**
点击 Enter 回答问题。
进入生成的文件夹,安装依赖,并运行应用:
**cd profitoro-nuxt**
**npm install**
**npm run dev**
在localhost:3000
上打开页面,并确保你看到这个:
Nuxt 应用的初始默认页面
让我们来探索文件夹的结构和代码。有一个名为pages
的文件夹,你可以在里面找到index.vue
页面。还有一个名为components
的文件夹 - 在这里我们将存储我们的组件。有一个nuxt.config.js
文件,其中存储了所有基本配置。简而言之,就是这样。
让我们来处理pages
文件夹。我们的 ProFitOro 应用的哪些组件可以定义为pages
?很容易识别它们,因为我们已经定义了路由。所以,我会说我们可以识别以下页面:
-
index.vue
:这将检查用户是否已登录,并渲染登录页面或番茄钟计时器页面 -
login.vue
:这个页面与我们当前的LandingComponent.vue
完全相同 -
pomodoro.vue
:这将是包含番茄钟计时器组件的页面 -
settings.vue
:这个页面将代表我们的Settings.vue
组件 -
statistics.vue
:这个页面将负责渲染Statistics.vue
组件 -
workouts.vue
:这个页面将负责管理锻炼
让我们为所有这些页面创建占位符。这是我在pages
文件夹内部的目录结构:
**├── pages**
**│ ├── index.vue**
**│ ├── login.vue**
**│ ├── pomodoro.vue**
**│ ├── settings.vue**
**│ ├── statistics.vue**
**│ └── workouts.vue**
这是login.vue
页面的初始内容:
//login.vue
<template>
<div>
login
</div>
</template>
<script>
</script>
<style scoped>
</style>
其他页面都和这个页面非常相似,除了index.vue
页面:
//index.vue
<template>
<div>
<pomodoro></pomodoro>
<login></login>
</div>
</template>
<script>
**import login from './login'**
**import pomodoro from './pomodoro'**
export default {
components: {login, pomodoro}
}
</script>
<style>
</style>
如果你在浏览器中打开此应用程序,并尝试在浏览器的地址栏中键入不同的路径(localhost:3000/pomodoro
,localhost:3000/settings
等),你将看到它实际上呈现了相应的页面。多么美妙啊!我们不需要定义任何路由或任何额外的配置就能实现这种行为!在chapter7/4/profitoro-nuxt
文件夹中检查此部分的代码。
使用 nuxt-link 添加链接
就像vue-router
提供了一个名为router-link
的组件一样,Nuxt 提供了一个非常相似的组件,名为nuxt-link
。让我们使用 nuxt-links 而不是 router-links 来更改我们的HeaderComponent
,并将此组件包含在我们的页面中。
在这之前,让我们安装sass-loader
,因为,如果你记得的话,我们正在使用 sass 预处理器来处理我们的 CSS,而我们的HeaderComponent
实际上在很大程度上依赖于它。因此,请继续运行以下命令:
**npm install --save-dev node-sass sass-loader**
我还重新包含了 Bootstrap 样式,使用它的sass样式而不是纯 CSS。查看chapter7/5/profitoro-nuxt
文件夹中的assets/styles
文件夹。在此文件夹中运行npm install
,并将其用作本部分的工作目录。
现在让我们将HeaderComponent.vue
和Logo.vue
复制到components/common
文件夹中。我们的 logo 标记将发生变化。之前它被包裹在router-link
组件内,并指向主页。我们将使用nuxt-link
组件,而不是使用router-link
:
//components/common/Logo.vue
<template>
**<nuxt-link to="/">**
<img class="logo" :src="src" alt="ProFitOro">
**</nuxt-link>**
</template>
请注意,我们将src
属性绑定到src
值。我们将从assets
文件夹获取我们的源。在 Nuxt 应用程序中,我们可以使用~
符号来指示应用程序的根目录。使用此符号实际上有助于使用相对路径。因此,logo 的源数据属性将如下所示:
// components/common/Logo.vue
<script>
export default {
data () {
return {
**src: require('~/assets/profitoro_logo.svg')**
}
}
}
</script>
我们的 logo 已经准备好了;现在是时候检查HeaderComponent
组件,并用nuxt-links
替换所有的路由链接。
打开刚刚复制的HeaderComponent.vue
组件,暂时删除从 Vuex 存储中使用的所有数据,只保留Logo
组件的import
:
//components/common/HeaderComponent.vue
<script>
import Logo from '~/components/common/Logo'
export default {
components: {
Logo
}
}
</script>
另外,删除标记内部所有数据的引用,只保留链接并用nuxt-link
组件替换它们。因此,我们的链接部分将如下所示:
//components/common/HeaderComponent.vue
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<**nuxt-link** class="nav-link" **to="/"**>Home **</nuxt-link>**
</li>
<li class="nav-item">
<**nuxt-link** class="nav-link" **to="settings"**>Settings **</nuxt-link>**
</li>
<li class="nav-item">
<**nuxt-link** class="nav-link" **to="statistics"**>Statistics **</nuxt-link>**
</li>
<li class="nav-item">
<**nuxt-link** class="nav-link" **to="workouts"**>Workouts **</nuxt-link>**
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<button class="btn btn-secondary" >Logout</button>
</form>
将HeaderComponent
导入到我们的页面(settings
,statistics
,pomodoro
和workouts
)中:
//pages/pomodoro.vue
<template>
<div class="container">
**<header-component></header-component>**
pomodoro
</div>
</template>
<script>
**import HeaderComponent from '~/components/common/HeaderComponent'**
export default {
components: {
**HeaderComponent**
}
}
</script>
<style scoped lang="scss">
@import "../assets/styles/main";
</style>
打开页面。检查我们的链接是否完全没有改变:
我们的链接看起来完全一样!
检查一下,即使我们的响应性仍然存在。如果调整页面大小,你会看到 Bootstrap 的菜单按钮:
菜单按钮仍然存在
当然,最重要的部分是路由工作!点击链接并检查页面是否变化。
你也注意到了当你从一个页面切换到另一个页面时发生了一个很好的过渡吗?
过渡是自动发生的,我们没有额外编写任何代码让它发生!
你可以在 chapter7/6/profitoro-nuxt
文件夹中找到到目前为止的最终代码。
练习 - 使菜单按钮工作
正如我们已经检查过的,我们的响应式菜单按钮仍然存在。但是,如果你点击它,什么也不会发生!这是因为这个按钮的行为是在 bootstrap.js
依赖中定义的,而我们还没有包含它。使用 nuxt.config.js
来包含必要的 JavaScript 文件,使菜单按钮再次变得伟大。
完成后,检查我在 chapter7/7/profitoro-nuxt
文件夹中的解决方案。特别要检查 nuxt.config.js
文件的 head
部分。
Nuxt.js 和 Vuex store
在这一部分不会有任何新东西 - Vuex store 可以以与以前相同的方式使用。啊,等等。在 Nuxt 应用程序内,我们必须导出返回 Vuex store 而不是实例本身的函数。在这方面查看官方文档:nuxtjs.org/guide/vuex-store
。所以,基本上我们不会使用以下代码:
export default new Vuex.Store({
state,
getters,
mutations: {
...
},
actions
})
相反,我们必须这样做:
**export default () => Vuex.Store**({
state,
getters,
mutations: {
...
},
actions
})
让我们也利用这个机会在一个单独的文件中初始化 Firebase 应用程序,并将其用作我们应用程序的单例。因此,将 firebaseApp
的初始化移动到其单独的 firebase/index.js
文件中,并用导入的 firebaseApp
实例替换所有 state.firebaseApp
的出现。
最后,但同样重要的是,不要忘记安装所需的 vuexfire
和 firebase
依赖项:
**npm install --save vuexfire firebase**
在 chapter7/8/profitoro-nuxt
文件夹中检查此部分的代码。特别要注意 store
和 firebase
文件夹。
Nuxt.js 中间件
你还记得我们如何不得不在 vue 路由实例中引入beforeEach
方法,以防止一些路由在用户未经身份验证时被渲染吗?Nuxt.js 有一个非常类似的机制。你只需要定义一个所谓的middleware
,在其中可以根据一些条件(例如,基于 Vuex 存储中的isAuthenticated
属性的值)重定向请求,然后告诉页面它们必须依赖于身份验证中间件。然后,每当尝试路由到给定页面时,中间件的函数将运行并执行其要求执行的任何操作。
让我们将这种类型的中间件添加到我们的 ProFitOro Nuxt 应用程序中。在middleware
文件夹内创建一个名为authentication.js
的文件,并添加以下内容:
//middleware/authenticated.js
export default function ({ store, redirect }) {
if (!store.getters.isAuthenticated) {
return redirect('/')
}
}
这段代码负责检查isAuthenticated
属性并在其为 false 或未定义时将用户重定向到主页。
现在,在设置、统计和锻炼页面中添加 middleware 属性:
<template>
<...>
</template>
<script>
//...
export default {
**middleware: 'authenticated'**,
//...
}
</script>
打开页面并尝试单击我们刚刚添加了 middleware 的页面的相应链接。它不会起作用!尝试删除一些页面的 middleware 代码,并检查路由是否正常工作。这不是很棒吗?
检查chapter7/9/profitoro-nuxt
文件夹中的此部分代码。检查middleware/index.js
文件和pages
文件夹中的 Vue 页面。
练习-完成所有!
嗯,为了使我们的 ProFitOro 成为 Nuxt.js 应用程序,我们已经做了很多工作,但我们的功能还不完全。我们仍然需要复制很多组件。所以,请做吧。现在,这只是一个很好的复制粘贴的问题。所以,请做,并确保我们的 ProFitOro 正常工作。
如果有疑问,请查看chapter7/10/profitoro-nuxt
文件夹。您可能会遇到尝试使用Enter键登录并发现自己成为匿名用户的问题。这是一个将在接下来的章节中修复的小问题。现在,请每次尝试使用有效凭据登录时,只需不要忘记点击登录按钮!
摘要
在本章中,我们使用不同的工具为我们的应用程序添加了基本路由。首先,我们学习了如何使用 vue-router 来实现路由功能,然后我们使用 Nuxt.js 模板来使用旧组件和样式构建全新的应用程序。我们使用了 Nuxt vue 提供的页面概念,以便以与vue-router
相同的路由功能,并以轻松和不显眼的方式将我们的 ProFitOro 应用程序转变为 Nuxt 应用程序。我们显著减少了代码量并学到了新东西。完全是赢家!
在本章中,我们还使用了 Bootstrap 的navbar
以一种漂亮和响应的方式显示我们的导航路由,并且学会了即使进行了最彻底的重构,当我们使用 Bootstrap 方法时,功能和响应性仍然与我们同在。再次取得了巨大成功!
我们的应用程序几乎完全功能,但是它仍然缺少主要功能 - 锻炼。目前,在番茄工作法间隔期间,我们正在展示一个硬编码的俯卧撑锻炼。
在阅读本书时,您是否正在使用 ProFitOro 应用程序?如果是的话,我想我会在街上认出你 - 你会因为做了这么多俯卧撑而有巨大的肌肉。
是时候在我们的应用程序中添加更多的锻炼了,不是吗?如果你还记得需求,锻炼是协作工作的主题。因此,我们将在下一章中添加这个功能。我们将使用 Firebase 的数据存储机制来存储锻炼的图像,实时数据库来存储锻炼的对象,Bootstrap 的卡片布局来显示不同的锻炼,以及基于 Bootstrap 的表单来向我们的应用程序添加新的锻炼。
第八章:让我们合作 - 使用 Firebase 数据存储和 Vue.js 添加新的锻炼
在上一章中,我们学习了如何使用vue-router
和Nuxt.js
为 Vue 应用程序添加一些基本导航。我们已经重新设计了我们的 ProFitOro 应用程序,将其转变为基于 Nuxt 的应用程序。现在我们的应用程序是功能性的,它具有身份验证机制,并且可以导航。但是,它仍然缺少最重要的功能之一 - 锻炼。在本章中,我们将实现锻炼管理页面。你还记得它在第二章 底层 - 教程解释中的要求吗?
这个页面应该允许用户查看数据库中现有的锻炼,选择或取消选择它们在番茄钟休息期间显示,对它们进行评分,甚至添加新的锻炼。我们不打算实现所有这些功能。但是,我们将实现足够的功能让你能够继续这个应用程序,并且以巨大的成功完成它的实现!因此,在本章中,我们将做以下工作:
-
为锻炼管理页面定义一个响应式布局,它将包括两个基本部分 - 所有锻炼的可搜索列表以及向列表中添加新锻炼的可能性
-
使用 Firebase 数据库和数据存储机制存储新的锻炼以及锻炼图片
-
使用 Bootstrap 模态框显示每个单独的锻炼
-
使用响应式布局和 fixed-bottom 类使我们的页脚更好看
使用 Bootstrap 类创建布局
在我们开始为锻炼页面实现布局之前,让我提醒你模拟看起来是什么样子的:
这是我们最初在模拟中定义的东西
我们将做一些略有不同的事情 - 类似于我们在设置页面所做的事情。让我们创建一个在移动设备上堆叠的两列布局。因此,这个模拟将适用于移动屏幕,但在桌面设备上会显示两列。
让我们在components/workouts
文件夹内添加两个组件 - WorkoutsComponent.vue
和NewWorkoutComponent.vue
。在这些新组件的模板中添加一些虚拟文本,并在workouts.vue
页面中定义我们的两列布局。你肯定记得,为了在小设备上堆叠列,并在其他设备上使用不同大小的列,我们必须使用col-*-<number>
表示法,其中*
表示设备的大小(sm
表示小,md
表示中,lg
表示大,等等),数字表示列的大小,范围从1
到12
。由于我们希望我们的布局在小设备上堆叠(这意味着列的大小应为12
),并且在中大型设备上是两个大小相等的列,我想出了以下布局定义:
// pages/workouts.vue
<template>
<div class="container">
<header-component></header-component>
<div class="row justify-content-center">
<div class="**col-sm-12 col-md-6 col-lg-6**">
**<workouts-component></workouts-component>**
</div>
<div class="**col-sm-12 col-md-6 col-lg-6**">
**<new-workout-component></new-workout-component>**
</div>
</div>
<footer-component></footer-component>
</div>
</template>
不要忘记将WorkoutsComponent.vue
和NewWorkoutComponent.vue
组件都导入workouts.vue
页面:
// pages/workouts.vue
<script>
//...
**import { NewWorkoutComponent, WorkoutComponent, WorkoutsComponent } from '~/components/workouts'**
export default {
components: {
/...
**NewWorkoutComponent**,
**WorkoutsComponent**
}
}
</script>
现在我们有了一个两列响应式布局:
用于训练管理页面的两列响应式布局
检查chapter8/1/profitoro
文件夹中的此实现的代码。特别注意components/workouts
文件夹的内容和workouts.vue
页面的内容。
使页脚漂亮
你不厌倦这个硬编码词“页脚”总是在我们的内容下面吗?
丑陋的飞行硬编码页脚总是粘在我们的内容上
让我们对此做些什么!如果你查看我们的模型,那里有三列:
-
版权信息的一列
-
另一个是当天的事实
-
最后是作者信息
你已经知道该怎么做了,对吧?再次强调,我们希望这些列在中大型设备上均匀分布,并在移动设备上堆叠。因此,我们的代码将如下所示:
// components/common/FooterComponent.vue
<template>
<div class="footer">
<div class="container row">
<div class="copyright **col-lg-4 col-md-4 col-sm-12**">Copyright</div>
<div class="fact **col-lg-4 col-md-4 col-sm-12**">Working out sharpens your memory</div>
<div class="author **col-lg-4 col-md-4 col-sm-12**"><span class="bold">Workout Lovers</span></div>
</div>
</div>
</template>
让我们暂时将“当天事实”部分硬编码。好吧,现在我们的页脚看起来好一些了。至少它不再只是“页脚”这个词在那里:
我们的页脚不再只是“页脚”这个词,但它仍然粘在主内容上
然而,它仍然固定在主要内容上,这并不是很好。如果我们的页脚固定在视口底部会很棒。这是一个常见的问题,在互联网上会找到很多关于这个问题的文章和解决方案:stackoverflow.com/questions/18915550/fix-footer-to-bottom-of-page
。幸运的是,我们正在使用 Bootstrap,它带有一系列用于粘性顶部、固定底部等的实用类。
提示
为了使您的页脚在 Bootstrap 中固定,只需向其添加这个类:fixed-bottom
一旦将这个类添加到您的页脚中,您将看到它如何固定在视口底部。尝试调整视口大小,将页面底部上下移动,您会发现我们的页脚会跟随在底部。
在chapter8/2/profitoro
文件夹中检查本节的代码。唯一的变化是HeaderComponent.vue
组件,它位于components/common
文件夹中。
使用 Firebase 实时数据库存储新的锻炼
在开始本节之前,请检查chapter8/3/profitoro
文件夹中的代码。Workouts
和NewWorkout
组件都填充有标记。
提示
不要忘记运行npm install
和npm run dev
!
它还没有起作用,但显示了一些东西:
带有一些内容的锻炼管理页面
在本节中,我们将向 Firebase 数据库中的锻炼资源添加锻炼对象。之后,我们最终可以学习如何使用 Firebase 数据存储机制存储图像。
首先,让我们像为统计和配置对象一样添加 Firebase 绑定。打开action.js
文件,找到bindFirebaseReferences
方法。在这里,我们应该为workouts
资源添加绑定。因此,这个方法现在包含三个绑定:
// state/actions.js
**bindFirebaseReferences**: firebaseAction(({state, commit, dispatch}, user) => {
let db = firebaseApp.database()
let configRef = db.ref(`/configuration/${user.uid}`)
let statisticsRef = db.ref(`/statistics/${user.uid}`)
**let workoutsRef = db.ref('/workouts')**
dispatch('bindFirebaseReference', {reference: configRef, toBind: 'config'}).then(() => {
commit('setConfigRef', configRef)
})
dispatch('bindFirebaseReference', {reference: statisticsRef, toBind: 'statistics'}).then(() => {
commit('setStatisticsRef', statisticsRef)
})
**dispatch('bindFirebaseReference', {reference: workoutsRef, toBind: 'workouts'}).then(() => {**
**commit('setWorkoutsRef', workoutsRef)**
**})**
})
一旦应用程序卸载,我们还应该解除它们的绑定:
//state/actions.js
unbindFirebaseReferences: firebaseAction(({unbindFirebaseRef, commit}) => {
commit('setConfigRef', null)
commit('setStatisticsRef', null)
**commit('setWorkoutsRef', null)**
try {
unbindFirebaseRef('config')
unbindFirebaseRef('statistics')
**unbindFirebaseRef('workouts')**
} catch (error) {
return
}
})
让我们还向我们的状态添加workoutsRef
和workouts
属性。最后但并非最不重要的是,不要忘记实现名为setWorkoutsRef
的 mutation:
// state/mutations.js
setWorkoutsRef (state, value) {
state.workoutsRef = value
}
现在,有了存储在我们状态中的workoutsRef
,我们可以实现将其更新为新创建的锻炼的操作。之后,我们将能够在NewWorkout
组件中使用此操作并填充我们的锻炼数据库。
查看 Firebase 关于读取和写入实时数据库的文档:firebase.google.com/docs/database/web/read-and-write
。向下滚动,直到找到“新帖子创建”示例:
Firebase 数据库文档中的新帖子创建示例
你不觉得这个案例和我们的非常相似吗?用户添加的每个锻炼都有其名称、描述和图片(或者甚至多张图片)。锻炼也属于创建它们的用户。所以,也许我们可以做一些非常类似的事情。如果我们决定实现每个用户删除他们的锻炼的可能性,为user-workouts
创建一个资源可能会很有用。在复制此代码之前,让我们就锻炼对象数据结构达成一致意见。它应该包含什么?由于它来自NewWorkout
组件,它将已经带有锻炼的名称、描述和图片 URL。我们应该在action
内丰富它吗?可能,我们应该添加添加它的用户的名称和 UID,创建日期和评分属性。这应该足够了。所以,我们的锻炼数据结构将如下所示:
{
**name**: 'string',
**description**: 'string',
**pictures**: ['string'],
**username**: 'string',
**uid**: 'string',
**rate**: 'number',
**date**: 'timestamp'
}
name
、description
、username
和uid
属性都是字符串。pictures
属性应该是 URL 字符串的数组,rating
应该是一个数字,让我们以时间戳的形式存储我们的date
属性。
注意
很好,我们正在实现前端和后端部分,所以我们在我们之间达成了数据架构的一致。如果你曾经在一个有前端和后端开发人员的团队中工作过,请不要忘记在任何实施之前达成数据架构的一致!
因此,我们知道描述、名称和图片 URL 应该在NewWorkout
组件内填充。因此,让我们在我们的action
方法内填充其他所有内容。最后,它看起来会非常类似于 Firebase 示例:
// store/actions.js
**createNewWorkout** ({commit, state}, workout) {
if (!workout) {
return
}
**workout.username = state.user.displayName**
**workout.uid = state.user.uid**
**workout.date = Date.now()**
**workout.rate = 0**
// Get a key for a new Workout.
let newWorkoutKey = state.workoutsRef.push().key
// Write the new post's data simultaneously in the posts list and the user's post list.
let updates = {}
updates['/**workouts**/' + newWorkoutKey] = workout
updates['/**user-workouts**/' + state.user.uid + '/' + newWorkoutKey] = workout
return firebaseApp.database().ref().update(updates)
},
再次注意,我们正在引入一个名为user-workouts
的新资源。我们可以以与统计和配置用户数据相同的方式将此资源绑定到我们的状态。如果我们决定实现删除用户资源,这可能会很方便。
现在,让我们转到我们的NewWorkout
组件。在这里,我们只需要将一些 Vue 模型绑定到相应的输入和单击事件绑定到提交按钮。应用按钮上的单击事件应绑定到createNewWorkout
动作,同时调用相应的数据。暂时不要担心pictures
,我们将在下一节中处理它们。
此时,我们可以用状态训练对象替换Workouts
组件中的硬编码训练数组:
//Components/Workouts.vue
// ...
<script>
import {mapState} from 'vuex'
export default {
**computed: {**
**...mapState(['workouts'])**
**}**
}
</script>
//...
检查您新创建的训练立即出现在训练部分的方式!
检查chapter8/4/profitoro
文件夹中此部分的最终代码。注意存储文件(actions.js
,mutations.js
)以及components/workouts
文件夹中的NewWorkoutComponent
和WorkoutsComponent
组件。
使用 Firebase 数据存储存储图像
Firebase 云存储允许您上传和检索不同的内容(文件、视频、图像等)。同样,Firebase 提供了一种访问和管理数据库的方式,您可以访问和管理存储桶。您可以上传 Blob、Base64 字符串、文件对象等。
首先,您应告诉您的 Firebase 应用程序,您将使用 Google 云存储。因此,您需要向应用程序配置对象添加storageBucket
属性。在 Google Firebase 控制台上检查应用程序的设置,并将storageBucket
引用复制到firebase/index.js
文件中:
// Initialize Firebase
import firebase from 'firebase'
//...
let config = {
apiKey: 'YOUR_API_KEY',
databaseURL: 'https://profitoro-ad0f0.firebaseio.com',
authDomain: 'profitoro-ad0f0.firebaseapp.com',
**storageBucket: 'gs://profitoro-ad0f0.appspot.com'**
}
//...
现在您的 Firebase 应用程序知道要使用哪个存储桶。让我们还打开 Firebase 控制台的数据存储选项卡,并为我们的训练图像添加一个文件夹。让我们称之为…训练:
在 Firebase 数据存储选项卡中创建一个名为“workouts”的文件夹
现在一切准备就绪,可以开始使用我们的云存储桶。
首先,我们必须获取对我们的训练文件夹的引用,以便我们可以修改它。查看有关存储桶引用创建的 Firebase API 文档:firebase.google.com/docs/storage/web/create-reference
。在我们的情况下,引用将如下所示:
firebaseApp.storage().ref().child('workouts')
我们应该在哪里使用它?在存储训练之前,我们应该存储图片文件,获取它们的云 URL,并将这些 URL 分配给训练的pictures
属性。因此,这是我们的计划:
-
创建一个上传文件并返回这些文件的下载 URL 的方法
-
在调用
createNewWorkout
动作之前使用这个方法来为 workout 对象的 pictures 属性分配 URL
让我们创建一个上传文件并返回其downloadURL
的方法。查看 Firebase 文档,了解如何使用其 API 上传文件:firebase.google.com/docs/storage/web/upload-files
。看一下从 Blob 或文件上传部分。你会看到我们应该在云存储引用上使用"put
"方法,提供一个文件对象。这将是一个返回快照对象的 promise:
var file = ... // use the Blob or File API
ref.put(file).then(function(snapshot) {
console.log('Uploaded a blob or file!');
});
这个snapshot
对象是什么?这是存储在云上的文件的表示。它包含了很多信息,但对我们来说最重要的是它的downloadURL
属性。因此,我们的 promise 看起来会和示例 promise 非常相似,但它将返回snapshot.downloadURL
。因此,打开actions.js
文件,创建一个名为uploadImage
的新方法。这个方法将接收一个文件对象,在我们的workout
云文件夹引用上创建一个子引用,然后put
一个文件并返回downloadURL
。因此,它看起来会像这样:
function _uploadImage (file) {
let ref = firebaseApp.storage().ref().child('workouts')
return **ref.child(file.name)**.put(file).then(snapshot => {
**return snapshot.downloadURL**
})
}
你难道没有看到一个小问题吗?如果两个不同的用户提交了不同的图片,但使用了相同的名称,那么这些图片将会互相覆盖。作为一个小练习,想想避免这个问题的方法。
提示
提示:看一下这个 npm 包:
因此,我们有一个上传文件并返回其downloadURL
的 promise。然而,这还不是我们最终的动作。我们最终的action
方法应该上传一个数组的文件,因为这是我们从多文件输入中得到的 - 一组文件对象。因此,我们最终的 promise 将只返回所有 promise 的结果,它看起来会像下面这样简单:
uploadImages ({state}, files) {
return **Promise.all**(files.map(**_uploadImage**))
}
现在可以在NewWorkout
组件中调用这个动作,然后再调用createNewWorkout
动作。
首先,我们需要将pictures
属性绑定到文件输入元素。显而易见的选择是使用v-model
指令将属性pictures
绑定到输入上:
<input **v-model="pictures"** type="file" multiple class="form-control-file" id="imageFile">
尽管如此显而易见吗?v-model
指令确定了双向数据绑定,但我们如何设置数据呢?文件输入的数据要么是FileObject
,要么是FileList
。我们该如何设置它呢?似乎对这个元素应用双向数据绑定是没有意义的。
注意
实际上,你不能将响应式数据绑定到文件输入,但是你可以在 change 事件中设置你的数据:
forum.vuejs.org/t/vuejs2-file-input/633/2
因此,我们必须监听change
事件,并在每次更改时设置我们的数据。让我们将这个事件绑定到filesChange
方法:
// NewWorkoutComponent.vue
<input @change="**filesChange($event.target.files)**" type="file" multiple class="form-control-file" id="imageFile">
现在让我们创建这个方法,只需将this.pictures
分配给我们接收到的参数。好吧,不是只是分配,因为我们接收到的是一个FileList
对象,它并不完全是一个可以迭代的数组。因此,我们需要将它转换成一个简单的File
对象数组。
提示
我们可以使用 ES6 扩展运算符来做到这一点:
filesArray = [...fileListObject]
因此,我们的filesChange
方法将如下所示:
// NewWorkoutComponent.vue
export default {
methods: {
//...
**filesChange (files) {**
**this.pictures = [...files]**
**}**
//...
}
}
现在我们终于可以更新我们的onCreateNew
方法了。首先,它应该分发uploadImages
动作,并在承诺解决后分发createNewWorkout
动作,将承诺的结果分配给pictures
数组。现在这个方法将如下所示:
// NewWorkoutComponent.vue
onCreateNew (ev) {
ev.preventDefault()
ev.stopPropagation()
**this.uploadImages(this.pictures).then(picUrls => {**
this.createNewWorkout({
name: this.name,
description: this.description,
pictures: **picUrls**
})
this.reset()
})
}
不要忘记导入uploadImages
动作。另外,创建一个reset
方法,将所有数据重置为初始状态。
创建一些带有图片的锻炼,并享受结果!
让我们搜索!
所以现在我们可以创建锻炼,并看到它们显示在锻炼列表中。然而,我们有这个不错的搜索输入,但它什么也没做:(. 尽管如此,我们正在使用 Vue.js,所以实现这个搜索真的很容易。我们只需要创建一个searchTerm
数据属性,并将其绑定到搜索输入,然后通过这个searchTerm
过滤锻炼数组。因此,我将添加计算属性,让我们称之为workoutsToDisplay
,这个属性将表示一个通过名称、描述和用户名属性过滤的锻炼属性(我们从 Vuex 存储的状态中导入的属性)。因此,它将给我们提供通过所有这些术语进行搜索的可能性:
// WorkoutsComponent.vue
<script>
//...
export default {
//...
computed: {
...mapState(['workouts']),
**workoutsToDisplay () {**
**return this.workouts.filter(workout => {**
**let name = workout.name.toLowerCase()**
**let description = workout.description.toLowerCase()**
**let username = workout.username.toLowerCase()**
**let term = this.searchTerm.toLowerCase()**
**return name.indexOf(term) >= 0 || description.indexOf(term) >= 0 || username.indexOf(term) >= 0**
**})**
**}**
}
//...
}
</script>
不要忘记将searchTerm
属性添加到组件的数据中,并将其绑定到搜索输入元素:
<template>
<div>
<div class="form-group">
<input **v-model="searchTerm"** class="input" type="search" placeholder="Search for workouts">
</div>
</div>
</template>
<script>
//...
export default {
data () {
return {
name: '',
username: '',
datecreated: '',
description: '',
pictures: [],
rate: 0,
**searchTerm**: ''
}
}
}
</script>
当然,我们现在应该遍历workoutsToDisplay
数组来显示锻炼卡片,而不是遍历锻炼数组。因此,只需稍微编辑卡片div
的v-for
指令:
v-for="workout in **workoutsToDisplay**"
打开页面并尝试搜索!如果我按用户名搜索,只会显示由该用户创建的锻炼:
有道理,因为我创建了所有现有的锻炼直到现在
如果我按锻炼的名称搜索,比如俯卧撑,只会出现这个锻炼:
按锻炼名称搜索
我们快要完成了!现在我们唯一要做的就是在番茄钟的休息时段显示从锻炼列表中随机选择的锻炼,而不是硬编码的数据。尝试在pomodoro.vue
页面中自己做到这一点。
现在您可以创建新的锻炼,并且它们将立即出现在锻炼部分。它们还会在我们的番茄钟休息期间出现在主页上。
干得好!检查chapter8/5/profitoro
文件夹中此部分的代码。特别注意store/actions.js
文件中的新操作以及components/workouts
文件夹中的Workouts
和NewWorkout
组件。查看随机锻炼是如何被选择并显示在pomodoro.vue
页面中的。
使用 Bootstrap 模态框显示每个锻炼
现在我们可以在页面上看到所有现有的锻炼,这很棒。然而,我们的用户真的很想详细了解每个锻炼-查看锻炼的描述,对其进行评分,查看谁创建了它们以及何时创建的等等。在小的“卡片”元素中放置所有这些信息是不可想象的,因此我们需要一种放大每个元素以便能够查看其详细信息的方法。Bootstrap 模态框是提供此功能的绝佳工具。查看 Bootstrap 文档有关模态 API 的信息:v4-alpha.getbootstrap.com/components/modal/
。
注意
请注意,Bootstrap 4 在撰写本文时处于 alpha 阶段,这就是为什么在某个时候这个链接可能不再有效,所以只需在官方 Bootstrap 网站上搜索相关信息即可。
基本上,我们需要一个触发模态的元素和模态标记本身。在我们的情况下,每个小锻炼卡都应该被用作模态触发器;WorkoutComponent
将是我们的模态组件。因此,只需在 Workouts 组件内的card
元素中添加data-toggle
和data-target
属性:
// WorkoutsComponent.vue
<div class="card-columns">
<div data-toggle="modal" data-target="#workoutModal" v-for="workout in workouts" class="card">
<img class="card-img-top img-fluid" :src="workout.pictures && workout.pictures.length && workout.pictures[0]" :alt="workout.name">
<div class="card-block">
<p class="card-text">{{ workout.name }}</p>
</div>
</div>
</div>
现在让我们来处理WorkoutComponent
组件。假设它将接收以下属性:
-
名称
-
描述
-
用户名
-
创建日期
-
费率
-
图片
因此,我们可以为我们的模态构建一个非常简单的标记,类似于这样:
<template>
<div class="modal fade" id="**workoutModal**" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">**{{ name }}**</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="text-center">
<img **:src="pictures && pictures.length && pictures[0]"** class="img-fluid" :alt="name">
</div>
<p>**{{ description }}**</p>
</div>
<div class="modal-footer">
<p>Created on **{{ datecreated }}** by **{{ username }}**</p>
</div>
</div>
</div>
</div>
</template>
请记住,这个模态需要具有与其从切换元素进行定位的完全相同的 ID 属性。
不要忘记在props
属性下指定所需的属性:
// WorkoutComponent.vue
<script>
export default {
**props: ['name', 'description', 'username', 'datecreated', 'rate', 'pictures']**
}
</script>
现在这个组件可以被导入到 Workouts 组件中并在那里使用:
// WorkoutsComponent.vue
<template>
<div>
<...>
<div class="card-columns">
<...>
</div>
**<workout-component**
**:name="name"**
**:description="description"**
**:username="username"**
**:datecreated="datecreated"**
**:pictures="pictures"**
**:rate="rate">**
**</workout-component>**
</div>
</template>
如果你现在点击一些小卡片,空的模态将会打开:
模态有效!但是它是空的
我们肯定还应该做一些事情,以便所选元素的数据传播到组件的数据中。让我们添加一个方法来执行这项工作,并将其绑定到card
元素的click
事件上:
// WorkoutsComponent.vue
<div data-toggle="modal" data-target="#workoutModal" v-for="workout in workouts" class="card" **@click="onChosenWorkout(workout)"**>
该方法将只是将锻炼的数据复制到相应组件的数据中:
// WorkoutsComponent.vue – **methods** section
**onChosenWorkout** (workout) {
this.name = workout.name
this.description = workout.description
this.username = workout.username
this.datecreated = workout.date
this.rate = workout.rate
this.pictures = workout.pictures
}
现在看起来好多了!
数据绑定确实有效!
看起来不错,所有数据都在这里,但还不完美。想想我们如何能改进它。
练习
使模态底部显示的日期可读。以这样的方式做,使底部看起来像这样:
锻炼模态的底部,带有可读的数据
尝试使用现有工具,而不是重新发明轮子。
提示
想想 moment.js 库:
自己检查一下,直到这一刻的最终代码在chapter8/6/profitoro
文件夹中。注意components/workout
文件夹中的Workouts
和Workout
组件。
是时候应用一些样式了
我们的应用程序现在已经完全功能,可以立即使用。当然,它还不完美。它缺乏验证和一些功能,一些要求尚未实现,最重要的是...它缺乏美感!它全是灰色,没有风格...我们是人类,我们喜欢美丽的东西,不是吗?每个人都以自己的方式实现风格。我强烈建议,如果你想使用这个应用程序,请找到自己的风格和主题,并实现它并与我分享。我会很乐意看到它。
至于我,因为我不是设计师,我请我的好朋友 Vanessa(www.behance.net/MeegsyWeegsy
)为 ProFitOro 应用程序设计一个漂亮的设计。她做得很好!因为我忙着写这本书,所以我没有时间实现 Vanessa 的设计,因此我请我的好朋友 Filipe(github.com/fil090302
)帮助我。Filipe 也做得很好!一切看起来都和 Vanessa 实现的一样。我们使用了scss
,所以你一定很熟悉,因为我们在这个应用程序中已经在使用它作为预处理器。
您可以重用现有的样式来覆盖一些变量,以创建自己的主题。请在chapter8/7/profitoro
文件夹中检查最终代码。所有样式都位于assets/styles
目录中。它具有以下结构:
目录结构
至于最终的外观,就是这样的。
这是带有 Pomodoro 计时器的主页面:
包含 Pomodoro 计时器的主页面
这是设置页面的样子:
设置页面的外观和感觉
最后,这就是 Workouts 页面的样子:
Workouts 页面的外观和感觉
你仍然需要实现统计页面-现在,它只显示完成的 Pomodoro 的总数:
统计页面尚未完全完成,只显示完成的 Pomodoros 的总数
还有一些工作要做,但你不觉得我们迄今为止做得很好吗?我们不仅拥有一个完全可配置的番茄钟计时器,还可以在工作日进行小型锻炼。这是多么棒啊!
总结
在本章中,我们终于实现了锻炼管理页面。现在我们可以看到数据库中存储的所有锻炼,并创建我们自己的锻炼。我们学会了如何使用 Google Firebase 数据存储系统和 API 来存储静态文件,并且能够将新创建的锻炼存储在 Firebase 实时数据库中。我们还学会了如何使用 Bootstrap 模态框,并将其用于在漂亮的模态弹出窗口中显示每个锻炼。
在下一章中,我们将进行每个软件实施过程中最重要的工作 - 我们将测试迄今为止所做的工作。我们将使用 Jest (facebook.github.io/jest/
) 来测试我们的应用程序。之后,我们将最终部署我们的应用程序并定义未来的工作。你准备好测试你的工作了吗?那就翻开下一页吧!
第九章:测试测试和测试
在上一章中,我们实现了锻炼管理页面。我们学习了如何使用 Google Firebase 数据存储机制来存储静态文件,并且再次使用了实时数据库来存储锻炼对象。我们使用 Bootstrap 为锻炼管理页面构建了一个响应式布局,并学习了如何使用 Bootstrap 的模态组件在一个漂亮的弹出窗口中显示每个单独的锻炼。现在我们有一个完全负责的应用程序。多亏了 Bootstrap,我们不需要实现任何特殊的东西来获得一个漂亮的移动表示。在移动屏幕上添加新的锻炼的样子如下:
在移动屏幕上添加新的锻炼
这是我们的模态在移动设备上的样子:
在移动设备上显示的锻炼模态
现在是测试我们的应用程序的时候了。我们将使用 Jest(facebook.github.io/jest/
)来构建单元测试和运行快照测试。在本章中,我们将做以下事情:
-
学习如何配置我们的 Vue.js 应用程序与 Jest 一起工作
-
使用 Jest 断言测试 Vuex 存储
-
学习如何使用
jest.mock
和jest.fn
方法模拟复杂对象 -
学习如何为 Vue 组件实现快照测试
为什么测试很重要?
我们的 ProFitOro 应用程序运行得很好,是吗?我们在浏览器中打开了它很多次,检查了所有实现的功能,所以它只是工作,对吧?是的,这是真的。现在去你的设置页面,尝试将计时器的值更改为一些奇怪的值。尝试使用负值,尝试使用巨大的值,尝试使用字符串,尝试使用空值……你认为这可以称为良好的用户体验吗?
你不会想要在这么多分钟内工作,对吧?
你尝试过创建一个奇怪的锻炼吗?你尝试过在创建时输入一个巨大的锻炼名称并看看它是如何显示的吗?有成千上万种边缘情况,所有这些都应该仔细测试。我们希望我们的应用程序是可维护的、可靠的,并且提供令人惊叹的用户体验。
什么是 Jest?
你知道 Facebook 的人永远不会厌倦创造新工具。React、redux、react-native 以及所有这些响应式家族对他们来说还不够,他们创建了一个真正强大、易于使用的测试框架,叫做 Jest:facebook.github.io/jest/
。Jest 非常酷,因为它足够自包含,让你不必分心于繁琐的配置或寻找异步测试插件、模拟库或伪计时器来与你喜欢的框架一起使用。Jest 是一个多合一的工具,虽然非常轻量级。此外,在每次运行时,它只运行自上次测试运行以来发生变化的测试,这非常优雅和快速!
最初为测试 React 应用程序而创建,Jest 被证明适用于其他用途,包括 Vue.js 应用程序。
查看罗曼·库巴在 2017 年 6 月波兰 Vue.js 大会上的精彩演讲(youtu.be/pqp0PsPBO_0
),他在其中简要解释了如何使用 Jest 测试 Vue 组件。
我们的应用不仅仅是一个 Vue 应用程序,它是一个使用了 Vuex 存储和 Firebase 的 Nuxt 应用程序。所有这些依赖项使得测试变得有点困难,因为我们必须模拟所有这些东西,还有 Nuxt 应用程序本身的特殊性。然而,这是可能的,一切设置好之后,编写测试的乐趣是巨大的!让我们开始吧!
开始使用 Jest
让我们从测试一个小的求和函数开始,检查它是否正确地对两个数字求和。
首先当然是安装 Jest:
**npm install jest**
创建一个名为test
的目录,并添加一个名为sum.js
的文件,内容如下:
// test/sum.js
export default function sum (a, b) {
return a + b
}
现在为这个函数添加一个测试规范文件:
// sum.spec.js
import sum from './sum'
describe('sum', () => {
**it('create sum of 2 numbers', () => {**
**expect(sum(15, 8)).toBe(23)**
**})**
})
我们需要一个命令来运行测试。在package.json
文件中添加一个"test"
条目,它将调用一个jest
命令:
// package.json
"scripts": {
//...
**"test": "jest"**
}
现在如果你运行npm test
,你会看到一些错误:
使用 Jest 运行测试时的测试输出中的错误
这是因为我们的 Jest 不知道我们在使用ES6!所以,我们需要添加babel-jest
依赖项:
**npm install babel-jest --save-dev**
安装完babel-jest之后,我们需要添加一个.babelrc
文件,内容如下:
// .babelrc
{
"presets": ["es2015"]
}
你是否对 IDE 关于describe
、it
和其他未被识别的全局变量的警告感到烦恼?只需在你的.eslintrc.js
文件中添加一个jest: true
条目:
// .eslintrc.js
module.exports = {
root: true,
parser: 'babel-eslint',
env: {
browser: true,
node: true,
**jest: true**
},
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
rules: {},
globals: {}
}
现在如果你运行npm test
,测试通过了!
恭喜!你刚刚设置并运行了你的第一个 Jest 测试!
覆盖率
单元测试有助于确保它们检查的代码片段(单元)对于任何可能和不可能的输入都能正常工作。每个编写的单元测试都覆盖了相应的代码片段,就像一条毯子一样,保护这段代码免受未来的故障,并使我们对代码的功能和可维护性感到舒适。代码覆盖有不同的类型:语句覆盖、行覆盖、分支覆盖等等。代码覆盖越多,代码就越稳定,我们就越舒适。这就是为什么在编写单元测试时,每次运行时检查代码覆盖率非常重要。使用 Jest 很容易检查代码覆盖率。你不需要安装任何外部工具或编写额外的配置。只需执行带有覆盖率标志的测试命令:
npm test -- --coverage
你会神奇地看到这个美丽的覆盖率输出:
使用覆盖率运行 Jest 测试
像魔术一样,对吧?
在chapter9/1/profitoro
目录中找到代码。别忘了在其上运行npm install
。
测试实用函数
现在让我们测试我们的代码!让我们从 utils 开始。创建一个名为utils.spec.js
的文件,并导入leftPad
函数:
import { leftPad } from '~/utils/utils'
再看看这个函数:
// utils/utils.js
export const leftPad = value => {
if (('' + value).length > 1) {
return value
}
return '0' + value
}
如果输入字符串的长度大于1
,则此函数应返回输入字符串。如果字符串的长度为1
,则应返回带有前导0
的字符串。
测试起来似乎很容易,对吧?我们将编写两个测试用例:
// test/utils.spec.js
describe('utils', () => {
describe('leftPad', () => {
it('should return the string itself if its length is more than 1', () => {
**expect(leftPad('01')).toEqual('01')**
})
it('should add a 0 from the left if the entry string is of the length of 1', () => {
**expect(leftPad('0')).toEqual('00')**
})
})
})
啊...如果你运行这个测试,你会得到一个错误:
当然,可怜的 Jest,并不知道我们在 Nuxt 应用程序中使用的别名。对于它来说,~
符号什么都不等于!幸运的是,这很容易解决。只需在package.json
文件中添加jest
条目,并在其中添加一个名称映射条目:
// package.json
"jest": {
"moduleNameMapper": {
**"^~(.*)$": "<rootDir>/$1"**
}
}
现在 Jest 将知道以~
开头的所有内容都应映射到根目录。如果你现在运行npm test -- --coverage
,你会看到测试通过了!
映射根目录别名后,测试可以正常运行
然而,代码覆盖率确实很低。这是因为我们的工具中还有另一个要测试的函数。检查utils.js
文件。你能看到numberOfSecondsFromNow
方法吗?它也需要一些测试覆盖率。它计算了从给定输入时间到现在经过的时间。我们应该如何处理这个Date.now
?我们无法预测测试结果,因为我们无法保证测试运行时的现在时刻与我们检查时的时刻相同。每一毫秒都很重要。简单!我们应该模拟Date.now
对象!
使用 Jest 进行模拟
事实证明,即使是看似不可能的事情(停止时间)在 Jest 中也是可能的。使用jest.fn()
函数很容易模拟Date.now
对象。
查看关于使用 Jest 进行模拟的文档:
facebook.github.io/jest/docs/en/snapshot-testing.html#tests-should-be-deterministic
我们可以通过调用Date.now = jest.fn(() => 2000)
来模拟Date.now
函数。
现在我们可以轻松测试'numberOfSecondsFromNow'
函数:
// test/utils.spec.js
import { leftPad, numberOfSecondsFromNow } from '~/utils/utils'
//...
describe(**'numberOfSecondsFromNow'**, () => {
it('should return the exact number of seconds from now', () => {
**Date.now = jest.fn(() => 2000)**
expect(numberOfSecondsFromNow(1000)).toEqual(1)
})
})
现在覆盖率更好了,但如果我们能覆盖我们有趣的beep
函数,那就更完美了。我们应该在其中测试什么?让我们尝试测试一下,当调用beep
函数时,Audio.play
方法被调用。模拟函数有一个特殊的属性叫做mock,其中包含了关于这个函数的所有信息——已经对它执行的调用次数,传递给它的信息等等。因此,我们可以像这样模拟Audio.prototype.play
方法:
let mockAudioPlay = jest.fn()
Audio.prototype.play = mockAudioPlay
在调用 beep 方法后,我们可以像这样检查模拟上执行的调用次数:
expect(mockAudioPlay.mock.calls.length).toEqual(1)
或者我们可以断言模拟已经被调用了,就像这样:
expect(mockAudioPlay).toHaveBeenCalled()
整个测试可能看起来像下面这样:
describe('beep', () => {
it('should call the Audio.play functuon', () => {
let mockAudioPlay = jest.fn()
Audio.prototype.play = mockAudioPlay
beep()
expect(mockAudioPlay.mock.calls.length).toEqual(1)
expect(mockAudioPlay).toHaveBeenCalled()
})
})
为了避免由于模拟原生函数而产生的副作用,我们可能希望在测试后重置我们的模拟:
it('should call the Audio.play functuon', () => {
// ...
expect(mockAudioPlay).toHaveBeenCalled()
**mockAudioPlay.mockReset()**
})
在这方面查看 Jest 文档:facebook.github.io/jest/docs/en/mock-function-api.html#mockfnmockreset
。
或者,您可以配置 Jest 设置,以便在每次测试后自动重置模拟。为此,在package.json
文件中的 Jestconfig
对象中添加clearMocks
属性:
//package.json
"jest": {
**"clearMocks": true,**
"moduleNameMapper": {
"^~(.*)$": "<rootDir>/$1"
}
},
耶!测试通过了。检查一下覆盖率。看起来相当不错;然而,分支覆盖率仍然不完美:
utils.js 文件的分支覆盖率仅为 75%
为什么会发生这种情况?首先,检查未覆盖的行
列。它显示了测试未覆盖的行。这是numberOfSecondsFromNow
方法的第22
行:
export const numberOfSecondsFromNow = startTime => {
const SECOND = 1000
if (!startTime) {
**return 0**
}
return Math.floor((Date.now() - startTime) / SECOND)
}
作为替代方案,您可以检查项目目录中的coverage
文件夹,并在浏览器中打开lcov-report/index.html
文件,以更直观地了解发生了什么:
代码覆盖率 HTML 以一种美观的方式显示了覆盖和未覆盖的行
在这里,您可以清楚地看到第22
行标记为红色,这意味着它没有被测试覆盖。好吧,让我们来覆盖它!只需添加一个新的测试,覆盖startTime
属性未传递给此方法的情况,并确保它返回0
:
// test/utils.js
describe(**'numberOfSecondsFromNow'**, () => {
**it('should return 0 if no parameter is passed', () => {**
**expect(numberOfSecondsFromNow()).toEqual(0)**
**})**
it('should return the exact number of seconds from now', () => {
Date.now = jest.fn(() => 2000)
expect(numberOfSecondsFromNow(1000)).toEqual(1)
})
})
现在带着覆盖标志运行测试。天啊!这不是很棒吗?
100%的代码覆盖率,是不是很棒?
本节的最终代码可以在chapter9/2/profitoro
文件夹中找到。
使用 Jest 测试 Vuex 存储
现在让我们尝试测试我们的 Vuex 存储。我们要测试的存储最关键的部分是我们的操作和突变,因为它们实际上可以改变存储的状态。让我们从突变开始。在test
文件夹中创建mutations.spec.js
文件并导入mutations.js
:
// test/mutations.spec.js
import mutations from '~/store/mutations'
我们已经准备好为我们的突变函数编写单元测试。
测试突变
突变是非常简单的函数,它接收一个状态对象,并将其属性设置为给定值。因此,测试突变非常简单——我们只需模拟状态对象,并将其传递给我们想要测试的突变,以及我们想要设置的值。最后,我们必须检查该值是否已实际设置。例如,让我们测试setWorkingPomodoro
突变。这是我们的突变的样子:
// store/mutations.js
setWorkingPomodoro (state, workingPomodoro) {
state.config.workingPomodoro = workingPomodoro
}
在我们的测试中,我们需要为状态对象创建一个模拟。它不需要代表完整的状态;它至少需要模拟状态的config
对象的workingPomodoro
属性。然后我们将调用突变,传递给它我们的模拟状态和workingPomodoro
的新值,并断言这个值已经应用到我们的模拟中。因此,这些是步骤:
-
为状态对象创建一个模拟:
let state = {config: {workingPomodoro: 1}}
-
使用新值调用突变:
mutations.setWorkingPomodoro(state, 30)
-
断言该值已设置为模拟对象:
expect(state.config).toEqual({workingPomodoro: 30})
这个测试的完整代码看起来如下:
// test/mutations.spec.js
import mutations from '~/store/mutations'
describe('mutations', () => {
describe('setWorkingPomodoro', () => {
it('should set the workingPomodoro property to 30', () => {
let state = {config: {workingPomodoro: 1}}
mutations.setWorkingPomodoro(state, 30)
expect(state.config).toEqual({workingPomodoro: 30})
})
})
})
相同的机制应该被应用于测试其余的变化。继续并完成它们吧!
使用 Jest 进行异步测试——测试动作
让我们继续测试更复杂的东西——我们的动作!我们的动作大多是异步的,并且它们在内部使用复杂的 Firebase 应用程序对象。这使得它们非常具有挑战性,但我们确实喜欢挑战,不是吗?让我们来看看actions.js
文件中的第一个动作。它是uploadImages
动作,看起来是这样的:
uploadImages ({state}, files) {
return Promise.all(files.map(this._uploadImage))
}
我们可能在这里测试什么呢?例如,我们可以测试_uploadImage
函数被调用的次数与传递的图像数组的大小完全相同。为此,我们必须模拟_uploadImage
方法。为了做到这一点,让我们在actions
中也导出它:
// store/actions.js
function _uploadImage (file) {
//...
}
export default {
**_uploadImage**,
uploadImages ({state}, files) {
return Promise.all(files.map(**this._uploadImage**))
}
//...
}
现在我们可以模拟这个方法并检查mock
被调用的次数。模拟本身非常简单;我们只需要将actions._uploadImage
分配给jest.fn()
:
// test/actions.spec.js
it('should call method _uploadImage 3 times', () => {
**actions._uploadImage = jest.fn()**
})
从现在开始,我们的actions._uploadImage
具有一个特殊的魔法属性叫做mock
,我们已经谈论过了。这个对象让我们有机会访问对_uploadImage
方法的调用次数:
actions._uploadImage.mock.calls
因此,要断言调用次数为三,我们只需运行以下断言:
expect(**actions._uploadImage.mock.calls.length**).toEqual(**3**)
提示
在这里查看有关在 Jest 中模拟函数的完整文档:
facebook.github.io/jest/docs/mock-functions.html#content
很好,但我们应该在哪里调用这个期望呢?uploadImages
函数是异步的;它返回一个 promise。不知何故,我们可以窥视未来并监听 promise 的解析,然后在那里调用我们的断言。我们应该定义一些回调,并在 promise 解析后调用它们吗?不,不需要。只需调用您的函数,并在then
回调中运行断言。因此,我们的测试将如下所示:
// test/actions.spec.js
import actions from '~/store/actions'
describe('actions', () => {
describe('uploadImages', () => {
it('should call method _uploadImage 3 times', () => {
actions._uploadImage = jest.fn()
actions.uploadImages({}, [1, 2, 3])**.then(() => {**
**expect(actions._uploadImage.mock.calls.length).toEqual(3)**
**})**
})
})
})
它就是这样工作的!
现在让我们创建一个更复杂的模拟——针对我们的firebaseApp
。我们如何决定模拟什么以及如何模拟?只需查看代码并检查正在执行的操作。因此,让我们例如检查createNewWorkout
方法:
// store/actions.js
createNewWorkout ({commit, state}, workout) {
//...
**let newWorkoutKey = state.workoutsRef.push().key**
let updates = {}
updates['/workouts/' + newWorkoutKey] = workout
updates['/user-workouts/' + state.user.uid + '/' + newWorkoutKey] = workout
**return firebaseApp.database().ref().update(updates)**
}
这里发生了什么?状态的workoutsReference
生成了一些新的键,然后创建了名为updates
的对象。该对象包含两个条目 - 分别为保存了 workout 对象的 Firebase 数据库资源。
然后调用 Firebase 的数据库update
方法与此对象。因此,我们必须模拟数据库的update
方法,以便我们可以检查调用它时传入的数据。我们还必须以某种方式将此模拟注入到大型 Firebase 应用程序模拟中。创建一个文件夹来保存我们的模拟文件,并将其命名为__mocks__
。在此目录中添加两个文件 - firebaseMocks.js
和firebaseAppMock.js
。在firebaseMocks
文件中为update
方法创建一个空函数:
// __mocks__/firebaseMocks.js
export default {
**update: () => {}**
}
创建一个firebaseApp
对象的模拟,它将在其database
方法内调用模拟的update
函数:
// __mocks__/firebaseAppMock.js
import firebaseMocks from './firebaseMocks'
export default {
**database**: () => {
return {
ref: function () {
return {
**update: firebaseMocks.update**
}
}
}
}
}
为了测试createNewWorkout
方法,我们将使用jest.mock
函数将 Firebase 对象绑定到其模拟。查看有关jest.mock
函数的详细文档:
facebook.github.io/jest/docs/en/jest-object.html#jestmockmodulename-factory-options
。
在导入actions.js
模块之前,我们需要绑定我们的模拟。这样,它将已经使用模拟对象。因此,我们的导入部分将如下所示:
// test/actions.spec.js
import mockFirebaseApp from '~/__mocks__/firebaseAppMock'
**jest.mock('~/firebase', () => mockFirebaseApp)**
**import actions from '~/store/actions'**
让我们看看workout
对象的情况,以便了解如何模拟和进行确定性测试。我们有以下这些行:
// actions.js
workout.username = state.user.displayName
workout.uid = state.user.uid
因此,我们状态对象的模拟必须包含具有预定义的displayName
和uid
的用户对象。让我们创建它:
let state = {
user: {
displayName: 'Olga',
uid: 1
}}
接下来会发生什么?
workout.date = Date.now()
workout.rate = 0
再次,我们需要模拟Date.now
对象。让我们像在utils
测试规范中所做的那样做同样的事情:
Date.now = **jest.fn(() => 2000)**
让我们进一步阅读我们的方法。它包含一行代码,根据workoutsRef
状态对象生成newWorkoutKey
变量:
let newWorkoutKey = state.workoutsRef.push().key
让我们在我们的状态模拟中也模拟workoutsRef
:
let state = {
user: {
displayName: 'Olga',
uid: 1
},
**workoutsRef: {**
**push: function () {**
**return {**
**key: 59**
**}**
**}**
}}
现在我们知道,当我们调用addNewWorkout
方法时,最终预期会调用 Firebase 数据库的update
方法,该方法将包含两个条目的对象 - 一个带有键/user-workouts/1/59
,另一个带有键/workouts/59
,两者都具有相同的workout
对象的条目:
{
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
所以,首先我们需要创建一个间谍。间谍是一个特殊的函数,它将替换我们绑定到它的函数,并监视这个函数发生的任何事情。再次强调,你不需要为间谍安装任何外部插件或库。Jest 已经内置了它们。
注意
在官方文档中查看 Jest 间谍:
facebook.github.io/jest/docs/jest-object.html#jestspyonobject-methodname
因此,我们想在update
模拟函数上创建一个间谍。让我们创建一个间谍:
const spy = jest.**spyOn**(firebaseMocks, 'update')
最后,我们的断言将如下所示:
expect(spy).toHaveBeenCalledWith({
'/user-workouts/1/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
},
'/workouts/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
})
整个测试将如下所示:
describe('createNewWorkout', () => {
it('should call update with', () => {
const spy = jest.spyOn(firebaseMocks, 'update')
Date.now = jest.fn(() => 2000)
let state = {
user: {
displayName: 'Olga',
uid: 1
},
workoutsRef: {
push: function () {
return {
key: 59
}
}
}}
actions.createNewWorkout({state: state}, {})
expect(spy).toHaveBeenCalledWith({
'/user-workouts/1/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
},
'/workouts/59': {
'date': 2000,
'rate': 0,
'uid': 1,
'username': 'Olga'
}
})
})
})
现在你知道如何在不同的 Firebase 方法上创建模拟和如何在它们上创建间谍,你可以创建其余的测试规范来测试其余的操作。在chapter9/3/profitoro
文件夹中查看此部分的代码。
让我们继续学习如何使用 Jest 实际测试我们的 Vue 组件!
使 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件一起工作
测试依赖于 Vuex 存储和 Nuxt.js 的 Vue 组件并不是一件容易的任务。我们必须准备好几件事情。
首先,我们必须安装jest-vue-preprocessor
,以便告诉 Jest Vue 组件文件是有效的。我们还必须安装babel-preset-stage-2
,否则 Jest 会抱怨 ES6 的spread操作符。运行以下命令:
**npm install --save-dev jest-vue-preprocessor babel-preset-stage-2**
安装完依赖项后,在.babelrc
文件中添加stage-2
条目:
// .babelrc
{
"presets": ["es2015", "stage-2"]
}
现在我们需要告诉 Jest 它应该使用babel-jest
转换器来处理常规的 JavaScript 文件,以及使用jest-vue-transformer
来处理 Vue 文件。为了做到这一点,在package.json
文件的 jest 条目中添加以下内容:
// package.json
"jest": {
**"transform": {**
**"^.+\\.js$": "<rootDir>/node_modules/babel-jest",**
**".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"**
}
}
我们在我们的组件中使用了一些图像和样式。这可能会导致一些错误,因为 Jest 不知道这些 SVG 文件是什么。让我们在package.json
文件的moduleNameMapper
Jest 条目中再添加一个条目:
// package.json
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": **"<rootDir>/__mocks__/fileMock.js"**,
"\\.(css|scss)$": **"<rootDir>/__mocks__/styleMock.js"**,
// ...
}
}
我们这样做是因为我们并不真的想测试图片或 CSS/SCSS 文件。
将styleMock.js
和fileMock.js
添加到__mocks__
目录,内容如下:
// styleMock.js
module.exports = {}
// fileMock.js
module.exports = 'test-file-stub'
查看官方文档以获取更多相关细节:facebook.github.io/jest/docs/webpack.html
。
为 Vue 和 Vuex 文件添加名称映射:
// package.json
"jest": {
// ...
"moduleNameMapper": {
// ...
**"^vue$": "vue/dist/vue.common.js",**
**"^vuex$": "vuex/dist/vuex.common.js",**
"^~(.*)$": "<rootDir>/$1"
}
},
作为配置的最后一步,我们需要映射 Vue 文件的名称。Jest 很笨,无法理解我们实际上是在导入 Vue 文件,如果我们没有导入它的扩展名。因此,我们必须告诉它,从components
或pages
文件夹导入的任何内容都是 Vue 文件。因此,在这些配置步骤的最后,我们的 jest 的moduleNamMapper
条目将如下所示:
"jest": {
//...
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.js",
"^vue$": "vue/dist/vue.common.js",
"^vuex$": "vuex/dist/vuex.common.js",
**"^~/(components|pages)(.*)$": "<rootDir>/$1/$2.vue",**
"^~(.*)$": "<rootDir>/$1"
}
}
我们现在准备测试我们的组件。您可以在chapter9/4/profitoro
文件夹中找到所有这些配置步骤的最终代码。
使用 Jest 测试 Vue 组件
让我们从测试Header
组件开始。由于它依赖于 Vuex 存储,而 Vuex 存储又高度依赖于 Firebase,我们必须做与测试 Vuex 操作相同的事情——在将存储注入到被测试的组件之前模拟 Firebase 应用程序。首先创建一个名为HeaderComponent.spec.js
的规范文件,并将以下内容粘贴到其import
部分:
import Vue from 'vue'
**import mockFirebaseApp from '~/__mocks__/firebaseAppMock'**
**jest.mock('~/firebase', () => mockFirebaseApp)**
**import store from '~/store'**
import HeaderComponent from '~/components/common/HeaderComponent'
请注意,我们首先模拟 Firebase 应用程序,然后导入我们的存储。现在,为了能够使用模拟存储正确测试我们的组件,我们需要将存储注入其中。这样做的最佳方法是在其中创建一个带有HeaderComponent
的Vue
实例:
// HeaderComponent.spec.js
let $mounted
beforeEach(() => {
$mounted = new Vue({
template: '<header-component **ref="headercomponent"**></header-component>',
**store: store()**,
**components: {**
**'header-component': HeaderComponent**
**}**
}).$mount()
})
请注意,我们已经将引用绑定到已安装的组件。现在我们将能够通过调用$mounted.$refs.headercomponent
来访问我们的头部组件:
**let $headerComponent = $mounted.$refs.headercomponent**
在这个组件中我们可以测试什么?它实际上没有太多的功能。它有一个名为onLogout
的方法,该方法调用logout
操作并将/
路径推送到组件的$router
属性。因此,我们实际上可以模拟$router
属性,调用onLogout
方法,并检查该属性的值。我们还可以对logout
操作进行监视,并检查它是否已被调用。因此,我们对组件的onLogout
方法的测试可以如下所示:
// HeaderComponent.spec.js
test('onLogout', () => {
let $headerComponent = $mounted.$refs.headercomponent
**$headerComponent.$router = []**
const spy = jest.spyOn($headerComponent, 'logout')
$headerComponent.onLogout()
**expect(spy).toHaveBeenCalled()**
**expect($headerComponent.$router).toEqual(['/'])**
})
运行测试。您将看到许多与 Nuxt 组件未正确注册相关的错误:
关于 nuxt-link 组件的 Vue 错误
嗯,如果你能忍受这些错误,就忍受吧。否则,以生产模式运行您的测试:
// package.json
"test": "NODE_ENV=production jest"
提示
请注意,如果您以生产模式运行测试,实际上可能会错过一些相关错误。
恭喜!您已经能够使用 Jest 测试依赖于 Nuxt、Vuex 和 Firebase 的 Vue 组件!检查chapter9/5/profitoro
目录中的此测试代码。
使用 Jest 进行快照测试
Jest 最酷的功能之一是快照测试。什么是快照测试?当我们的组件被渲染时,它们会产生一些 HTML 标记,对吧?一旦你的应用程序稳定下来,很重要的一点是,新添加的功能不会破坏已经存在的稳定标记,你不觉得吗?这就是快照测试存在的原因。一旦你为某个组件生成了快照,它将保留在快照文件夹中,并在每次测试运行时,它将比较输出与现有的快照。创建快照非常容易。在挂载组件后,你只需要在该组件的 HTML 上调用期望的toMatchSnapshot
:
let $html = $mounted.$el.outerHTML
expect($html).**toMatchSnapshot()**
我将为一个测试套件文件中的所有页面运行快照测试。在这之前,我将模拟我们的 Vuex 存储器的 getter,因为有些页面使用未初始化的用户对象,从而导致错误。因此,在我们的__mocks__
文件夹内创建一个名为gettersMock
的文件,并添加以下内容:
// __mocks__/gettersMock.js
export default {
**getUser: () => {**
**return {displayName: 'Olga'}**
**}**,
getConfig: () => {
return {
workingPomodoro: 25,
shortBreak: 5,
longBreak: 10,
pomodorosTillLongBreak: 3
}
},
getDisplayName: () => {
return 'Olga'
},
getWorkouts: () => {
return []
},
getTotalPomodoros: () => {
return 10
},
isAuthenticated: () => {
return false
}
}
让我们回到导入部分。正如我们已经发现的那样,Jest 在确定导入内容时并不是很擅长,因此它会抱怨相对导入(那些从点开始的导入,例如,在每个components
文件夹内的index.js
文件中)。让我们用它们的绝对等价物替换所有这些相对导入路径:
// components/landing/index.js
export {default as Authentication} from '**~/components**/landing/Authentication'
//...
我还在package.json``jest
条目内的名称映射器条目中添加了一个映射:
"jest": {
"moduleNameMapper": {
//...
**"^~/(components/)(common|landing|workouts)$": "<rootDir>/$1/$2"**
//...
}
}
太棒了!创建一个pages.snapshot.spec.js
文件,并导入所有必要的模拟对象和所有页面。不要忘记将相应的模拟对象绑定到 Vuex“getter”函数和 Firebase 应用程序对象。你的导入部分应该如下所示:
// pages.snapshot.spec.js
import Vue from 'vue'
import mockFirebaseApp from '~/__mocks__/firebaseAppMock'
import mockGetters from '~/__mocks__/getterMocks'
**jest.mock('~/firebase', () => mockFirebaseApp)**
**jest.mock('~/store/getters', () => mockGetters)**
import store from '~/store'
**import IndexPage from '~/pages/index'**
**import AboutPage from '~/pages/about'**
**import LoginPage from '~/pages/login'**
**import PomodoroPage from '~/pages/pomodoro'**
**import SettingsPage from '~/pages/settings'**
**import StatisticsPage from '~/pages/statistics'**
**import WorkoutsPage from '~/pages/workouts'**
我们将为每个页面创建一个测试规范。我们将以与我们绑定Header
组件相同的方式绑定每个页面组件。我们将导出我们想要测试的组件作为 Vue 实例的组件,并在创建后挂载此 Vue 实例。因此,索引组件绑定将如下所示:
// pages.snapshot.spec.js
let $mounted = new Vue({
template: '<index-page></index-page>',
store: store(),
components: {
'index-page': IndexPage
}
}).$mount()
你现在唯一需要做的就是执行快照期望。因此,索引页面的完整测试规范将如下所示:
// pages.snapshot.spec.js
describe('pages', () => {
test('index snapshot', () => {
let $mounted = new Vue({
template: '<index-page></index-page>',
store: store(),
components: {
'index-page': IndexPage
}
}).$mount()
**let $html = $mounted.$el.outerHTML**
**expect($html).toMatchSnapshot()**
})
})
对所有页面重复相同的步骤。运行测试!检查覆盖率。现在我们在谈论!我们实际上触及了几乎所有应用程序的组件!看看这个:
我们应用程序的几乎所有组件和文件都出现在覆盖报告中!
最重要的事情,实际上是快照测试的整个目的,就是在测试文件夹内生成的名为__snapshots__
的文件夹。在这里,您将找到所有页面的所有 HTML 标记的新生成快照。这些快照看起来像这样:
ProFitOro 页面的 Jest 快照
每当您进行影响标记的操作时,测试将失败。如果您真的想要更新快照,请使用更新标志运行测试:
**npm test -- --u**
我发现快照测试是一个非常有趣和令人兴奋的功能!
提示
非常重要的是要提交您的快照文件!查看官方 Jest 网站上有关快照测试的详细文档:
facebook.github.io/jest/docs/snapshot-testing.html
本章的最终代码可以在chapter9/6/profitoro
文件夹中找到。
总结
在本章中,我们使用了非常热门的技术来测试我们的 Vue 应用程序。我们使用了 Jest,并学习了如何创建模拟,测试组件,并使用它进行快照测试。
在下一章中,我们将最终看到我们的应用程序上线!我们将使用 Google Firebase Hosting 部署它,并提供必要的 CI/CD 工具,以便我们的应用程序在每次推送到主分支时都会自动部署和测试。您准备好看到您的作品上线并运行了吗?让我们开始吧!
第十章:使用 Firebase 部署
在上一章中,我们为应用程序的代码设置了测试框架,从现在开始可以使用单元测试和快照测试。在本章中,我们将使我们的应用程序上线!我们还将设置持续集成(CI)和持续部署(CD)环境。因此,在本章中,我们将学习如何执行以下操作:
-
使用本地 Firebase 工具部署到 Firebase 托管
-
使用 CircleCI 设置 CI 工作流程
-
使用 Firebase 和 CircleCI 设置暂存和生产环境
从本地机器部署
在本节中,我们将使用 Firebase 命令行工具部署我们的应用程序。我们已经完成了。查看 Google Firebase 文档进行快速入门:firebase.google.com/docs/hosting/quickstart
。
基本上,如果你还没有安装 Firebase 工具,请立即安装!
**npm install -g firebase-tools**
现在切换到你的项目目录并初始化一个 Firebase 项目:
**firebase init**
从下拉菜单中选择托管。
提示
这并不是很明显,所以请记住,要实际从列表中选择某个东西,你必须按下空格。
按空格选择托管功能
之后,从列表中选择你的 ProFitOro 项目,然后指定dist
文件夹作为构建输出目录:
输入资产的公共目录 dist
回答下一个问题选择“否”,然后你就完成了!确保 Firebase 在你的项目文件夹中创建firebase.json
和.firebaserc
文件。
这就是firebase.json
文件的样子:
// firebase.json
{
"hosting": {
"public": "dist"
}
}
这就是你的.firebaserc
文件的样子:
.firebasercs
{
"projects": {
"default": "profitoro-ad0f0"
}
}
你已经完成了!现在,如果我们使用npm run generate
命令生成静态资产,这些资产将最终出现在dist
文件夹中。之后运行firebase deploy
,你的应用程序将立即部署!
因此,请继续执行以下操作:
**npm run generate**
**firebase deploy**
如果遇到错误或问题,请执行以下操作:
-
确保你的 Firebase CLI 是最新的
-
如果需要,使用
firebase login --reauth
重新进行身份验证 -
如果出现错误,请尝试使用
firebase use --add
添加项目
恭喜!你的应用程序已经启动运行!
注意
您可能会问,如果最终我们只是生成静态资产进行部署,为什么我们要费心使用整个 Nuxt 路由和服务器端渲染。问题是,不幸的是,Firebase 只托管静态文件。为了能够运行一个节点服务器,我们应该使用另一个容器,比如,例如 Heroku:stackoverflow.com/questions/30172320/firebase-hosting-with-own-server-node-js
。
还有一件事情你应该知道:现在我们无法在本地运行我们的应用程序;如果我们尝试这样做,我们将会收到一个webpack
错误:
当我们尝试在本地运行应用程序时出现 webpack 错误
出于某种原因,我们的actions.js
文件尝试导入firebase.json
而不是位于firebase
目录内的 Firebase 应用程序index.js
文件。这很容易解决。将 Firebase 目录重命名为firebaseapp
- 最终,它就是位于内部的内容。请在chapter10/1/profitoro
文件夹中找到与此部分对应的代码。注意根目录中的新firebase.json
和.firebaserc
文件,以及所有 Firebase 应用程序的导入已更改为firebaseapp
文件夹。
使用 CircleCI 设置 CI/CD
现在,如果我们想部署我们的应用程序,我们首先必须在本地运行测试,以确保一切正常,然后使用firebase deploy
命令进行部署。理想情况下,所有这些都应该是自动的。理想情况下,如果我们将代码推送到主分支,一切都应该自动进行,不需要我们的干预。具有自动化测试检查的自动化部署过程称为持续部署。这个术语的意思就像它听起来的那样 - 您的代码正在持续部署。有很多工具可以让您一次点击按钮或只需推送到主分支即可自动将代码部署到生产环境。从可靠的 Jenkins 开始,到 Codeship、CloudFlare、CircleCI、Travis……列表是无穷无尽的!我们将使用 CircleCI,因为它与 GitHub 集成得很好。如果您想了解如何使用 Travis 进行部署,请查看我之前关于 Vue.js 的书籍:
www.packtpub.com/web-development/learning-vuejs-2
首先,你应该将你的项目托管在 GitHub 上。请按照 GitHub 文档学习如何初始化你的仓库:
help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
或者只需 fork 我的:
一旦你的仓库上线,就在 CircleCI 上创建你的账户:
使用 CircleCI web 界面,创建一个新项目,并从列表中选择你的仓库。之后,选择 Linux 操作系统和 Node 作为语言:
CircleCI 项目配置
现在我们必须为我们的项目添加一个 CircleCI 配置,这样第一次推送时就知道该做什么。创建一个名为config.yml
的文件的.circleci
文件夹,并包含以下内容:
// .circleci/config.yml
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:7.10
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
# run tests!
- run: npm test
提交并推送更改到主分支。转到 CircleCI 界面,点击开始构建按钮:
点击开始构建按钮
如果你像我一样幸运,你会看到以下成功的输出:
CircleCI 成功!
让我们在我们的README.md
文件中添加一个状态徽章,这样它就会出现在 GitHub 上。转到你的 CircleCI 项目设置(点击项目名称旁边的齿轮):
点击项目名称旁边的齿轮以打开项目的设置选项卡
在设置部分,选择通知|状态徽章:
导航到设置|通知|状态徽章
复制并粘贴 markdown 代码到你的README.md
文件中,使其看起来如下:
// README.md
# Profitoro
**[![CircleCI](https://circleci.com/gh/chudaol/profitoro.svg?style=svg)](https://circleci.com/gh/chudaol/profitoro)**
> Take breaks during work. Exercise during breaks.
提交并推送更改到主分支!
如果你现在打开你的 GitHub 仓库,你会看到这个漂亮的徽章上写着通过:
CircleCI 徽章显示一个好消息:通过
但我们的整个目的不仅仅是看到一个漂亮的绿色徽章,而是真正能够部署到 Firebase 托管容器。为了做到这一点,我们必须配置 CircleCI。我们通过向config.yml
文件添加deploy
部分来实现这一点。查看circleci.com/docs/2.0/configuration-reference/#deploy
上的文档。为了能够部署到 Firebase 托管,我们需要登录。很明显,CircleCI 在任何情况下都没有登录到我们的 Firebase 帐户。幸运的是,这对我们来说很容易解决。我们需要生成一个 CI 令牌,然后在我们的deploy
命令中使用它。
注意
可以使用firebase login:ci
命令生成 Firebase CI 令牌。
只需在控制台中运行此命令:
**firebase login:ci**
您将获得类似于此的输出:
Firebase login:ci 命令的输出
转到您的 CircleCI 的 Web 界面,并找到您项目的设置。在左侧,您会看到名为构建设置的选项卡。单击环境变量链接,将弹出环境变量部分。单击添加变量按钮,添加名为FIREBASE_TOKEN
的变量,值为YOUR_GENERATED_TOKEN
:
在您的 CircleCI 项目设置中添加一个新的环境变量
现在让我们在config.yml
文件中添加一个部署步骤。在这之前,请记住我们必须调用firebase deploy
命令。好吧,为此,我们应该在 CircleCI 服务器上全局安装 Firebase 工具。而不是在 CircleCI 服务器上污染一些全局安装的软件,让我们将其安装为dev 依赖项,然后从node_modules
文件夹中调用命令。因此,首先,将firebase-tools
安装为dev
依赖项:
**npm install --save-dev firebase-tools**
现在我们终于可以添加deploy
步骤了。在这一步中,我们必须使用npm run generate
命令生成资产,并使用我们的令牌运行firebase deploy
(命令将是firebase deploy --token=<YOUR_FIREBASE_TOKEN>
)。我们不必指定令牌本身,因为我们已经为其创建了一个环境变量,所以命令将如下所示:
**firebase deploy --token=$FIREBASE_TOKEN**
整个deploy
条目将如下所示:
// .circleci/config.yml
jobs:
build:
#...
steps:
- checkout
#...
# deploy!
**- deploy:**
**command: |**
**if [ "${CIRCLE_BRANCH}" == "master" ]; then**
**npm run generate**
**./node_modules/.bin/firebase deploy --token=$FIREBASE_TOKEN --non-interactive**
**fi**
推送更改。检查您的 CircleCI 控制台。成功部署后,检查您的 Firebase 控制台的托管选项卡,并确保最后一次部署正好在此时进行:
确保最后一次部署正好在此时进行!
这不是很棒吗?每当您将新更改推送到主分支时,它们将首先进行测试,只有在所有测试都通过后才会部署到您的 Firebase 托管提供商!我们设置所有这些需要多长时间?20 分钟?太棒了!
设置暂存和生产环境
您可能知道,直接部署到生产环境并不是一个很好的做法。即使测试通过,我们也必须先检查一切是否正确,这就是为什么我们需要一个暂存环境。
让我们在 Firebase 控制台上创建一个新项目,并将其命名为profitoro-staging
。现在使用 Firebase 命令行工具向我们的项目添加一个新环境。只需在控制台中运行此命令:
**firebase use –add**
选择正确的项目:
选择一个新创建的 profitoro-staging 项目
在下一步中键入别名staging
:
What alias do you want to use for this project? (e.g. staging) **staging**
检查.firebaserc
文件是否已添加新条目:
// .firebaserc
{
"projects": {
"default": "profitoro-ad0f0",
**"staging": "profitoro-staging"**
}
}
如果您现在在本地运行firebase use staging
命令,然后在其后运行firebase deploy
,您的项目将部署到我们新创建的暂存环境。如果您想切换并部署到生产环境,只需运行firebase use default
命令,然后是firebase deploy
命令。
现在我们需要重新配置我们的 CircleCI 工作流程。我们想要实现的是自动将资产部署到暂存服务器,然后进行手动批准以便部署到生产环境。为此,我们将使用带有手动批准的工作流配置。请查看有关此事的 CircleCI 官方文档页面:circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
。
我们最终会得到两个非常相似的作业-第一个将被称为build
,它将包含与以前完全相同的内容,唯一的区别是部署步骤将使用别名staging
:
version: 2
jobs:
build:
docker
#...
# **deploy to staging!**
- deploy:
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
npm run generate
**./node_modules/.bin/firebase use staging**
./node_modules/.bin/firebase deploy --token=$FIREBASE_TOKEN --non-interactive
fi
第二个任务将被称为deploy
,它将执行与staging
任务完全相同的步骤(只是为了确保一切都没问题)。唯一的区别是在部署之前它将使用default
别名:
**build**:
#...
deploy:
docker:
# ...
# **deploy to production!**
- deploy:
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
npm run generate
**./node_modules/.bin/firebase use default**
./node_modules/.bin/firebase deploy --token=$FIREBASE_TOKEN --non-interactive
fi
之后,我们将添加一个名为workflows
的新条目,如下所示:
// .circleci/config.yml
jobs:
build:
#...
deploy:
#...
workflows:
version: 2
build-and-approval-deploy:
jobs:
- build
- hold:
type: approval
requires:
- build
- deploy:
requires:
- hold
提交并推送到主分支。检查您的 CircleCI 控制台。成功部署到暂存环境后,单击Workflow选项卡,并检查它实际上是暂停状态:
工作流程处于暂停状态
检查您的暂存环境网站,并确保一切正常。
在完全确信一切都没问题之后,我们可以将我们的构建推广到生产环境。单击您的工作流程,然后单击批准按钮:
现在我们可以手动批准生产部署。
过一会儿,将会取得巨大成功!这不是很棒吗?
提示
尽管这超出了本书的范围,但请记住,在对暂存环境运行一些检查时,您不希望搞砸生产数据库。因此,为了使暂存成为真正的暂存,生产成为真正的生产,我们还应该设置一个暂存数据库。
检查chapter10/2/profitoro
文件夹中的此部分代码。您需要注意的唯一两件事是.firebaserc
配置文件和位于.circleci/config.yml
目录中的 CircleCI 配置。
我们取得了什么成就?
亲爱的读者,我们已经走过了一段漫长的旅程。我们从最开始构建了我们的响应式应用程序,直到部署。我们使用了诸如 Vue.js、Bootstrap 4 和 Google Firebase 等优秀的技术来构建我们的应用程序。我们不仅使用了所有这些技术并学习了它们如何协同工作,而且实际上遵循了整个软件开发过程。
我们从业务理念、需求定义、用户故事定义和模型创建开始。我们继续进行实际实施-前端和后端都有。我们使用 Jest 进行了彻底的测试,最终将应用程序部署到了两个不同的环境中。甚至不仅仅是部署-我们实施了一个 CD 策略,它将自动为我们执行部署过程。
最重要的是-我们最终得到了一个完全功能的应用程序,可以让我们在工作期间管理时间并保持健康!
我甚至创建了一个 Facebook 页面:
如果你喜欢 ProFitOro 的标志设计,请向我的朋友 Carina 表示爱意和感谢:
<car.marg@gmail.com>
如果你喜欢模拟设计的方式,你应该感谢我的朋友和同事 Safi:
如果你喜欢 ProFitOro 的设计和插图,请查看我的朋友 Vanessa 的其他作品(www.behance.net/MeegsyWeegsy
),并与她交谈,如果你觉得她可能也能帮助你。
如果你喜欢使用 SCSS 实现设计的方式,请给我的朋友 Filipe 一些赞(github.com/fil090302
)。
总结
在本章中,我们使用了 CircleCI 和 Firebase 来保证我们不断部署的软件的持续质量。正如我已经提到的,看到你从零开始创建的东西运行起来是如此美好!
然而,我们的工作还没有完成。还有很多改进要做。我们需要验证。我们需要编写更多的测试来增加我们的代码覆盖率!我们需要更多的锻炼,而且我们需要它们看起来漂亮。我们可能需要一些后台管理,让负责人可以在实际出现在所有人可见的锻炼列表之前检查每个添加的锻炼并批准它。
我们需要一个合适的统计页面,带有一些漂亮的图形。我们需要优化图像渲染。我们需要为每个锻炼显示多张图片。我们可能需要为锻炼添加视频支持。我们还需要在番茄工作计时器结束后出现的锻炼屏幕上做一些工作。现在看起来是这样的:
这里有很多按钮!但实际上它们都不起作用 😦
这里有三个按钮,但它们都不起作用。
所以,正如你所看到的,虽然我们已经完成了这本书,也有了一个功能齐全的软件,但我们还有一些工作要做。实际上,这让我感到非常高兴,因为这让我觉得我现在不必说再见。
与我分享您的想法,做一些了不起的事情并与我分享,或在 GitHub 上创建一些拉取请求或问题。我很乐意回答您。如果您有任何问题,建议或想法,请给我发电子邮件至<chudaol@gmail.com>
。
感谢阅读本书并……去工作……出去!
索引
A
-
会计
-
关于/ AAA 解释
-
操作
-
定义/ 定义操作和突变
-
警报组件
-
参考/ 继续结合 Vue.js 和 Bootstrap
-
匿名用户
-
管理/ 管理匿名用户
-
应用程序
-
部署/ 部署您的应用程序
-
脚手架/ 搭建应用程序
-
异步测试
-
Jest,使用/ 使用 Jest 进行异步测试-测试操作
-
身份验证/ AAA 解释
-
身份验证
-
关于/ AAA 解释
-
使用 Firebase 工作/ Firebase 的身份验证工作原理是什么?
-
身份验证,Firebase 文档
-
参考/ 更新用户配置文件
-
身份验证 API,Firebase
-
参考/ 如何将 Firebase 身份验证 API 连接到 Web 应用程序
-
身份验证 UI
-
增强/ 再次使身份验证 UI 变得很棒
-
授权
-
关于/ AAA 解释
B
-
引导
-
用于添加表单/ 使用 Bootstrap 添加表单
-
关于/ Bootstrap
-
参考/ Bootstrap
-
功能/ Bootstrap
-
组件/ Bootstrap 组件
-
工具/ Bootstrap 工具
-
布局/ Bootstrap 布局
-
Vue.js,结合/ 结合 Vue.js 和 Bootstrap, 继续结合 Vue.js 和 Bootstrap
-
用于检查倒计时计时器组件的响应性/ 使用 Bootstrap 检查倒计时计时器的响应和适应性
-
用于检查倒计时计时器组件的适应性/ 使用 Bootstrap 检查倒计时计时器的响应和适应性
-
Bootstrap 标记
-
添加/ 添加 Bootstrap 标记
-
Bootstrap-Vue
-
参考/ 继续结合 Vue.js 和 Bootstrap
-
Bootstrap 类
-
用于创建布局/ 使用 Bootstrap 类创建布局
-
Bootstrap 模态框
-
用于显示锻炼/ 使用 Bootstrap 模态框显示每个锻炼, 锻炼
-
参考 / 使用 Bootstrap 模态框显示每个锻炼
-
Bootstrap 导航栏
-
使用,用于导航链接 / 使用 Bootstrap 导航栏进行导航链接
-
按钮
-
参考 / 倒计时计时器组件 - 让我们倒计时!
C
-
驼峰式命名 / 定义 ProFitOro 组件
-
卡片 Bootstrap 组件
-
参考 / 结合 Vue.js 和 Bootstrap
-
卡片,Bootstrap 文档
-
参考 / 添加 Bootstrap 支持的标记
-
CI/CD
-
设置,使用 Circle CI / 使用 CircleCI 设置 CI/CD
-
Circle CI
-
用于设置 CI/CD / 使用 CircleCI 设置 CI/CD
-
CircleCI
-
参考 / 使用 CircleCI 设置 CI/CD, 设置暂存和生产环境
-
用于对齐的类,Bootstrap
-
参考 / 管理匿名用户
-
代码拆分 / 代码拆分或延迟加载
-
组件
-
消息卡,提取到 / 将消息卡提取到它们自己的组件中
-
组件,Bootstrap / Bootstrap 组件
-
组件,Vue / Vue 组件
-
倒计时计时器组件
-
响应性 / 使用 Bootstrap 实现倒计时器的响应和适应性
-
适应性 / 使用 Bootstrap 实现倒计时器的响应和适应性
-
倒计时,计数 / 倒计时组件-让我们倒计时!
-
自定义域
-
连接到 Firebase 项目 / 额外里程-将您的 Firebase 项目连接到自定义域
-
自定义模板,vue-cli
-
参考 / Vue-cli
D
-
数据库条目
-
添加到 Firebase 应用程序数据库 / 向 Firebase 应用程序数据库添加第一个条目
E
- 元素 / Vue.js
F
-
文件上传
-
参考 / 使用 Firebase 数据存储存储图像
-
Firebase
-
关于 / 在 Firebase 控制台中创建项目, 什么是 Firebase?
-
服务 / 什么是 Firebase?
-
Firebase API 文档
-
参考 / 使用 Firebase 数据存储存储图像
-
Firebase 应用程序数据库
-
数据库条目,添加 / 向 Firebase 应用程序数据库添加第一个条目
-
Firebase 身份验证 API
-
工作流程/ Firebase 如何进行身份验证?
-
连接到 Web 应用程序/ 如何将 Firebase 身份验证 API 连接到 Web 应用程序
-
Firebase 控制台
-
项目,创建于/ 在 Firebase 控制台中创建项目
-
参考/ 在 Firebase 控制台中创建项目, 设置 Firebase 项目
-
Firebase 数据库
-
Vuex 存储,连接到/ 将 Vuex 存储连接到 Firebase 数据库
-
Firebase 数据存储
-
用于存储图像/ 使用 Firebase 数据存储存储图像, 让我们搜索!
-
Firebase 项目
-
Vue.js 应用程序,连接到/ 将 Vue.js 应用程序连接到 Firebase 项目
-
连接到自定义域/ 额外里程-将 Firebase 项目连接到自定义域
-
设置/ 设置 Firebase 项目
-
Firebase 实时数据库
-
用于存储锻炼/ 使用 Firebase 实时数据库存储新锻炼
-
文档,参考/ 使用 Firebase 实时数据库存储新锻炼
-
Firebase 实时数据库文档
-
参考 / 将 Vuex 存储连接到 Firebase 数据库
-
Firebase SDK
-
基于密码的身份验证 / Firebase 身份验证是如何工作的?
-
基于电子邮件的身份验证 / Firebase 身份验证是如何工作的?
-
联合实体提供者身份验证 / Firebase 身份验证是如何工作的?
-
基于电话号码的身份验证 / Firebase 身份验证是如何工作的?
-
自定义身份验证系统集成 / Firebase 身份验证是如何工作的?
-
匿名用户身份验证 / Firebase 身份验证是如何工作的?
-
flex-box
-
参考 / Bootstrap 实用工具
-
页脚
-
自定义 / 美化页脚
-
表单
-
添加,使用 Bootstrap / 使用 Bootstrap 添加表单
-
表单,Bootstrap 文档
-
参考 / 使用 Bootstrap 添加表单
-
功能,Bootstrap
-
引用 / Bootstrap
-
功能需求
-
收集 / 收集需求
G
-
GoDaddy
-
参考 / 额外里程 – 将 Firebase 项目连接到自定义域
-
Google Firebase
-
参考 / 从本地机器部署
H
-
Heroku
-
参考 / 从本地机器部署
-
历史 API
-
参考 / Vue 路由器
-
人机交互(HCI)
-
关于 / 模拟
我
-
图片
-
存储,使用 Firebase 数据存储 / 使用 Firebase 数据存储存储图片, 让我们搜索!
J
-
Jest
-
关于 / 什么是 Jest?
-
参考 / 什么是 Jest?
-
使用 / 开始使用 Jest
-
覆盖率 / 覆盖率
-
模拟 / 使用 Jest 进行模拟
-
模拟,参考 / 使用 Jest 进行模拟
-
文档,参考 / 使用 Jest 进行模拟
-
用于测试 Vuex 存储 / 使用 Jest 测试 Vuex 存储
-
异步测试 / 使用 Jest 进行异步测试 - 测试操作
-
与 Vuex 一起工作 / 使用 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件
-
使用 Vue 组件工作 / 使用 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件
-
与 Nuxt.js 一起工作 / 使 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件一起工作
-
与 Firebase 一起工作 / 使 Jest 与 Vuex、Nuxt.js、Firebase 和 Vue 组件一起工作
-
用于测试 Vue 组件 / 使用 Jest 测试 Vue 组件
-
用于快照测试 / 使用 Jest 进行快照测试
-
jest.mock 函数
-
参考 / 使用 Jest 进行异步测试-测试操作
-
Jest 间谍
-
参考 / 使用 Jest 进行异步测试-测试操作
-
巨幕
-
参考 / 介绍锻炼
K
- KebabCased / 定义 ProFitOro 组件
L
-
布局
-
创建,使用 Bootstrap 类 / 使用 Bootstrap 类创建布局
-
懒加载
-
关于 / 代码拆分或懒加载
-
参考 / 代码拆分或懒加载
-
本地机器
-
从本地机器部署 / 从本地机器部署
M
-
菜单按钮
-
工作 / 练习-使菜单按钮工作
-
消息卡
-
提取到组件中 / 将消息卡提取到它们自己的组件中
-
模拟 / 使用 Jest 进行模拟
-
模拟功能
-
参考 / 使用 Jest 进行异步测试 - 测试操作
-
模型
-
关于 / 模型
-
登录页面 / 第一页 - 登录和注册
-
番茄钟计时器,显示 / 主页显示番茄钟计时器
-
锻炼,在休息期间 / 休息期间的锻炼
-
设置区域 / 设置
-
统计 / 统计
-
锻炼 / 锻炼
-
标志 / 标志
-
模式*历史选项
-
参考 / Vue 路由器
-
Moment.js 库
-
参考 / 添加实用功能以使事物看起来更好
-
moment.js 库
-
参考 / 练习
-
突变
-
定义 / 定义操作和突变
-
突变,Vuex 存储
-
参考 / Vuex 状态管理架构
N
-
导航栏组件
-
参考 / 使用 Bootstrap 导航栏进行导航链接
-
导航
-
添加,使用 vue-router / 使用 vue-router 添加导航
-
根据身份验证限制 / 练习-根据身份验证限制导航
-
导航链接
-
使用 Bootstrap 导航栏 / 使用 Bootstrap 导航栏进行导航链接
-
名词
-
检索 / 名词
-
npm 包
-
参考 / 使用 Firebase 数据存储存储图像
-
nuxt-link
-
用于添加链接 / 使用 nuxt-link 添加链接
-
nuxt-starter 模板
-
关于 / Nuxt.js
-
参考 / Nuxt.js
-
Nuxt.js
-
关于 / Nuxt.js
-
URL / Nuxt.js
-
和 Vuex 存储 / Nuxt.js 和 Vuex 存储
-
Nuxt.js 中间件 / Nuxt.js 中间件
O
-
偏移列
-
参考 / 使用 Bootstrap 响应式和自适应倒计时器
-
单向数据绑定 / Vue.js
P
-
路径 SVG 元素
-
参考 / SVG 和三角函数
-
人物角色 / 人物角色
-
请介绍自己页面
-
参考 / 你好,用户
-
关于 / 你好,用户
-
番茄工作法
-
参考 / 陈述问题
-
番茄工作法计时器
-
主要原则/ 阐明问题
-
实现/ 实现番茄工作法计时器
-
SVG 和三角函数/ SVG 和三角函数, 练习
-
倒计时计时器组件,实现/ 实现倒计时计时器组件
-
关于/ 番茄工作法计时器, 练习
-
个性化/ 个性化番茄工作法计时器
-
预渲染 SPA 插件
-
参考/ 服务器端渲染
-
问题
-
阐明/ 阐明问题
-
profitoro
-
参考/ 使用 CircleCI 设置 CI/CD
-
ProFitOro 应用程序
-
认证到/ 认证到 ProFitOro 应用程序
-
ProFitOro 组件
-
定义/ 定义 ProFitOro 组件
-
项目
-
在 Firebase 控制台中创建/ 在 Firebase 控制台中创建项目
-
pull-*类
-
参考/ 使用 Bootstrap 实现倒计时计时器的响应性和适应性
-
push-*类
-
参考/ 使用 Bootstrap 实现倒计时计时器的响应性和适应性
R
-
响应式应用程序
-
关于/ 我们取得了什么成就?
-
router-view 组件
-
参考/ Vue 路由器
S
-
服务器端渲染(SSSR)
-
关于/ 服务器端渲染
-
参考/ 服务器端渲染
-
服务,Firebase
-
认证/ 什么是 Firebase?
-
数据库/ 什么是 Firebase?
-
托管/ 什么是 Firebase?
-
存储/ 什么是 Firebase?
-
单页面应用程序(SPA)/ Vue 路由器
-
快照测试
-
Jest,使用/ 使用 Jest 进行快照测试
-
参考/ 使用 Jest 进行快照测试
-
暂存和生产环境
-
设置/ 设置暂存和生产环境
-
样式
-
应用/ 是时候应用一些样式了
T
-
模板文字
-
参考/ 倒计时组件- 让我们倒计时!
-
模板,vue-cli
-
webpack/ Vue-cli
-
webpack-simple/ Vue-cli
-
browserify/ Vue-cli
-
browserify-simple/ Vue-cli
-
简单/ Vue-cli
-
测试
-
重要性 / 为什么测试很重要?
-
Vue 文档
-
参考 / 练习
-
双向数据绑定 / Vue.js
U
-
统一建模语言(UML) / 检索名词和动词
-
用户资料
-
更新 / 更新用户资料
-
用户故事 / 用户故事
-
实用函数
-
测试 / 测试实用函数
V
-
v-on 指令
-
参考 / 倒计时组件- 让我们倒计时!
-
动词
-
检索 / 动词
-
Vue
-
组件 / Vue 组件
-
vue-cli
-
关于 / Vue-cli
-
参考 / Vue-cli, 搭建应用程序
-
vue-router
-
用于添加导航 / 使用 vue-router 添加导航
-
参考 / 使用 Bootstrap 导航栏进行导航链接
-
vue-router 库
-
参考 / Vue 路由器
-
Vue.js
-
功能,添加 / 使用 Vue.js 使事情功能化
-
实用函数,添加 / 添加实用函数使事情看起来更美观
-
关于 / Vue.js
-
参考 / Vue.js, 使用 CircleCI 设置 CI/CD
-
在脚本中包含 / 直接在脚本中包含
-
与 Bootstrap 结合 / 结合 Vue.js 和 Bootstrap, 继续结合 Vue.js 和 Bootstrap
-
Vue.js 应用程序
-
脚手架 / 搭建 Vue.js 应用程序
-
连接到 Firebase 项目 / 将 Vue.js 应用程序连接到 Firebase 项目
-
Vue 应用程序
-
URL / 服务器端渲染
-
Vue 组件
-
使用 Jest 进行测试 / 使用 Jest 测试 Vue 组件
-
Vue 指令
-
关于 / Vue 指令
-
条件渲染 / 条件渲染
-
文本,与 HTML / 文本与 HTML
-
循环 / 循环
-
数据,绑定 / 绑定数据
-
事件,处理 / 处理事件
-
Vue 文档
-
参考 / 搭建 Vue.js 应用程序
-
vuefire 包装器
-
参考 / 什么是 Firebase?
-
Vue 实例 / Vue.js
-
Vue 项目
-
关于 / Vue 项目-入门
-
CDN 版本,使用 / CDN
-
npm 依赖项,添加到 package.json 文件 / NPM
-
Vue 路由器 / Vue 路由器
-
Vuex
-
参考,用于模块 / 设置 Vuex 存储
-
Vuexfire
-
参考 / 将 Vuex 存储连接到 Firebase 数据库
-
Vuex 状态管理架构 / Vuex 状态管理架构
-
Vuex 存储
-
状态 / Vuex 状态管理架构, 设置 Vuex 存储
-
获取器 / Vuex 状态管理架构, 设置 Vuex 存储
-
突变 / Vuex 状态管理架构, 设置 Vuex 存储
-
设置 / 设置 Vuex 存储
-
行动 / 设置 Vuex 存储
-
连接到 Firebase 数据库 / 将 Vuex 存储连接到 Firebase 数据库
-
参考 / Nuxt.js 和 Vuex 存储
-
测试,使用 Jest / 使用 Jest 测试 Vuex 存储
-
测试,突变 / 测试突变
-
测试,操作 / 使用 Jest 进行异步测试 - 测试操作
W
-
观察者
-
参考 / 练习
-
网络应用
-
Firebase 身份验证 API,连接到 / 如何将 Firebase 身份验证 API 连接到网络应用程序
-
webpack 文档
-
参考 / 代码拆分或延迟加载
-
WireframeSketcher
-
参考 / 模型
-
锻炼
-
使用 Firebase 实时数据库存储 / 使用 Firebase 实时数据库存储新的锻炼
-
使用 Bootstrap 模态框显示 / 使用 Bootstrap 模态框显示每个锻炼, 练习
-
锻炼
-
关于 / 介绍锻炼