Vue3-秘籍-全-
Vue3 秘籍(全)
原文:
zh.annas-archive.org/md5/915E62C558C25E5846A894A1C2157B6C
译者:飞龙
前言
Vue 是一个最小的前端框架,赋予开发人员创建 Web 应用程序、原型、大型企业应用程序、桌面应用程序和移动应用程序的能力。
Vue 3 是 Vue 的完全重写,并对框架的所有核心 API 进行了更改。这次重写改变了用 TypeScript 编写的代码。在 Vue 3 中,我们暴露了所有核心 API,使每个人都有可能使用 Vue。
本书从实现 Vue 3 的新功能到迁移现有 Vue 应用程序到最新版本的方法开始。您将学习如何在 Vue 中使用 TypeScript,并找到解决常见挑战和问题的简洁解决方案,从实现组件、派生物和动画,到构建插件、添加状态管理、路由和开发完整的单页应用程序(SPA)。
本书中使用的一些库、插件和框架可能会在本书编写和您阅读之间接收更新。因此,请注意任何可能导致破坏性变化的 API 更改或版本更改。
本书适合对象
这本书适用于希望了解更多关于 Vue 并希望提高他们的 Vue 技能的 Web 开发人员。我们将从介绍 Vue 3 和 TypeScript 技术开始。在接下来的章节中,读者将了解 Vue 中的新概念及其生态系统插件、UI 框架和高级技巧。
通过从头到尾地阅读本书,您将能够创建一个 Vue 应用程序,使用所有必要的 Vue 插件,并使用顶级 Vue UI 框架。如果您已经熟悉 Vue,您将发现相关的新模式。
本书涵盖内容
第一章,理解 Vue 3 和创建组件,为读者提供了如何使用新的 Vue 3 API 创建自定义 Vue 组件的方法,并使用 Vue 的暴露核心 API 和组合 API。本章还帮助读者将 Vue 2 应用程序初步升级到 Vue 3。
第二章,介绍 TypeScript 和 Vue 生态系统,向读者介绍了 TypeScript 超集以及如何使用它,从基本类型、接口和类型注解开始。读者将准备好使用 Vue CLI、TypeScript 和vue-class-component
开发 Vue 应用程序。
第三章,“数据绑定、表单验证、事件和计算属性”,讨论了基本的 Vue 开发和组件概念,包括v-model
、事件监听器、计算属性和for
循环。读者将介绍 Vuelidate 插件用于表单验证以及如何在 Vue 组件上使用它,以及如何使用vue-devtools
调试 Vue 组件。
第四章,“组件、混合和功能组件”,向读者介绍了使用不同方法构建组件,包括用于内容的自定义插槽、验证的 props、功能组件以及创建用于代码重用的混合。然后,它向读者介绍了一系列不同的方法来访问子组件的数据,创建依赖注入组件和动态注入组件,以及如何延迟加载组件。
第五章,“通过 HTTP 请求从 Web 获取数据”,向读者展示了如何在 JavaScript 上为 HTTP 调用创建 Fetch API 的自定义包装器,如何在 Vue 中使用该包装器,以及如何在 Vue 上实现自定义异步函数。读者还将学习如何在 axios 中替换包装器的 Fetch API,以及如何在 axios 上实现自定义处理程序。
第六章,“使用 vue-router 管理路由”,介绍了 Vue 的路由插件以及如何在 Vue 上使用它为 Vue 应用程序的页面创建路由。它介绍了管理路由路径的过程,路由路径上带有参数的动态路径,页面组件的延迟加载,为路由创建身份验证中间件,以及使用别名和重定向。
第七章,“使用 Vuex 管理应用程序状态”,探讨了 Vue 状态管理插件,帮助读者了解 Vuex 的工作原理以及如何应用于他们的应用程序。本章还为读者提供了创建 Vuex 模块、操作、突变和获取器的配方,并探讨了如何为存储定义基本状态。
第八章,使用过渡和 CSS 为您的应用程序添加动画,通过提供基于 CSS 的自定义动画示例,探讨了 CSS 动画和过渡的基础知识。这些将与 Vue 自定义组件一起使用,以实现一个外观漂亮的应用程序,并为应用程序的用户提供最佳体验。
第九章,使用 UI 框架创建漂亮的应用程序,介绍了流行的 UI 框架。读者将使用 Buefy、Vuetify 和 Ant-Design 构建用户注册表单,并了解它们的设计概念。本章的目的是教会读者如何使用 UI 框架创建一个外观良好的应用程序。
第十章,将应用程序部署到云平台,展示了如何在 Vercel、Netlify 和 Google Firebase 等自定义第三方主机上部署 Vue 应用程序。通过本章的示例,读者将学会如何使用集成的存储库钩子和自动部署功能自动部署他们的应用程序。
第十一章,专业联赛-指令、插件、SSR 和更多,探讨了 Vue 的高级主题,包括模式、最佳实践、如何创建插件和指令,以及如何使用 Quasar 和 Nuxt.js 等高级框架创建应用程序。
为了充分利用本书
Vue 3 beta 是撰写本书时可用的版本。所有的代码将在 GitHub 存储库的最终版本上进行更新:github.com/PacktPublishing/Vue.js-3.0-Cookbook
您需要安装 Node.js 12+,将 Vue CLI 更新到最新版本,并拥有某种良好的代码编辑器。其他要求将在每个示例中介绍。所有软件要求都适用于 Windows、macOS 和 Linux。
要开发 iOS 移动应用程序,您需要一台 macOS 机器以便访问 Xcode 和 iOS 模拟器。以下是总结所有要求的表格:
书中涵盖的软件/硬件 | 操作系统要求 |
---|---|
Vue CLI 4.X | Windows / Linux / macOS |
TypeScript 3.9.X | Windows / Linux / macOS |
Quasar-CLI 1.X | Windows / Linux / macOS |
Nuxt-CLI 3.X.X | Windows / Linux / macOS |
Visual Studio Code 1.4.X 和 IntelliJ WebStorm 2020.2 | Windows / Linux / macOS |
Netlify-CLI | Windows / Linux / macOS |
Vercel-CLI | Windows / Linux / macOS |
Firebase-CLI | Windows / Linux / macOS |
Node.js 12+- | Windows / Linux / macOS |
Python 3 | Windows / Linux / macOS |
Xcode 11.4 和 iOS 模拟器 | macOS |
如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support注册,直接将文件发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
在www.packt.com上登录或注册。
-
选择“支持”选项卡。
-
单击“代码下载”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用以下最新版本解压缩或提取文件夹:
-
Windows 下的 WinRAR/7-Zip
-
Mac 下的 Zipeg/iZip/UnRarX
-
Linux 下的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Vue.js-3.0-Cookbook
。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有来自我们丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/
上找到。快来看看吧!
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。 例如:"将下载的WebStorm-10*.dmg
磁盘映像文件挂载为系统中的另一个磁盘。"
代码块设置如下:
<template>
<header>
<div id="blue-portal" />
</header>
</header>
任何命令行输入或输出都以以下方式编写:
$ npm run serve
粗体:表示新术语,重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。例如:"单击电子邮件按钮,将被重定向到电子邮件注册表单"
警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。
部分
在本书中,您会经常看到几个标题(准备工作,如何做,它是如何工作的,还有更多,和另请参阅)。
为了清晰地说明如何完成食谱,请按照以下部分使用这些部分:
准备工作
本节告诉您在食谱中可以期待什么,并描述如何设置食谱所需的任何软件或初步设置。
如何做…
本节包含了遵循食谱所需的步骤。
它是如何工作的…
本节通常包括对前一节发生的事情的详细解释。
还有更多…
本节包括有关食谱的其他信息,以使您对食谱更加了解。
另请参阅
本节为食谱提供了其他有用信息的链接。
第一章:了解 Vue 3 和创建组件
Vue 3 带来了许多新功能和变化,所有这些都旨在帮助开发并改善框架的整体稳定性、速度和可维护性。借鉴其他框架和库的灵感,Vue 核心团队设法在 API 上实现了很高的抽象水平,现在任何人都可以使用 Vue,无论他们是前端开发人员还是后端开发人员。
在本章中,我们将学习如何将我们的 Vue 项目升级到新版本,以及一些新的 Vue 功能,比如多个根元素,新的属性继承引擎,我们如何在另一个应用程序中使用暴露的响应性 API,以及如何使用新的组合 API 创建组件。
在本章中,您将学习以下内容:
-
Vue 3 有什么新功能
-
将您的 Vue 2 应用程序升级到 Vue 3
-
使用多个根元素创建组件
-
使用属性继承创建组件
-
在 Vue 范围之外使用响应性和可观察 API
-
使用组合 API 创建组件
Vue 3 有什么新功能
你可能想知道一个框架的新版本怎么会在互联网上引起如此大的轰动?想象一下把一辆汽车开上高速公路,做一个完整的 360 度翻滚,然后继续朝着同一个方向全速前进。这将引起一场戏剧性的场面,这正是描述 Vue 将从 2 版本升级到 3 版本的完美方式。
在本章的第一部分,我将向您介绍 Vue 的改进,框架中添加了什么,发生了什么变化,以及它将如何影响您编写 Vue 应用程序的方式。
框架的改进
在这个新版本中,Vue 框架有许多改进;所有这些改进都集中在尽可能使框架更好。以下是一些可能影响用户和开发人员日常开发和使用框架的改进。
底层
外壳看起来和旧的一样,但引擎是一件艺术品。在新版本中,没有留下来自 Vue 2 的代码。核心团队使用 TypeScript 从头开始构建了框架,并重写了一切,以最大程度地提高框架的性能。
选择了 TypeScript 来创建 Vue 核心团队和开源社区更易于维护的代码库,并改进自动完成功能,如 IDE 和代码编辑器提供的IntelliSense或typeahead,无需特殊的插件和扩展。
渲染引擎
对于 Vue 3,使用了一种新的算法开发了一个新的渲染引擎,用于影子 DOM。这个新的渲染引擎默认情况下完全暴露在框架的核心中,无需由框架执行。这使得可以实现一个全新的渲染函数的新实现,可以注入到框架中并替换原始的渲染引擎。
在这个新版本的 Vue 中,从头开始编写了一个新的模板编译器。这个新的编译器使用了一种新的缓存操作和管理渲染元素的新技术,并应用了一种新的提升方法来创建 VNodes。
对于缓存操作,应用了一种新的方法来控制元素的位置,其中元素可以是具有计算数据的动态元素,也可以是对可以被改变的函数的响应。
Vue 核心团队制作了一个浏览器,可以看到新模板编译器如何渲染最终的render
函数。可以在vue-next-template-explorer.netlify.app/
上查看。
暴露的 API
通过所有这些修改,可以在 Vue 应用程序范围之外的文件中渲染所有暴露给使用的 Vue API。可以在 React 应用程序中使用 Vue 响应性或影子 DOM,而无需在 React 应用程序内部渲染 Vue 应用程序。这种可扩展性是将 Vue 转变为更多功能的框架的一种方式,它可以在任何地方使用,不仅仅是在前端开发中。
新的自定义组件
Vue 3 引入了三个新的自定义组件,开发人员可以使用这些组件来解决旧问题。这些组件在 Vue 2 中也存在,但作为第三方插件和扩展。现在它们由 Vue 核心团队制作,并添加到 Vue 核心框架中。
片段
在 Vue 2 中,我们总是需要在单文件组件内部的组件周围有一个父节点。这是由于 Vue 2 的渲染引擎的构造方式所致,需要在每个节点上都有一个根元素。
在 Vue 2 中,我们需要有一个包装元素,封装将要呈现的元素。在这个例子中,我们有一个div
HTML 元素,包装了两个p
HTML 子元素,这样我们就可以在页面上实现多个元素:
<template>
<div>
<p>This is two</p>
<p>children elements</p>
</div> </template>
现在,在 Vue 3 中,可以在单文件组件中声明任意数量的根元素,而无需使用新的 Fragments API 特殊插件,它将处理多个根元素。这有助于为用户保持更清洁的最终代码,而无需为包装元素而创建空壳:
<template>
<p>This is two</p>
<p>root elements</p> </template>
正如我们在 Vue 3 代码中看到的,我们能够有两个根p
HTML 元素,而无需包装元素。
Teleport
Teleport
组件,也称为 Portal 组件,顾名思义,是一个可以使元素从一个组件移动到另一个组件的组件。这一开始可能看起来很奇怪,但它有许多应用,包括对话框、自定义菜单、警报、徽章和许多其他需要出现在特殊位置的自定义 UI。
想象一个标题组件,您希望在组件上放置一个自定义插槽,以便放置组件:
<template>
<header>
<div id="blue-portal" />
</header>
</header>
然后,您想在此标题上显示一个自定义按钮,但您希望从页面上调用此按钮。您只需要执行以下代码:
<template>
<page>
<Teleport to="blue-portal">
<button class="orange-portal">Cake</button>
</Teleport>
</page>
</template>
现在,您的按钮将显示在标题上,但代码将在页面上执行,从而访问页面范围。
悬念
当等待数据的时间比您想要的时间长时,如何为用户显示自定义加载程序?现在这是可能的,而无需自定义代码;Vue 将为您处理。Suspense
组件将管理此过程,在数据加载完成后显示默认视图,并在加载数据时显示备用视图。
您可以编写一个特殊的包装器,如下所示:
<template>
<Suspense>
<template #default>
<data-table />
</template>
<template #fallback>
<loading-gears />
</template>
</Suspense>
</template>
新的 Vue 组合 API 将了解组件的当前状态,因此它将能够区分组件是正在加载还是准备好显示。
API 更改
Vue 3 进行了一些 API 更改,这些更改是为了清理 Vue API 并简化开发而必要的。其中一些是破坏性的更改,另一些是新增的。但不用担心;Vue 2 对象开发并没有被移除,它仍然存在,并将继续使用。这种声明方法是许多开发人员选择 Vue 而不是其他框架的原因之一。
Vue 3 中将出现一些重要的变化,这些变化很重要,需要更多了解。我们将讨论 Vue 3 中将引入的最重要的变化,以及如何处理它们。
在 Vue 3 中,正在引入一种创建组件的新方法——组合 API。这种方法将使您的代码更易于维护,并为您提供更可靠的代码,您将拥有 TypeScript 的全部功能。
一些较小的变化
在 Vue 3 中存在一些较小的变化,需要提及。这些变化涉及我们以前用来编写代码的一种方法,现在在使用 Vue 3 时已经被替换。这并不是一项艰巨的工作,但您需要了解这些变化。
再见过滤器,你好过滤器!Vue 过滤器 API
在 Vue 2 中,我们使用filters
的方式已经不再可用。Vue 过滤器已从 API 中删除。这一变化是为了简化渲染过程并加快速度。最终,所有过滤器都是接收一个字符串并返回一个字符串的函数。
在 Vue 2 中,我们使用filters
如下:
{{ textString | filter }}
现在,在 Vue 3 中,我们只需要传递一个function
来操作string
:
{{ filter(textString) }}
公交车刚刚离开车站!事件总线 API
在 Vue 2 中,我们能够利用全局 Vue 对象的力量创建一个新的 Vue 实例,并使用这个实例作为一个事件总线,可以在组件和函数之间传输消息而不需要任何麻烦。我们只需要发布和订阅事件总线,一切都很完美。
这是在组件之间传输数据的一个好方法,但对于 Vue 框架和组件来说是一种反模式的方法。在 Vue 中,在组件之间传输数据的正确方式是通过父子通信或状态管理,也被称为状态驱动架构。
在 Vue 3 中,$on
、$off
和$once
实例方法已被移除。现在,要使用事件总线策略,建议使用第三方插件或框架,如 mitt(github.com/developit/mitt
)。
不再有全局 Vue——挂载 API
在 Vue 2 中,我们习惯于导入 Vue,并在挂载应用程序之前,使用全局 Vue 实例来添加plugins
、filters
、components
、router
和store
。这是一种很好的技术,我们可以向 Vue 实例添加任何内容,而无需直接附加到挂载的应用程序上。它的工作原理如下:
import Vue from 'vue';
import Vuex from 'vuex';
import App from './App.vue';
Vue.use(Vuex);
const store = new Vuex.store({});
new Vue({
store,
render: (h) => h(App),
}).$mount('#app');
现在,在 Vue 3 中,这是不再可能的。我们需要直接将每个component
、plugin
、store
和router
附加到挂载的实例上:
import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
const store = createStore({});
createApp(App)
.use(store)
.mount('#app');
使用这种方法,我们可以在同一个全局应用程序中创建不同的 Vue 应用程序,而不会相互干扰。
v-model,v-model,v-model - 多个 v-model
在开发单文件组件时,我们被限制为只能使用一个v-model
指令和一个.sync
选项来进行第二次更新更改。这意味着我们需要使用大量自定义事件发射器和巨大的对象负载来处理组件内的数据。
在这次重大变化中,引入了一个相关的破坏性变化,导致 Vue API 中的model
属性被移除。这个属性用于自定义组件,以前可以做与新的 v-model 指令现在所做的相同的事情。
使用v-model
指令的新方法将改变糖语法的工作方式。在 Vue 2 中,要使用v-model
指令,我们需要创建一个组件,期望接收props
为"value"
,当有变化时,我们需要发出一个'input'
事件,就像下面的代码:
<template>
<input
:value="value"
@input="$emit('input', $event)"
/>
</template>
<script>
export default {
props: {
value: String,
},
}
</script>
在 Vue 3 中,为了使语法糖工作,组件将接收的props
属性和事件发射器将发生变化。现在,组件期望一个名为modelValue
的props
,并发出一个名为'update:modelValue'
的事件,就像下面的代码:
<template>
<input
:modelValue="modelValue"
v-on:['update:modelValue']="$emit('update:modelValue', $event)"
/>
</template>
<script>
export default {
props: {
modelValue: String,
},
}
</script>
但是多个v-model
指令呢?理解v-model
的破坏性变化是了解多个v-model
新方法如何工作的第一步。
要创建多个v-model
组件,我们需要创建各种props
,并使用'update:value'
事件发出值作为模型指令的名称:
<script>
export default {
props: {
name: String,
email: String,
},
methods: {
updateUser(name, email) {
this.$emit('update:name', name);
this.$emit('update:email', email);
}
}
}
</script>
在我们想要使用多个v-model
指令的组件中,使用以下代码:
<template>
<custom-component
v-model:name="name"
v-model:email="email"
/>
</template>
组件将有每个v-model
指令,绑定到子组件正在发出的事件。在这种情况下,子组件发出'update:email'
(父组件)以便能够使用v-model
指令与 email 修饰符。例如,您可以使用v-model:email
来创建组件和数据之间的双向数据绑定。
组合 API
这是 Vue 3 最受期待的功能之一。组合 API 是创建 Vue 组件的一种新方式,以优化的方式编写代码,并在组件中提供完整的 TypeScript 类型检查支持。这种方法以更简单和更高效的方式组织代码。
在这种声明 Vue 组件的新方式中,你只需要一个setup
属性,它将被执行并返回组件执行所需的一切,就像这个例子:
<template>
<p @click="increaseCounter">{{ state.count }}</p> </template> <script> import { reactive, ref } from 'vue'; export default {
setup(){
const state = reactive({
count: ref(0)
}); const increaseCounter = () => {
state.count += 1;
} return { state, increaseCounter }
} } </script>
您将从 Vue 核心导入reactivity
API,以在对象类型数据属性中启用它,例如state
。ref
API 可以使基本类型值(如count
)具有反应性,它是一个数字。
最后,函数可以在setup
函数内部声明,并在返回的对象中传递。然后,所有内容都可以在<template>
部分中访问。
现在,让我们继续进行一些示例。
技术要求
在本章中,我们将使用Node.js和Vue-CLI。
注意 Windows 用户!您需要安装一个名为windows-build-tools
的 NPM 包,以便能够安装以下必需的包。为此,请以管理员身份打开 Power Shell 并执行以下命令:
> npm install -g windows-build-tools
要安装 Vue-CLI,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
创建基本文件
在本章的所有示例中,我们将使用这个基本模板,现在我们将创建它。确保在开始示例之前按照以下步骤创建文件:
-
在任何文件夹中创建一个新的
.html
文件并打开它。 -
创建一个
html
标签,并添加一个head
HTML 元素作为子元素。在head
HTML 元素内部,添加一个带有src
属性定义为http://unpkg.com/vue@next
的script
HTML 元素:
<html> <head>
<script src="https://unpkg.com/vue@next"></script>
</head>
</html>
- 作为
head
HTML 元素的同级,创建一个body
HTML 元素。在body
HTML 元素内部,添加一个带有属性id
定义为"app"
的div
HTML 元素:
<body>
<div id="app">
</div>
</body>
- 最后,作为
div
HTML 元素的同级,创建一个带有空内容的script
HTML 元素。这将是我们放置示例代码的地方:
<script></script>
将您的 Vue 2 应用程序升级到 Vue 3
将您的项目从 Vue 2 升级到 Vue 3 有时可以自动完成,但在其他情况下,需要手动完成。这取决于您在应用程序中使用 Vue API 的深度。
对于由 Vue-CLI 制作和管理的项目,这个过程将变得更加顺畅,并且与使用自定义框架包装 CLI 的项目相比,将有更加简单的方法。
在这个食谱中,您将学习如何使用 Vue-CLI 升级您的应用程序以及如何手动升级项目和依赖项。
准备工作
这个食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
为了将您的 Vue 2 项目升级到 Vue 3,您将需要将升级分为不同的部分。我们有框架本身的升级,然后是生态系统组件,比如vue-router
和vuex
,最后是将所有内容汇总的捆绑器。
框架升级带来了一些破坏性的变化。本章的Vue 3 中的新内容部分介绍了一些破坏性的变化,还有一些可能出现在更高级的 API 模式中。您必须手动更新并检查您的组件是否适用于框架的升级。
使用 Vue-CLI 升级项目
使用最新版本的 Vue-CLI,您将能够在项目中使用 Vue 3,并且您将能够将当前项目更新到 Vue 3。
要将 Vue-CLI 更新到最新版本,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install @vue/cli-service@latest
手动升级项目
要手动升级项目,您首先需要将项目依赖项升级到它们的最新版本。您不能在 Vue 3 中使用旧版本的 Vue 生态系统插件。要做到这一点,请执行以下步骤:
- 我们需要升级 Vue 框架、ESLint 插件(Vue 依赖的插件)和捆绑器的
vue-loader
。要升级它,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install vue@next eslint-plugin-vue@next vue-loader@next
- 我们需要将新的 Vue 单文件组件编译器作为项目的依赖项添加进去。要安装它,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install @vue/compiler-sfc@latest
- 如果您在项目中使用单元测试和
@vue/test-utils
包,您还需要升级此依赖项。要升级它,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install @vue/test-utils@next @vue/server-test-utils@latest
- 对于 Vue 生态系统插件,如果你使用
vue-router
,你也需要升级它。要升级它,你需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install vue-router@next
- 如果你的应用程序使用
vuex
作为默认状态管理,你也需要升级它。要升级它,你需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install vuex@next
更改起始文件
使用新版本的包,我们需要改变我们的起始文件。在使用 Vue-CLI 起始工具创建的 Vue 项目中,你会找到一个名为main.js
或main.ts
的文件。如果你使用 TypeScript,该文件位于src
文件夹中。现在按照以下说明进行操作:
- 打开项目中
src
文件夹中的main.js
文件。在文件顶部,导入包的位置,你会看到以下代码:
import Vue from 'vue';
我们需要将其更改为新的 Vue 暴露的 API 方法。为此,我们需要从 Vue 包中导入createApp
,如下所示:
import { createApp } from 'vue';
-
从你的代码中移除全局 Vue 静态属性定义的
Vue.config.productionTip
。 -
应该改变你的应用程序的挂载函数。旧的 API 看起来像这样:
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
旧的 API 应该改为新的createApp
API,如下所示:
createApp(App)
.use(router)
.use(store)
.mount('#app')
-
打开你的
vuex
存储实例化文件(通常,该文件位于src/store
,命名为store.js
或index.js
)。 -
将存储的创建从实例化一个新的
vuex
类改为新的createStore
API。vuex
v3 类的实例化可能看起来像这样:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: { /* ... */ },
mutations: { /* ... */ },
actions: { /* ... */ },
getters: { /* ... */ },
modules: { /* ... */ },
});
你需要用createStore
API 替换它的内容,例如:
import { createStore } from 'vuex';
export default createStore({
state: { /* ... */ },
mutations: { /* ... */ },
actions: { /* ... */ },
getters: { /* ... */ },
modules: { /* ... */ },
});
-
在
vue-router
生态系统中,我们需要用新的 API 替换路由器创建的旧 API。为此,打开路由器创建文件(在src/router
文件夹中,通常命名为router.js
或index.js
)。 -
最后,在创建文件中,用新的
createRouter
API 替换旧的vue-router
类实例化。vue-router
v3 类的实例化可能看起来像这样:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
export default new VueRouter({
routes: [{
path: '/',
name: 'HomePage',
component: () => import('pages/home'),
}]
});
你还需要用新的createRouter
和createWebHistory
API 替换new VueRouter
的实例化,就像这个例子一样:
import {
createRouter,
createWebHistory,
} from 'vue-router';
Vue.use(VueRouter);
export default createRouter({
history: createWebHistory(),
routes: [{
path: '/',
name: 'HomePage',
component: () => import('pages/home'),
}]
});
它是如何工作的...
在升级过程中,Vue 为我们提供了两种更新项目的方式。第一种方式是使用 Vue-CLI 插件,它试图自动化几乎所有升级所需的过程和更改。
第二种方法是手动升级项目。这种方法需要开发人员将所有依赖项升级到最新版本,安装新的单文件组件编译器@vue/compiler-sfc
,并将 Vue 应用程序、路由器和存储的入口文件更改为新的 API。
在项目的起始结构更改后,开发人员需要检查组件,看看是否存在任何 Vue 3 破坏性更改,将组件重构为新的 Vue 3 API,并从 Vue 2 中删除已弃用的 API。
创建具有多个根元素的组件
在 Vue 3 中,可以创建具有多个根元素的组件,无需包装元素。这个选项也被称为片段。
在 React 中,这已经很久了,但在 Vue 中,您需要使用自定义的第三方插件,如vue-fragment
(github.com/Thunberg087/vue-fragment
)来使用此功能。
在这个教程中,您将学习如何创建一个具有多个根元素的组件,以及如何将其与<template>
部分和render
函数一起使用。
如何做...
在这个教程中,我们将创建两个多个根元素组件的示例,一个是使用<template>
结构,另一个是使用render
函数。为了做到这一点,这个教程将分为两部分。
使用结构创建组件
为了在我们的示例中使用<template>
结构,我们将使用 Vue 对象的template
属性,我们可以将字符串或模板字符串作为值传递,这将由 Vue 脚本插值并呈现在屏幕上:
-
使用“创建基本文件”部分的基本示例,创建一个名为template.html
的新文件并打开它。
-
在空的<script>
HTML 元素中,通过对象解构Vue
全局常量,创建常量defineComponent
和createApp
:
const {
defineComponent, createApp, } = Vue;
- 创建一个名为
component
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,其中有三个属性:data
、methods
和template
:
const component = defineComponent({
data: () => ({}),
methods: {},
template: `` });
- 在
data
属性中,将其定义为一个单例函数,返回一个 JavaScript 对象,其中有一个名为count
的属性,并且默认值为0
:
data: () => ({
count: 0 }),
- 在
methods
属性中,创建一个名为addOne
的属性,这是一个函数,将通过1
增加count
的值:
methods: {
addOne() {
this.count += 1;
}, },
- 在
template
属性中,在模板字符串中,创建一个带有标题的h1
HTML 元素。然后,作为兄弟元素,创建一个带有绑定到click
事件的事件监听器的button
HTML 元素,当执行时触发addOne
函数:
template: `
<h1> This is a Vue 3 Root Element! </h1>
<button @click="addOne"> Pressed {{ count }} times. </button> `
- 最后,调用
createApp
函数,将component
常量作为参数传递。然后,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:
createApp(component)
.mount('#app');
使用渲染函数创建组件
为了在我们的示例中使用<template>
结构,我们将使用 Vue 对象的template
属性,我们可以将字符串或模板字符串作为值传递,Vue 脚本将对其进行插值处理并在屏幕上呈现:
-
使用“创建基本文件”部分的基本示例,创建一个名为render.html
的新文件并打开它。
-
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,从Vue
全局常量中调用defineComponent
、h
和createApp
方法:
const {
defineComponent,
h, createApp, } = Vue;
- 创建一个名为
component
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,该对象有三个属性:data
、methods
和render
:
const component = defineComponent({
data: () => ({}),
methods: {},
render() {}, });
- 在
data
属性中,将其定义为一个单例函数,返回一个具有名为count
且默认值为0
的 JavaScript 对象:
data: () => ({
count: 0 }),
- 在
methods
属性中,创建一个名为addOne
的属性,它是一个函数,将count
的值增加1
:
methods: {
addOne() {
this.count += 1;
}, },
- 在
render
属性中,执行以下步骤:
-
创建一个名为h1
的常量,并将其定义为h
函数,将'h1'
作为第一个参数传递,将要使用的标题作为第二个参数。
-
创建一个名为button
的常量,它将是h
函数,将"button"
作为第一个参数传递,将一个具有onClick
属性且值为this.addOne
的 JavaScript 对象作为第二个参数传递,将button
的内容作为第三个参数。
-
返回一个数组,第一个值为h1
常量,第二个值为button
常量:
render() {
const h1 = h('h1', 'This is a Vue 3 Root Element!');
const button = h('button', {
onClick: this.addOne,
}, `Pressed ${this.count} times.`); return [
h1,
button,
]; },
- 最后,调用
createApp
函数,将component
常量作为参数传递,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:
createApp(component)
.mount('#app');
工作原理...
新的 Vue 组件创建 API 需要由一个函数defineComponent
执行,并且作为参数传递的 JavaScript 对象几乎保持与 Vue 2 中的旧结构相同。在示例中,我们使用了相同的属性data
、render
、methods
和template
,这些属性都存在于 Vue 2 中。
在具有<template>
结构的示例中,我们不必创建包装元素来封装应用程序组件的内容,并且可以直接在组件上有两个根元素。
在render
函数示例中,发生了相同的行为,但最终示例使用了新的暴露的h
API,它不再是render
函数的参数。在按钮创建中出现了一个重大变化;我们必须在数据 JavaScript 对象内部使用onClick
属性,而不是on
属性和click
方法。这是因为 Vue 3 的 VNode 的新数据结构。
使用属性继承创建组件。
自 Vue 2 以来,组件上已经可以使用属性继承,但在 Vue 3 中,属性继承变得更好,并且具有更可靠的 API 可用于组件中。
组件中的属性继承是一种模式,它可以更快地开发基于 HTML 元素的自定义组件(例如自定义输入、按钮、文本包装器或链接)。
在这个示例中,我们将创建一个具有属性继承的自定义输入组件,直接应用于input
HTML 元素。
如何做...
在这里,我们将创建一个组件,该组件将在 DOM 树上的选定元素上具有完整的属性继承:
-
使用创建基本文件部分的基本示例,创建一个名为component.html
的新文件并打开它。
-
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,调用Vue
全局常量的defineComponent
和createApp
方法:
const {
defineComponent, createApp, } = Vue;
- 创建一个名为
nameInput
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,具有四个属性:name
、props
、template
和inheritAttrs
。然后,我们将inheritAttrs
的值定义为false
:
const nameInput = defineComponent({
name: 'NameInput',
props: {},
inheritAttrs: false,
template: `` });
- 在
props
属性中,添加一个名为modelValue
的属性,并将其定义为String
:
props: {
modelValue: String, },
- 在模板属性中,在模板字符串内部,我们需要执行以下操作:
-
创建一个label
HTML 元素,并将一个input
HTML 元素作为子元素添加。
-
在input
HTML 元素中,将v-bind
指令定义为一个 JavaScript 对象,其中包含this.$attrs
的解构值。
-
将变量属性value
定义为接收到的 prop 的modelValue
。
-
将input
属性type
设置为"text"
。
-
将匿名函数添加到change
事件监听器中,该函数接收一个event
作为参数,然后发出一个名为"update:modeValue"
的事件,载荷为event.target.value
:
template: ` <label>
<input
v-bind="{ ...$attrs, }"
:value="modelValue" type="text" @change="(event) => $emit('update:modelValue',
event.target.value)"
/> </label>`
- 创建一个名为
appComponent
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,其中包含两个属性,data
和template
:
const component = defineComponent({
data: () => ({}),
template: ``, });
- 在
data
属性中,将其定义为一个单例函数,返回一个具有名为name
的属性的 JavaScript 对象,其默认值为''
:
data: () => ({
name: '' }),
- 在模板属性中,在模板字符串中,我们需要执行以下操作:
-
创建一个NameInput
组件,其中v-model
指令绑定到name
数据属性。
-
创建一个带有值"border:0; border-bottom: 2px solid red;"
的style
属性。
-
创建一个带有值"name-input"
的data-test
属性:
template: ` <name-input
v-model="name" style="border:0; border-bottom: 2px solid red;"
data-test="name-input" />`
- 创建一个名为
app
的常量,并将其定义为createApp
函数,将component
常量作为参数传递。然后,调用app.component
函数,将要注册的组件的名称作为第一个参数传递,组件作为第二个参数传递。最后,调用app.mount
函数,将"#app"
作为参数传递:
const app = createApp(component); app.component('NameInput', nameInput); app.mount('#app');
工作原理...
在 Vue 3 中,为了创建一个组件,我们需要执行defineComponent
函数,传递一个 JavaScript 对象作为参数。这个对象保持了几乎与 Vue 2 相同的组件声明结构。在示例中,我们使用了相同的属性,data
,methods
,props
和template
,这些属性都存在于 V2 中。
我们使用inheritAttrs
属性来阻止将属性自动应用于组件上的所有元素,仅将其应用于具有v-bind
指令和解构this.$attrs
对象的元素。
要在 Vue 应用程序中注册组件,我们首先使用createApp
API 创建应用程序,然后执行app.component
函数在应用程序上全局注册组件,然后渲染我们的应用程序。
在 Vue 范围之外使用响应性和可观察 API
在 Vue 3 中,使用暴露的 API,我们可以在不需要创建 Vue 应用程序的情况下使用 Vue reactivity 和 reactive 变量。这使得后端和前端开发人员可以充分利用 Vue reactivity
API。
在这个示例中,我们将使用reactivity
和watch
API 创建一个简单的 JavaScript 动画。
如何做...
在这里,我们将使用 Vue 暴露的reactivity
API 创建一个应用程序,以在屏幕上呈现动画:
-
使用“创建基本文件”部分中的基本示例,创建一个名为reactivity.html
的新文件并打开它。
-
在<head>
标签中,添加一个新的<meta>
标签,属性为chartset
定义为"utf-8"
:
<meta charset="utf-8"/>
- 在
<body>
标签中,删除div#app
HTML 元素,并创建一个div
HTML 元素,id
定义为marathon
,style
属性定义为"font-size: 50px;"
:
<div
id="marathon"
style="font-size: 50px;" > </div>
- 在空的
<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,调用Vue
全局常量中的reactivity
和watch
方法:
const {
reactive,
watch, } = Vue;
- 创建一个名为
mod
的常量,定义为一个函数,接收两个参数a
和b
。然后返回一个算术运算,a
模b
:
const mod = (a, b) => (a % b);
- 创建一个名为
maxRoadLength
的常量,其值为50
。然后,创建一个名为competitor
的常量,其值为reactivity
函数,传递一个 JavaScript 对象作为参数,其中position
属性定义为0
,speed
定义为1
:
const maxRoadLength = 50; const competitor = reactive({
position: 0,
speed: 1, });
- 创建一个
watch
函数,传递一个匿名函数作为参数。在函数内部,执行以下操作:
-
创建一个名为street
的常量,并将其定义为一个大小为maxRoadLength
的Array
,并用*'_'*
.填充它。
-
创建一个名为marathonEl
的常量,并将其定义为 HTML DOM 节点#marathon
。
-
选择数组索引中competitor.position
的street
元素,并将其定义为*"![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/c8b07311-36a4-4df3-98fd-3b68200deed3.png)"*
,如果competitor.position
是偶数,或者如果数字是奇数,则定义为*"![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/562ed724-a630-4193-a9c6-4e143a9690e2.png)"*
。
-
将marathonEl.innertHTML
定义为*""*
和street.reverse().join('')
:
本示例中使用的表情符号是跑步的人和行走的人。表情符号图像可能因您的操作系统而异。本示例中呈现的图像是苹果操作系统的表情符号。
watch(() => {
const street = Array(maxRoadLength).fill('_');
const marathonEl = document.getElementById('marathon');
street[competitor.position] = (competitor.position % 2 === 1)
? '![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/c8b07311-36a4-4df3-98fd-3b68200deed3.png)'
: '![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/562ed724-a630-4193-a9c6-4e143a9690e2.png)'; marathonEl.innerHTML = '';
marathonEl.innerHTML = street.reverse().join(''); });
- 创建一个
setInterval
函数,将一个匿名函数作为参数传递。在函数内部,将competitor.position
定义为mod
函数,将competitor.position
加上competitor.speed
作为第一个参数,将maxRoadLength
作为第二个参数:
setInterval(() => {
competitor.position = mod(competitor.position +competitor.speed,
maxRoadLength) }, 100);
它是如何工作的...
使用 Vue 暴露的reactive
和watch
API,我们能够创建一个具有 Vue 框架中的响应性的应用程序,但不使用 Vue 应用程序。
首先,我们创建了一个响应式对象competitor
,它的工作方式与 Vue 的data
属性相同。然后,我们创建了一个watch
函数,它的工作方式与watch
属性相同,但是作为匿名函数使用。在watch
函数中,我们为竞争者开辟了一条跑道,并创建了一个简单的动画,使用两个不同的表情符号,根据在道路上的位置进行更改,以模拟屏幕上的动画。
最后,我们在屏幕上打印了当前的跑步者,并创建了一个setInterval
函数,每 100 毫秒改变竞争者在道路上的位置:
使用组合 API 创建组件
组合 API 是一种编写 Vue 组件的新方法,基于使用函数来组合组件,它使代码的组织和可重用性更好。
这种方法受到 React Hooks 的启发,并引入了创建特殊函数来组合应用程序的技术,这些函数可以在不需要在 Vue 应用程序内部的情况下共享,因为使用了暴露的 Vue API。
在这个示例中,我们将学习如何创建一个外部函数,用于获取用户的地理位置并在屏幕上显示这些数据,使用组合 API。
如何做...
在这里,我们将使用组合 API 创建一个组件,该组件将获取用户的 GPS 位置并在屏幕上显示该信息:
-
使用“创建基本文件”部分的基本示例,创建一个名为component.html
的新文件并打开它。
-
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,从Vue
全局常量中调用createApp
、defineComponent
、setup
、ref
、onMounted
和onUnmounted
方法:
const {
createApp,
defineComponent,
setup,
ref,
onMounted,
onUnmounted, } = Vue;
- 创建一个
fetchLocation
函数,在其中创建一个名为watcher
的let
变量。然后,创建一个名为geoLocation
的常量,并将其定义为navigator.geolocation
。接下来,创建一个名为gpsTime
的常量,并将其定义为ref
函数,将Date.now()
函数作为参数传递。最后,创建一个名为coordinates
的常量,并将其定义为ref
函数,将一个 JavaScript 对象作为参数传递,其中的属性accuracy
、latitude
、longitude
、altitude
、altitudeAccuracy
、heading
和speed
都定义为0
:
function fetchLocation() {
let watcher;
const geoLocation = navigator.geolocation;
const gpsTime = ref(Date.now());
const coordinates = ref({
accuracy: 0,
latitude: 0,
longitude: 0,
altitude: 0,
altitudeAccuracy: 0,
heading: 0,
speed: 0,
}); }
- 然后,在
fetchLocation
函数内部,在常量创建之后,创建一个名为setPosition
的函数,带有一个名为payload
的参数。在函数内部,将gpsTime.value
定义为payload.timestamp
参数,将coordinates.value
定义为payload.coords
参数:
function setPosition(payload) {
gpsTime.value = payload.timestamp
coordinates.value = payload.coords
}
- 在创建
setPosition
函数之后,调用onMounted
函数,将一个匿名函数作为参数传递。在函数内部,检查浏览器是否可用geoLocation
API,并将watcher
定义为geoLocation.watchPostion
函数,将setPosition
函数作为参数传递:
onMounted(() => {
if (geoLocation) watcher = geoLocation.watchPosition(setPosition); });
- 调用
onMounted
函数后,创建一个onUnmounted
函数,将一个匿名函数作为参数传递。在函数内部,检查watcher
是否已定义,然后执行geoLocation.clearWatch
函数,将watcher
作为参数传递:
onUnmounted(() => {
if (watcher) geoLocation.clearWatch(watcher); });
- 最后,在
fetchLocation
函数中,返回一个 JavaScript 对象,并将coordinates
和gpsTime
常量作为属性/值传递:
return {
coordinates,
gpsTime, };
- 创建一个名为
appComponent
的常量,并将其定义为defineComponent
函数,将一个具有setup
和template
属性的 JavaScript 对象作为参数传递:
const appComponent = defineComponent({
setup() {},
template: `` });
- 在
setup
函数中,创建一个常量,这是一个对象解构,包括fetchLocation
函数的coordinates
和gpsTime
属性:
setup() {
const {
coordinates,
gpsTime,
} = fetchLocation(); }
- 在
setup
函数内部,创建另一个名为formatOptions
的常量,并将其定义为一个具有year
、month
、day
、hour
和minute
属性的 JavaScript 对象,其值均为'numeric'
。然后,将属性hour12
定义为true
:
const formatOptions = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
};
- 在创建
formatOptions
常量之后,创建一个名为formatDate
的常量,并将其定义为一个函数,该函数接收一个名为date
的参数。然后,返回一个新的Intl.DateTimeFormat
函数,将navigator.language
作为第一个参数,将formatOption
常量作为第二个参数。然后,原型链连接format
函数,传递date
参数:
const formatDate = (date) => (new
Intl.DateTimeFormat(navigator.language,
formatOptions).format(date));
- 最后,在
setup
函数的末尾,返回一个 JavaScript 对象,其属性定义为coordinates
、gpsTime
和formatDate
常量:
return {
coordinates,
gpsTime,
formatDate };
- 在
template
属性中,进行以下操作:
-
创建一个带有文本“我的地理位置在{{ formatDate(new Date(gpsTime) }}”的h1
HTML 元素。
-
创建一个ul
HTML 元素,并添加三个li
HTML 元素作为子元素。
-
在第一个子元素中,添加文本“纬度:{{ coordinates.latitude }}”。
-
在第二个子元素中,添加文本“经度:{{ coordinates.longitude }}”。
-
在第三个子元素中,添加文本“海拔:{{ coordinates.altitude }}”:
template: `
<h1>My Geo Position at {{formatDate(new
Date(gpsTime))}}</h1>
<ul>
<li>Latitude: {{ coordinates.latitude }}</li>
<li>Longitude: {{ coordinates.longitude }}</li>
<li>Altitude: {{ coordinates.altitude }}</li>
</ul> `
- 最后,调用
createApp
函数,传递appComponent
常量作为参数。然后,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:
createApp(appComponent)
.mount('#app');
它是如何工作的...
在这个示例中,首先我们导入了暴露的 API - createApp
、defineComponent
、setup
、ref
、onMounted
和onUnmounted
- 作为常量,我们将使用它们来创建组件。然后,我们创建了fetchLocation
函数,它负责获取用户的地理位置数据,并将其作为响应式数据返回,当用户更改位置时可以自动更新。
能够获取用户 GPS 位置是因为现代浏览器上存在的navigator.geolocation
API,它能够获取用户当前的 GPS 位置。利用浏览器提供的数据,我们能够用它来定义由 Vue ref
API 创建的变量。
我们使用 Vue 对象声明的setup
函数创建了组件,因此渲染知道我们正在使用新的组合 API 作为组件创建方法。在setup
函数内部,我们导入了fetchLocation
函数的动态变量,并创建了一个方法,用于在模板上使用日期格式化。
然后我们返回导入的变量和过滤器,以便它们可以在模板部分中使用。在模板部分中,我们创建了一个标题,添加了最后一次 GPS 位置的时间,使用过滤器进行格式化,并创建了用户的纬度、经度和海拔的列表。
最后,我们使用createApp
公开的 API 创建了应用程序,并挂载了 Vue 应用程序。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation
找到有关Navigator.geolocation
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。
第二章:介绍 TypeScript 和 Vue 生态系统
TypeScript 是一种基于 Vue 的新语言,在Vue 3上得到了充分支持。现在可以使用类型化的 JSX(也称为 TSX),类型注解,代码的静态验证等等。
Vue 生态系统每天都在变得更加庞大,为了帮助我们,Vue 团队开发了一些工具来改善项目处理和管理。这些工具是 Vue CLI 和 Vue UI,它们是本地 Vue 开发的主要工具。
Vue CLI 工具是每个项目的开始;通过它,你可以选择基本功能或者你之前创建的预设,来创建一个新的 Vue 项目。项目创建后,你可以使用 Vue UI 来管理项目,添加新功能,检查项目的状态,以及几乎可以在命令行界面(CLI)中做的所有事情,还有更多功能。
在这些章节中,你将更多地了解 TypeScript 作为 JavaScript 的扩展,以及如何使用 Vue CLI 工具和 Vue UI 一起来启动和运行整个应用程序。
在本章中,我们将涵盖以下内容:
-
创建一个 TypeScript 项目
-
理解 TypeScript
-
创建你的第一个 TypeScript 类
-
使用 Vue CLI 创建你的第一个项目
-
使用 Vue UI 向 Vue CLI 项目添加插件
-
将 TypeScript 添加到 Vue CLI 项目中
-
使用vue-class-component
创建你的第一个 TypeScript Vue 组件
-
使用vue-class-component
创建自定义 mixin
-
使用vue-class-component
创建自定义函数装饰器
-
将自定义钩子添加到vue-class-component
-
将vue-property-decorator
添加到vue-class-component
技术要求
在本章中,我们将使用Node.js,Vue CLI和TypeScript。
注意,Windows 用户需要安装一个名为windows-build-tools
的 npm 包,以便安装以下所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行以下命令:
> npm install -g windows-build-tools
。
要安装Vue CLI工具,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
要安装TypeScript,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),执行以下命令:
> npm install -g typescript
创建一个 TypeScript 项目
TypeScript 是 JavaScript 的类型扩展,在编译时会生成纯 JavaScript 代码。它看起来像是一种新语言,但最终还是 JavaScript。
使用 TypeScript 的优势是什么?主要优势在于类型化的语法,有助于静态检查和代码重构。您仍然可以使用所有 JavaScript 库,并且可以直接使用最新的 ECMAScript 功能进行编程。
编译后,TypeScript 将生成一个纯 JavaScript 文件,可以在任何浏览器、Node.js 或任何能够执行 ECMAScript 3 或更新版本的 JavaScript 引擎上运行。
准备工作
要启动我们的项目,我们需要创建一个npm
项目。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm init -y
您还需要安装 TypeScript,因此打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install typescript --only=dev
如何操作...
环境准备就绪后,我们需要启动我们的 TypeScript 项目。让我们创建一个.ts
文件并进行编译:
- 要启动我们的 TypeScript 项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> tsc --init
这将在我们的文件夹中创建一个tsconfig.json
文件。这是一个编译器设置文件。在这里,您可以定义目标,开发中可用的 JavaScript 库,目标 ECMAScript 版本,模块生成等等。
在为 Web 开发时,不要忘记在tsconfig.json
文件的compilerOption
属性中添加文档对象模型(DOM)库,这样在开发时就可以访问 window 和 document 对象。
- 现在,我们需要创建我们的
index.ts
文件。让我们在index.ts
文件中创建一些简单的代码,以便在终端中记录一个数学计算:
function sum(a: number, b: number): number {
return a + b;
}
const firstNumber: number = 10;
const secondNumber: number = 20;
console.log(sum(firstNumber, secondNumber));
这个函数接收两个参数,a
和b
,它们的类型都设置为number
,并且函数预计返回一个number
。我们创建了两个变量,firstNumber
和secondNumber
,在这种情况下都设置为number
类型——分别是10
和20
,因此,将它们传递给函数是有效的。如果我们将它们设置为其他类型,比如字符串、布尔值、浮点数或数组,编译器会在变量和函数执行的静态类型检查方面抛出错误。
- 现在,我们需要将这段代码编译成 JavaScript 文件。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> tsc ./index.ts
编译后,我们可以在index.js
中看到最终文件。如果我们查看文件内部,最终代码将类似于这样:
function sum(a, b) {
return a + b;
}
var firstNumber = 10;
var secondNumber = 20;
console.log(sum(firstNumber, secondNumber));
你可能会想:“我的类型在哪里?”由于 ECMAScript 是一种动态语言,TypeScript 的类型只存在于超集级别,并不会传递到 JavaScript 文件中。
你的最终 JavaScript 文件将以转译文件的形式存在,其中包含在tsconfig.json
文件中定义的配置。
它是如何工作的...
当我们创建 TypeScript 项目时,一个名为tsconfig.json
的文件会在我们的文件夹中创建。这个文件协调了编译器和开发过程中的静态类型检查的所有规则。所有的开发都基于这个文件中定义的规则。每个环境都依赖于需要导入的特定规则和库。
在开发过程中,我们可以直接为常量、变量、函数参数、返回值等分配类型。这些类型定义可以防止基本类型错误和代码重构。
开发完成并编译项目后,最终产品将是一个纯 JavaScript 文件。由于 JavaScript 的动态类型,这个文件不会有任何类型检查。
这个 JavaScript 文件被转译成目标模型,并在配置文件中定义,所以我们可以无问题地执行它。
另请参阅
您可以在www.typescriptlang.org/docs/home.html
找到有关 TypeScript 的更多信息。
有一个关于从 JavaScript 迁移的指南在www.typescriptlang.org/docs/handbook/migrating-from-javascript.html
。
可以在www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
找到一个关于 TypeScript 的 5 分钟课程。
了解 TypeScript
TypeScript 是一种基于类型的语言。它的很多功能来自于能够使用静态代码分析与 JavaScript。这得益于存在于 TypeScript 环境内的工具。
这些工具包括编译器,在开发过程中和编译后可以提供静态分析,以及 ECMAScript 转译器,可以使您的代码在几乎任何 JavaScript 引擎上运行。
让我们更多地了解这种语言,以及它是如何工作的。
准备好了吗?
首先,我们需要创建一个npm
项目。打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:
> npm init -y
您还需要安装 TypeScript,因此打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:
> npm install typescript --only=dev
类型
使用 TypeScript 的主要特性是类型。在本节中,我们将学习有关类型的知识,如何声明它们以及如何使用它们。
这些是静态类型语言中的一些基本类型:
-
字符串
-
数字
-
布尔
-
数组
-
元组
-
枚举
-
任意
-
空
-
对象
让我们谈谈其中一些类型,并展示它们在 TypeScript 中的使用方式。
字符串
JavaScript 中的所有文本数据都将被视为字符串。要声明一个字符串,我们总是需要用双引号(")
或单引号(')
括起来,或者用反引号(
)`,通常称为模板字符串。
在文本中声明模板字符串对 TypeScript 来说不是问题。模板字符串是 ECMAScript 中的一个功能,它使得可以在字符串中添加变量而无需进行连接:
const myText: string = 'My Simple Text';
const myTextAgain: string = "My Simple Text";
const greeting: string = `Welcome back ${myName}!`;
数字
在 JavaScript 中,所有数字都是浮点值。在 TypeScript 中也是如此。这些数字得到了数字类型。除了十六进制和十进制数字外,ECMAScript 2015 中引入的二进制和八进制字面量也被视为数字:
const myAge: number = 31;
const hexNumber: number = 0xf010d;
const binaryNumber: number = 0b1011;
const octalNumber: number = 0o744;
布尔
编程语言中最基本的类型是布尔值——简单的 1 或 0,true 或 false。这被称为布尔:
const isTaskDone: boolean = false;
const isGreaterThen: boolean = 10 > 5;
数组
大多数语言中的一组元素通常被称为数组。在 TypeScript 中,我们可以以两种不同的方式声明它。
最简单的方法就是声明元素的类型,后面跟着[]
(方括号)来表示它是一个声明类型的数组:
const primeNumbers: number[] = [1, 3, 5, 7, 11];
或者,您可以使用Array<type>
声明进行通用声明。这不是最常用的方式,但根据您正在开发的代码,您可能需要使用它:
const switchInstructions: Array<boolean> = [true, false, false, true];
元组
元组是一种具有特定结构的变量类型。在结构上,元组是一个包含两个元素的数组;这两个元素由编译器和用户知道其类型,但这些元素不需要具有相同的类型:
let person: [string, number];
person = ['Heitor', 31];
console.log(`My name is ${person[0]} and I am ${person[1]} years old`);
如果尝试访问已知索引之外的元素,将会收到错误。
枚举
枚举类似于 JavaScript 对象,但它们具有一些特殊的属性,可以帮助开发应用程序。您可以为数字值设置友好的名称,或者为函数可以接受的变量的常量提供更受控制的环境。
可以创建一个数字枚举而不需要任何声明。通过这样做,它将从0
的初始值开始,并以最终索引号的值结束;或者,您可以通过传递枚举值的索引来获取枚举的名称:
enum ErrorLevel {
Info,
Debug,
Warning,
Error,
Critical,
}
console.log(ErrorLevel.Error); // 3
console.log(ErrorLevel[3]); // Error
或者,可以声明一个带有值的枚举。它可以是 TypeScript 编译器将解释其余元素作为第一个元素的增量,或者是一个单独的声明:
enum Color {
Red = '#FF0000',
Blue = '#0000FF',
Green = '#00FF00',
}
enum Languages {
JavaScript = 1,
PHP,
Python,
Java = 10,
Ruby,
Rust,
TypeScript,
}
console.log(Color.Red) // '#FF0000'
console.log(Languages.TypeScript) // 13
任意
由于 JavaScript 是一种动态语言,TypeScript 需要实现一个没有定义值的类型,因此它实现了any类型。any 类型最常用的情况是在使用来自第三方库的值时。在这种情况下,我们知道我们正在放弃类型检查:
let maybeIs: any = 4;
maybeIs = 'a string?';
maybeIs = true;
任何类型的主要用途是当您将传统 JavaScript 项目升级到 TypeScript 时,您可以逐渐向变量和函数添加类型和验证。
空
与 any 相反,void是完全没有类型的。最常用的情况是在不返回任何值的函数中:
function logThis(str: string): void{
console.log(str);
}
使用 void 来对变量进行类型设置是没有意义的,因为它只能被赋值为 undefined 和 null。
对象
TypeScripts 中的对象有一种特殊的声明形式,因为它可以声明为接口,作为直接的对象,或者作为自己的类型。
将对象声明为接口时,您必须在使用之前声明接口,必须传递所有属性,并且需要设置类型:
interface IPerson {
name: string;
age: number;
}
const person: IPerson = {
name: 'Heitor',
age: 31,
};
在将对象作为直接输入传递给函数时,有时是常见的:
function greetingUser(user: {name: string, lastName: string}) {
console.log(`Hello, ${user.name} ${user.lastName}`);
}
最后,它们用于声明对象的类型并重用它:
type Person = {
name: string,
age: number,
};
const person: Person = {
name: 'Heitor',
age: 31,
};
console.log(`My name is ${person.name}, I am ${person.age} years old`);
函数
在 TypeScript 中,最难声明的类型之一是函数。它可以在一个简单的函数链的连接中变得非常复杂。
在 TypeScript 中声明函数是函数将接收的参数和函数将返回的最终类型的组合。
你可以在常量内声明一个简单的函数,就像这样:
const sumOfValues: (a:number, b:number): number = (a: number, b: number): number => a + b;
一个更复杂的函数在常量内声明可以像这样声明:
const complexFunction: (a: number) => (b:number) => number = (a: number): (b: number) => number => (b: number): number => a + b;
当声明一个函数作为普通函数时,其类型方式几乎与常量方式相同,但你不需要声明这些函数是一个函数。以下是一个例子:
function foo(a: number, b:number): number{
return a + b;
}
接口
TypeScript 检查变量的值是否是正确的类型,同样的原则也适用于类、对象或代码之间的合同。这通常被称为“鸭子类型”或“结构子类型”。接口存在是为了填补这个空间并定义这些合同或类型。
让我们尝试通过这个例子来理解一个接口:
function greetingStudent(student: {name: string}){
console.log(`Hello ${student.name}`);
}
const newStudent = {name: 'Heitor'};
greetingStudent(newStudent);
这个函数将知道对象上有一个名为 name 的属性,并且可以调用它是有效的。
我们可以使用接口类型来重写它,以便更好地管理代码:
interface IStudent {
name: string;
course?: string;
readonly university: string;
}
function greetingStudent(student: IStudent){
console.log(`Hello ${student.name}`);
if(student.course){
console.log(`Welcome to the ${student.course}` semester`);
}
}
const newStudent: IStudent = { name: 'Heitor', university: 'UDF' };
greetingStudent(newStudent);
正如你所看到的,我们有一个新的属性叫做course
,在它上面声明了一个?
。这表示这个属性可以是 null 或 undefined。这被称为可选属性。
有一个声明为只读属性的属性。如果我们在变量创建后尝试更改它,我们将收到一个编译错误,因为它使属性变为只读。
装饰器
ECMAScript 6 引入了一个新特性——类。随着这些特性的引入,装饰器的使用也成为了 JavaScript 引擎上的可能。
装饰器提供了一种在类声明及其成员上添加注解和元编程语法的方式。由于它已经在 TC-39 委员会(其中TC代表技术委员会)上处于最终批准状态,TypeScript 编译器已经可以使用它。
要启用它,你可以在tsconfig.json
文件中设置标志:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器是一种特殊的声明,可以附加到类、方法、存取器属性或参数上。它们以@expression
的形式使用,其中表达式是一个在运行时将被调用的函数。
一个可以应用于类的装饰器的例子可以在以下代码片段中看到:
function classSeal(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
当你创建这个函数时,你是在说构造函数的对象和它的原型将被封闭。
在类内部使用它非常简单:
@classSeal
class Animal {
sound: string;
constructor(sound: string) {
this.sound = sound;
}
emitSound() {
return "The animal says, " + this.sound;
}
}
这些只是一些装饰器及其功能的例子,可以帮助你使用 TypeScript 进行面向对象编程(OOP)的开发。
总结
总之,类型只是在使用 TypeScript 和 JavaScript 进行开发过程中让我们的生活变得更加轻松的一种方式。
因为 JavaScript 是一种动态语言,没有静态类型,TypeScript 中声明的所有类型和接口都严格地只被 TypeScript 使用。这有助于编译器捕捉错误、警告,并且语言服务器可以帮助集成开发环境(IDE)在开发过程中分析你的代码。
这是 TypeScript 的基本介绍,涵盖了有关这种类型语言的基础知识,以及如何理解和使用它。关于它的使用还有很多要学习,比如泛型、模块、命名空间等等。
通过这个介绍,你可以了解新的Vue 3核心是如何工作的,以及如何在项目中使用 TypeScript 的基础知识,并利用项目中的类型语言。
关于 TypeScript,总是有更多的知识可以获取,因为它是建立在 JavaScript 之上的一种不断发展的“语言”,并且拥有一个不断增长的社区。
不要忘记查看 TypeScript 文档,以了解更多信息,以及它如何从现在开始改进你的代码。
另请参阅
你可以在www.typescriptlang.org/docs/handbook/basic-types.html
找到有关 TypeScript 基本类型的更多信息。
你可以在www.typescriptlang.org/docs/handbook/functions.html
找到有关 TypeScript 函数的更多信息。
你可以在www.typescriptlang.org/docs/handbook/enums.html
找到有关 TypeScript 枚举的更多信息。
你可以在www.typescriptlang.org/docs/handbook/advanced-types.html
找到有关 TypeScript 高级类型的更多信息。
你可以在www.typescriptlang.org/docs/handbook/decorators.html
找到有关 TypeScript 装饰器的更多信息。
在rmolinamir.github.io/typescript-cheatsheet/#types
上查看 TypeScript 类型的速查表。
创建你的第一个 TypeScript 类
在 TypeScript 中,没有一种主要的范式来编写程序。你可以选择面向对象的、结构化的,甚至是函数式的。
在大多数情况下,我们会看到使用面向对象编程范例。在这个配方中,我们将学习如何在 TypeScript 中创建一个类,它的继承,接口,以及代码中可以使用的其他属性。
做好准备
要开始我们的项目,我们需要创建一个npm
项目。要做到这一点,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm init -y
您还需要安装 TypeScript。要做到这一点,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install typescript --only=dev
如何做到这一点...
在 TypeScript 文件中编写类时,我们首先需要考虑这个类将做什么,这个类可以是什么,它如何通过继承被另一个类扩展,以及它如何在这个过程中受到影响。
想象一下,我们有一个基本的Animal
类。这个类可以有一些基本属性,比如它的name
,它是否发出sound
,它的family
,以及这种动物所吃的基本food chain
。
- 让我们从过程的基础开始,
food chain
。我们需要确保它是一个不可枚举的列表,并且每个使用它的文件最终都有相同的值。我们只需要调用一个常量变量:
export enum FoodChainType {
Carnivorous = 'carnivorous',
Herbivorous = 'herbivorous',
Omnivorous = 'omnivorous',
}
- 现在,我们想为我们的动物制作基本的
interface
。我们知道我们的动物有一个name
,可以发出一个sound
,可以成为一个family
的一部分,并且属于food chain
类别。在类中使用接口,我们在类和将要暴露的内容之间建立了一个合同,有助于开发过程:
interface IAnimal {
name: string;
sound?: string;
family: string;
foodChainType: FoodChainType;
}
- 有了这一切,我们可以制作我们的
Animal
类。每个类都可以有它的构造函数。类构造函数可以很简单,只包含一些变量作为参数,也可以更复杂,有一个对象作为参数。如果你的构造函数将有任何参数,需要一个接口或声明每个参数的类型。在这种情况下,我们的构造函数将是一个对象,只有一个参数,与Animal
相同,所以它将扩展IAnimal
接口:
interface IAnimalConstructor extends IAnimal {
}
- 现在,为了创建我们的类,我们声明了将要使用的接口和枚举。我们将首先声明该类将实现
IBasicAnimal
接口。为此,我们需要添加一些我们的类将具有的公共元素,并也声明这些元素。我们需要实现函数来显示它是什么动物以及它发出什么声音。现在,我们有了一个包含所有动物属性的基本类。它具有类和构造函数的单独接口。食物链的枚举以一种易于阅读的方式声明,因此该库的 JavaScript 导入可以无问题执行:
interface IBasicAnimal extends IAnimal {
whoAmI: () => void;
makeSound: () => void;
}
export class Animal implements IBasicAnimal {
public name: string;
public sound: string;
public family: string;
public foodChainType: FoodChainType;
constructor(params: IAnimalConstructor) {
this.name = params.name;
this.sound = params.sound || '';
this.family = params.family;
this.foodChainType = params.foodChainType;
}
public whoAmI(): void {
console.log(`I am a ${this.name}, my family is ${this.family}.
My diet is ${this.foodChainType}.`);
if (this.sound) {
console.log([...Array(2).fill(this.sound)].join(', '));
}
}
public makeSound(): void {
console.log(this.sound);
}
}
- 让我们用几行代码扩展这个类,将这个
Animal
转换成Dog
:
import {Animal, FoodChainType} from './Animal';
class Dog extends Animal {
constructor() {
super({
name: 'Dog',
sound: 'Wof!',
family: 'Canidae',
foodChainType: FoodChainType.Carnivorous,
});
}n
}
这是一种简单的方式,通过扩展父类并使用父类的子类定义来组成一个几乎与父类具有相同接口的新类。
它是如何工作的...
TypeScript 中的类与 Java 或 C#等语言中的其他类一样工作。编译器在开发和编译过程中评估这些共同的原则。
在这种情况下,我们创建了一个简单的类,其中有一些公共属性是子类固有的。这些变量都是可读的,可以被改变。
还有更多...
在 TypeScript 中,我们有各种可能的类的用法,比如抽象类、特殊修饰符,以及将类用作接口。我们在这里只是涵盖了类的基础知识,为我们提供了一个良好的起点。如果你想深入了解,TypeScript 文档非常有帮助,并且有很多例子可以帮助学习过程。
另请参阅
您可以在www.typescriptlang.org/docs/handbook/classes.html
找到有关 TypeScript 类的更多信息。
在rmolinamir.github.io/typescript-cheatsheet/#classes
上查看 TypeScript 类的速查表。
使用 Vue CLI 创建您的第一个项目
当 Vue 团队意识到开发人员在创建和管理他们的应用程序时遇到问题时,他们看到了一个机会,可以创建一个工具来帮助全世界的开发人员。Vue CLI 项目诞生了。
Vue CLI 工具是一个在终端命令中使用的 CLI 工具,如 Windows PowerShell、Linux Bash 或 macOS Terminal。它被创建为 Vue 开发的起点,开发人员可以开始一个项目并顺利地管理和构建它。Vue CLI 团队在开发时的重点是为开发人员提供更多时间来思考代码,花费更少的时间在工具上,以将他们的代码投入生产,添加新的插件或简单的热模块重载
。
Vue CLI 工具被调整得无需在将其投入生产之前将您的工具代码弹出 CLI 之外。
当版本 3 发布时,Vue UI 项目被添加到 CLI 作为主要功能,将 CLI 命令转换为更完整的可视解决方案,并增加了许多新的功能和改进。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做…
要创建 Vue CLI 项目,请按照以下步骤进行:
- 我们需要打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:
> vue create my-first-project
- CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键进行导航,Enter键继续,Spacebar键选择选项:
? Please pick a preset: (Use arrow keys) default (babel, eslint) ❯ **Manually select features**
-
有两种方法可以启动一个新项目。默认方法是一个基本的babel
和eslint
项目,没有任何插件或配置,还有手动
模式,您可以选择更多的模式、插件、linters 和选项。我们将选择手动
。
-
现在,我们被问及我们将在项目中需要的功能。这些功能是一些 Vue 插件,如 Vuex 或 Router(Vue-Router)、测试工具、linters 等:
? Check the features needed for your project: (Use arrow keys) ❯ Babel
TypeScript Progressive Web App (PWA) Support Router Vuex CSS Pre-processors ❯ Linter / Formatter
Unit Testing ❯ **E2E Testing**
- 对于这个项目,我们将选择
CSS 预处理器
并按Enter继续:
? Check the features needed for your project: (Use arrow keys) ❯ Babel
TypeScript Progressive Web App (PWA) Support Router Vuex ❯ CSS Pre-processors ❯ **Linter / Formatter**
Unit Testing **E2E Testing**
- 可以选择要与 Vue 一起使用的主要层叠样式表(CSS)预处理器——
Sass
、Less
和Stylus
。由您选择哪种最适合您:
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules
are supported by default): (Use arrow keys) Sass/SCSS (with dart-sass) Sass/SCSS (with node-sass) **Less** ❯ Stylus
- 现在是格式化您的代码的时候了。您可以在
AirBnB
、Standard
和Prettier
之间进行选择,并使用基本配置。那些在ESLint
中导入的规则可以随时进行自定义,没有任何问题,并且有一个完美的规则适合您的需求。您知道什么对您最好:
? Pick a linter / formatter config: (Use arrow keys) ESLint with error prevention only ❯ **ESLint + Airbnb config** ESLint + Standard config
ESLint + Prettier
- 设置完 linting 规则后,我们需要定义它们何时应用于您的代码。它们可以在保存时应用,也可以在提交时进行修复:
? Pick additional lint features: (Use arrow keys) **Lint on save** ❯ Lint and fix on commit
- 在定义所有这些插件、linters 和处理器之后,我们需要选择设置和配置存储的位置。存储它们的最佳位置是在一个专用文件中,但也可以将它们存储在
package.json
文件中:
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) ❯ **In dedicated config files** In package.json
- 现在,您可以选择是否要将此选择设置为将来项目的预设,以便您无需再次重新选择所有内容:
? Save this as a preset for future projects? (y/N) n
- CLI 将自动创建具有您在第一步中设置的名称的文件夹,安装所有内容并配置项目。
您现在可以浏览和运行项目了。Vue CLI 项目的基本命令如下:
-
npm run serve
—用于在本地运行开发服务器
-
npm run build
—用于构建和缩小应用程序以进行部署
-
npm run lint
—对代码执行 lint
您可以通过 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)执行这些命令。
还有更多...
CLI 内部有一个名为 Vue UI 的工具,可帮助管理 Vue 项目的过程。该工具将处理项目的依赖关系、插件和配置。
Vue UI 工具中的每个npm
脚本都被命名为任务,在这些任务中,您可以获得实时统计数据,例如资产、模块和依赖项的大小;错误或警告的数量;以及更深入的网络数据,以微调您的应用程序。
要进入 Vue UI 界面,您需要在 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)中执行以下命令:
> vue ui
另请参阅
在cli.vuejs.org/guide/
找到有关 Vue CLI 项目的更多信息。
请在cli.vuejs.org/dev-guide/plugin-dev.html
找到有关 Vue CLI 插件开发的更多信息。
使用 Vue UI 向 Vue CLI 项目添加插件
Vue UI 工具是 Vue 开发中最强大的附加工具之一。它可以让开发人员的生活更轻松,同时可以帮助管理 Vue 项目。
准备就绪
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
操作步骤...
首先,我们需要创建我们的 Vue CLI 项目。要找到如何创建 Vue CLI 项目,请查看“使用 Vue CLI 创建您的第一个项目”食谱。我们可以使用上一个食谱中创建的项目,也可以开始一个新项目。现在,按照说明添加插件:
- 打开 Vue UI 界面。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> vue ui
- 将会出现一个新的浏览器窗口,其中包含Vue UI界面:
在这里,您可以列出您的项目,创建一个新项目,或导入一个现有项目。
- 现在,我们将导入我们创建的插件:
您需要找到您创建的文件夹,然后单击“导入此文件夹”。
- 文件夹导入后,项目的默认仪表板将出现:
在这里,可以通过点击顶部的“自定义”按钮来自定义您的仪表板,添加新的小部件:
- 要添加新插件,必须单击左侧边栏中的“插件”菜单:
您在 Vue CLI 工具中添加的基本插件将已列在此处。
- 现在,我们将添加基本的 Vue 生态系统插件—**vuex **和 vue-router:
- 如果您检查您的代码,您将看到
main.js
文件已更改,并且vuex(store)
和vue-router(router)
插件现在已导入并注入到 Vue 实例中:
工作原理...
Vue UI 插件与npm
或yarn
配合使用,自动安装项目中的软件包,然后在可能的情况下注入 Vue 实例所需的条件。
如果插件是一个可视化、指令或非直接实例化的插件,Vue UI 将安装并管理它,但您需要导入它以在应用程序中使用。
向 Vue CLI 项目添加 TypeScript
在 JavaScript 项目中使用 TypeScript,即使是用于静态类型检查,也是一个好的做法。它有助于最小化项目内部的错误和对象问题。
通过 Vue UI 的帮助向 Vue 项目添加 TypeScript 非常简单,您将能够使用 TypeScript 的 JavaScript 代码。
准备就绪
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建我们的 Vue CLI 项目。要了解如何创建 Vue CLI 项目,请查看“使用 Vue CLI 创建您的第一个项目”食谱。我们可以使用上一个食谱中创建的项目,或者开始一个新项目。
要将 TypeScript 添加到 Vue CLI 项目中,请按照以下步骤进行:
- 打开 Vue UI 界面。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> vue ui
- 在您的项目中,转到插件管理器,点击+添加插件,然后搜索
@vue/cli-plugin-typescript
:
- 现在,点击页面底部的安装@vue/cli-plugin-typescript 按钮:
- 在插件下载完成之后,在最终安装之前,会要求您进行一些配置设置:
-
使用类样式组件语法?使用 TypeScript 的vue-class-component
插件。
-
与 TypeScript 一起使用 Babel(现代模式所需,自动检测的 polyfill,转译 JSX)?激活 Babel 以在 TypeScript 编译器之外转译 TypeScript。
-
使用 ESLint?将 ESLint 用作.ts
和.tsx
文件的检查器。
-
将所有.js 文件转换为.ts 文件?在安装过程中自动将所有.js
文件转换为.ts
文件。
-
允许编译.js 文件?激活tsconfig.json
标志以接受编译器中的.js
文件。
-
选择您的选项后,点击完成安装。
-
现在,您的项目是一个 TypeScript Vue 项目,所有文件都已配置好,准备好进行编码:
它是如何工作的...
Vue UI 作为插件管理器将为您下载为 Vue 制作的 TypeScript 包,并安装和配置它以符合您选择的设置。
您的项目将根据您的规格进行更改和修改,然后准备好进行开发。
另请参阅
在github.com/typescript-eslint/typescript-eslint
找到有关 TypeScript ESLint 的更多信息
在github.com/vuejs/vue-class-component
找到有关vue-class-component
的更多信息。
使用 vue-class-component 创建您的第一个 TypeScript Vue 组件
由于 Vue 组件是基于对象的,并且与 JavaScript 对象的this
关键字有着密切的关系,因此开发 TypeScript 组件会有点混乱。
vue-class-component
插件使用 ECMAScript 装饰器提案将静态类型的值直接传递给 Vue 组件,并使编译器更容易理解发生了什么。
准备工作
这个配方的前提条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建我们的 Vue CLI 项目。我们可以使用上一个配方中创建的项目,或者开始一个新的项目。要了解如何在 Vue CLI 项目中添加 TypeScript,请查看'向 Vue CLI 项目添加 TypeScript'配方。
按照说明使用 Typescript 和vue-class-component
创建你的第一个 Vue 组件:
-
在src/components
文件夹内创建一个名为Counter.vue
的新文件。
-
现在,让我们开始制作 Vue 组件的脚本部分。我们将创建一个包含数字数据的类,两个方法——一个用于增加,另一个用于减少——最后,一个计算属性来格式化最终数据:
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class Counter extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
- 现在是时候为这个组件创建模板和渲染了。这个过程与 JavaScript Vue 文件相同。我们将添加增加和减少数值的按钮,并显示格式化的文本:
<template>
<div>
<fieldset>
<legend>{{ formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button @click="decrease">Decrease</button>
</fieldset>
</div>
</template>
- 在
App.vue
文件中,我们需要导入刚刚创建的组件:
<template>
<div id="app">
<Counter />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
@Component({
components: {
Counter,
},
})
export default class App extends Vue {
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
- 现在,当你在终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)上运行
npm run serve
命令时,你将看到你的组件在屏幕上运行和执行:
工作原理...
vue-class-component
插件利用装饰器的新提案来向 TypeScript 类注入和传递一些属性。
这种注入有助于简化使用与 Vue 常见对象相比更符合 TypeScript 语法的组件开发过程。
另请参阅
在github.com/vuejs/vue-class-component
找到更多关于vue-class-component
的信息。
使用 vue-class-component 创建自定义 mixin
在 Vue 中,mixin
是一种在其他 Vue 对象中重用相同代码的方式,就像将mixin
的所有属性混合到组件中一样。
在使用 mixin 时,Vue 首先声明mixin
属性,然后是组件值,因此组件始终是最后且有效的值。此合并以深度模式进行,并且已在框架内声明了特定的方式,但可以通过特殊配置进行更改。
通过使用 mixin,开发人员可以编写小段的代码并在许多组件中重用它们。
这种方法简化了您的工作,并允许您更快地完成任务。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建我们的 Vue CLI 项目。我们可以使用上一个食谱中创建的项目,或者开始一个新项目。要了解如何使用 TypeScript 创建 Vue CLI 项目,请查看'使用 vue-class-component 创建您的第一个 TypeScript Vue 组件'食谱。
在这个食谱中,我们将其分为两个独立的部分。首先,我们将创建计数器组件,然后我们将使用共享的代码来创建 mixin。
创建计数器组件
现在,按照以下说明使用vue-class-component
创建自定义 mixin:
-
我们需要在src/components
文件夹中创建一个名为CounterByTen.vue
的新组件。
-
现在,让我们开始制作 Vue 组件的脚本部分。我们将创建一个类,其中将有一个类型为数字的变量和默认值为0
;两种方法,一种是增加10
,另一种是减少10
;最后,一个计算属性来格式化最终数据:
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class CounterByTen extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
- 是时候为这个组件创建模板和渲染了。该过程与 JavaScript Vue 文件相同。我们将添加增加和减少值的按钮以及显示格式化文本的按钮:
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
- 在
App.vue
文件中,我们需要导入刚刚创建的组件:
<template>
<div id="app">
<Counter />
<hr />
<CounterByTen />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
import CounterByTen from './components/CounterByTen.vue';
@Component({
components: {
Counter,
CounterByTen,
},
})
export default class App extends Vue {
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
提取相似的代码以用于 mixin
由于这两个组件具有相似的代码,我们可以提取这些相似的代码并创建一个 mixin。这个 mixin 可以在这两个组件中导入,它们的行为将是相同的:
-
在src/mixins
文件夹中创建一个名为defaultNumber.ts
的文件。
-
为了编写我们的 mixin,我们将从vue-class-component
插件中导入Component
和Vue
修饰符,作为 mixin 的基础。我们需要采用类似的代码并将其放入 mixin 中:
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class DefaultNumber extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
}
- 准备好 mixin 后,打开
src/components
文件夹中的Counter.vue
组件并导入它。为此,我们需要从vue-class-component
中导入一个特殊的导出,称为mixins
,并将其与我们想要扩展的 mixin 扩展。这将删除Vue
和Component
装饰器,因为它们已经在 mixin 上声明了:
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import DefaultNumber from '../mixins/defaultNumber';
@Component
export default class CounterByTen extends mixins(DefaultNumber) {
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
- 现在,当您在终端(macOS 或 Linux)上运行
npm run serve
命令或在命令提示符/PowerShell(Windows)上运行时,您将看到您的组件在屏幕上运行和执行:
它是如何工作的...
使用 TypeScript 使用 mixins 的过程与使用 Vue 对象的过程相同。共享的代码可以拆分成更小的文件,并在组件中调用,以便更轻松地编码。
在使用 TypeScript 和vue-class-component
时,需要在 mixins 上声明Vue
和Component
装饰器,因为将使用 mixin 的类已经具有此扩展,因为它扩展了此 mixin。
我们将相同的代码片段放在两个组件中,然后将其放在一个新文件中,然后在两个组件中调用它。
另请参阅
了解有关vue-class-component
mixins 的更多信息,请访问github.com/vuejs/vue-class-component#using-mixins
。
了解有关 Vue mixins 的更多信息,请访问v3.vuejs.org/guide/mixins.html
使用 vue-class-component 创建自定义函数装饰器
装饰器是在 ECMAScript 2015 中引入的。装饰器是一种高阶函数,它用另一个函数包装一个函数。
这为代码带来了许多新的改进,以及更大的生产力,因为它采用了函数式编程的原则并简化了它。
准备工作
此处的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建我们的 Vue CLI 项目。要了解如何创建 Vue CLI 项目,请查看“使用 Vue CLI 创建您的第一个项目”食谱。我们可以使用上一个食谱中创建的项目,或者开始一个新项目。
按照以下步骤使用vue-class-component
创建自定义函数装饰器:
-
在src/decorators
文件夹中创建一个名为componentMount.js
的文件。
-
我们需要从vue-class-component
中导入createDecorator
函数,以便在基于vue-class-component
的组件上使用它,并开始编写我们的装饰器:
import { createDecorator } from 'vue-class-component';
import componentMountLogger from './componentLogger';
export default createDecorator((options) => {
options.mixins = [...options.mixins, componentMountLogger];
});
createDecorator
函数就像 Vue vm (View-Model)的扩展,因此它不会有 ECMAScript 装饰器的属性,但会作为 Vue 装饰器的功能。
- 我们需要在我们的装饰器中使用
componentLogger.js
文件。这个函数将获取在“装饰”组件中设置的所有数据值,并对其添加一个监视器。每当它改变时,这个监视器将记录新值和旧值。这个函数只有在调试数据设置为true
时才会执行:
export default {
mounted() {
if (this.debug) {
const componentName = this.name || '';
console.log(`The ${componentName} was mounted
successfully.`);
const dataKeys = Object.keys(this.$data);
if (dataKeys.length) {
console.log('The base data are:');
console.table(dataKeys);
dataKeys.forEach((key) => {
this.$watch(key, (newValue, oldValue) => {
console.log(`The new value for ${key} is:
${newValue}`);
console.log(`The old value for ${key} is:
${oldValue}`);
}, {
deep: true,
});
});
}
}
},
};
- 现在,我们需要将装饰器导入到位于
src/components
文件夹中的Counter.vue
组件文件中,并向其添加调试器数据:
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button@click="decrease">Decrease</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import componentMount from '../decorators/componentMount';
@Component
@componentMount
export default class Counter extends Vue {
valueNumber: number = 0;
debug: boolean = true;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
它是如何工作的...
createDecorator
函数是一个工厂函数,它扩展了 Vue vm(View Model),产生了 Vue 组件的扩展,比如一个 Vue mixin。Vue mixin 是 Vue 组件的一个属性,可以用来在组件之间共享和重用代码。
当我们调用 mixin 时,它将当前组件作为第一个参数的选项(如果它附加到属性,则为键),以及它的索引。
我们添加了一个动态调试器,只有在存在调试数据并且设置为true
时才会附加。这个调试器将记录当前数据,并为数据的更改设置监视器,每次数据更改时都会在控制台上显示日志。
还有更多...
在使用 linters 时,一些规则可能会成为装饰器的问题。因此,明智的做法是仅在出现规则问题的文件上禁用它们,这些规则是代码正常工作所必需的。
例如,在 AirBnB 风格中,no-param-reassign
规则是必需的,因为装饰器使用选项作为传递值的引用。
另请参阅
在github.com/vuejs/vue-class-component#create-custom-decorators
找到有关使用vue-class-component
创建自定义装饰器的更多信息。
在www.typescriptlang.org/docs/handbook/decorators.html
找到有关 ECMAScript 装饰器的更多信息。
向 vue-class-component 添加自定义钩子
在 Vue 中,可以通过插件应用程序编程接口(API)向其生命周期添加钩子。最基本的例子是vue-router
与导航守卫,例如beforeRouterEnter
和beforeRouterLeave
函数钩子。
钩子,顾名思义,是每次发生某事时调用的小函数。
您可以利用这些钩子,使它们更加强大,为您的组件添加新的功能,例如检查特殊安全访问、添加搜索引擎优化(SEO)甚至预取数据。
准备工作
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建我们的 Vue CLI 项目。我们可以使用上一个配方中创建的项目,也可以开始一个新项目。要了解如何在 Vue CLI 项目中添加 TypeScript,请查看'向 Vue CLI 项目添加 TypeScript'配方。
现在,按照以下步骤,使用 TypeScript 和vue-class-component
为您的 Vue 项目添加自定义钩子:
- 我们需要将
vue-router
添加到项目中。这可以在创建 Vue CLI 项目时完成,也可以在创建项目后的 Vue UI 界面中完成。
如果提示选择模式,应该运行vue-router
。请注意,选择History选项将在部署时需要特殊的服务器配置。
-
打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),执行npm run serve
命令,您将看到vue-router
正在工作,并且有两个工作路由器:home
和about
。
-
让我们开始创建并命名我们的钩子以注册到主应用程序。为此,我们需要在src/classComponentsHooks
文件夹中创建一个vue-router.js
文件:
import Component from 'vue-class-component';
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
]);
- 我们需要将这个文件导入到
main.ts
文件中,因为它需要在应用程序最终构建之前被调用:
import './classComponentsHooks/vue-router';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App),
}).$mount('#app');
-
现在,我们已经在vue-class-component
中注册了这些钩子,并且它们可以在 TypeScript 组件中使用。
-
我们需要在src/views
文件夹中创建一个名为Secure.vue
的新路由位置。安全页面将有一个输入密码,vuejs
。当用户输入此密码时,路由守卫将授予权限,用户可以看到页面。如果密码错误,用户将被带回到主页。当他们离开页面时,警报将向用户显示一条消息:
<template>
<div class="secure">
<h1>This is an secure page</h1>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Route, RawLocation } from 'vue-router';
type RouteNext = (to?: RawLocation | false | ((vm: Vue) => any) |
void) => void;
@Component
export default class Home extends Vue {
beforeRouteEnter(to: Route, from: Route, next: RouteNext) {
const securePassword = 'vuejs';
const userPassword = prompt('What is the password?');
if (userPassword === securePassword) {
next();
} else if (!userPassword) {
next('/');
}
}
beforeRouteLeave(to: Route, from: Route, next: RouteNext) {
alert('Bye!');
next();
}
}
</script>
- 现在我们的页面已经完成,我们需要将其添加到
router.ts
文件中,以便在 Vue 应用程序中调用它:
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
component: () => import('./views/About.vue'),
},
{
path: '/secure',
name: 'secure',
component: () => import('./views/Secure.vue'),
},
],
});
- 路由添加完成并创建视图后,最后一步是将链接添加到主
App.vue
文件中,这样我们就会得到一个集成了钩子的组件:
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/secure">Secure</router-link>
</div>
<router-view/>
</div>
</template>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
#nav
padding 30px
a
font-weight bold
color #2c3e50
&.router-link-exact-active
color #42b983
</style>
它是如何工作的...
在执行 Vue 应用程序之前,类组件需要了解被添加到 Vue 原型的导航守卫是什么。因此,我们需要在main.ts
文件的第一行导入自定义钩子。
在组件中,通过注册钩子,可以将它们添加为方法,因为vue-class-component
已经将所有这些自定义导入转换为组件装饰器的基本方法。
我们使用了两个vue-router
导航守卫的钩子。这些钩子在每次路由进入或离开时都会被调用。我们没有使用的前两个参数to
和from
是携带有关未来路由和过去路由的信息的参数。
next
函数总是必需的,因为它执行路由更改。如果在函数中没有传递参数,路由将继续使用被调用的路由,但如果想要即时更改路由,可以传递参数来改变用户将要前往的位置。
另请参阅
在router.vuejs.org/guide/advanced/navigation-guards.html
中了解更多关于 vue-router 导航守卫的信息。
在github.com/vuejs/vue-class-component#adding-custom-hooks
中了解更多关于 vue-class-component 钩子的信息。
将 vue-property-decorator 添加到 vue-class-component
Vue 中一些最重要的部分在vue-class-component
中以 TypeScript 装饰器的形式缺失。因此,社区制作了一个名为vue-property-decorator
的库,这个库得到了 Vue 核心团队的全力支持。
这个库引入了一些缺失的部分,如 ECMAScript 提案装饰器,比如props
、watch
、model
、inject
等。
准备工作
这个教程的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
首先,我们需要创建 Vue CLI 项目。我们可以使用上一个示例中创建的项目,也可以开始一个新项目。要了解如何使用 TypeScript 创建 Vue CLI 项目,请查看'使用 vue-class-component 创建自定义 mixin'示例。
按照以下步骤将vue-property-decorator
添加到 Vue基于类的组件
中:
- 我们需要将
vue-property-decorator
添加到我们的项目中。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install -S vue-property-decorator
- 在组件的 mixin 中,我们将添加一个装饰器来接收一个 prop,这将是我们计算的数字的值:
import {
Vue,
Component,
Prop,
} from 'vue-property-decorator';
@Component
export default class DefaultNumber extends Vue {
valueNumber: number = 0;
@Prop(Number) readonly value: number | undefined;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
}
- 有了这个数字,当值发生变化时,我们需要使观察者向父组件发出事件,并在父组件内部值发生变化时更新值。为此,我们需要在
src/mixins
文件夹内创建一个名为numberWatcher.ts
的新文件:
import {
Watch,
Mixins,
} from 'vue-property-decorator';
import DefaultNumber from './defaultNumber';
export default class NumberWatchers extends Mixins(DefaultNumber) {
@Watch('valueNumber')
onValueNumberChanged(val: number) {
this.$emit('input', val);
}
@Watch('value', { immediate: true })
onValueChanged(val: number) {
this.valueNumber = val;
}
}
在 Vue 中,v-model
指令的工作原理类似于糖语法,它是 Vue $emit
函数和 Vue props
函数的组合。当值发生变化时,组件需要使用'input'
名称进行$emit
,并且组件需要在props
函数中有一个value
键,这将是从父组件传递到子组件的值。
- 随着我们的 mixin 更新,我们的组件也需要更新。首先,我们将更新
Counter.vue
组件,将导入的 mixin 从defaultNumber.ts
文件更改为numberWatcher.ts
:
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button @click="decrease">Decrease</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import NumberWatcher from '../mixins/numberWatcher';
@Component
export default class Counter extends mixins(NumberWatcher) {
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
- 现在,我们将更新
CounterByTen.vue
组件,并添加新创建的 mixin:
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import NumberWatcher from '../mixins/numberWatcher';
@Component
export default class CounterByTen extends mixins(NumberWatcher) {
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
- 一切就绪后,我们只需要更新
App.vue
组件。这一次,我们将在组件中存储一个变量,该变量将传递给两个子组件,当组件发出更新事件时,此变量将自动更改,也会更新其他组件:
<template>
<div id="app">
<Counter
v-model="amount"
/>
<hr />
<CounterByTen
v-model="amount"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
import CounterByTen from './components/CounterByTen.vue';
@Component({
components: {
Counter,
CounterByTen,
},
})
export default class App extends Vue {
amount: number = 0;
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
工作原理...
通过在vue-class-components
中注入装饰器,vue-property-decorator
帮助 TypeScript 编译器检查 Vue 代码的类型和静态分析。
我们使用了两个可用的装饰器,@Watch
和@Prop
装饰器。
当我们将代码的常见部分拆分成 mixin 的形式时,流程实现变得更加容易。
父组件向子组件传递了一个属性,传递了初始值和随后更新的值。
这个值在子组件内部进行检查和更新,用于更新计算函数使用的本地变量。当计算完成并且值发生变化时,watcher 会发出一个事件,传递给父组件,父组件更新主变量,循环继续。
还有更多...
还有另一个库,它与vue-property-decorator
相同,但用于vuex
插件,名为vuex-class
。
这个库使用与vue-property-decorator
相同的过程。它在组件中创建一个 inject 装饰器。这些装饰器帮助 TypeScript 编译器在开发过程中检查类型。
您可以在github.com/ktsn/vuex-class/
找到有关这个库的更多信息。
另请参阅
您可以在github.com/kaorun343/vue-property-decorator
找到有关vue-property-decorator
的更多信息。
第三章:数据绑定、表单验证、事件和计算属性
数据是当今世界上最宝贵的资产,知道如何管理它是必须的。在 Vue 中,我们有权利选择如何收集这些数据,按照我们的意愿进行操作,并将其传递到服务器。
在本章中,我们将更多地了解数据处理和数据处理过程、表单验证、数据过滤、如何向用户显示这些数据以及如何以与我们应用程序内部不同的方式呈现它。
我们将学习如何使用vue-devtools
深入了解 Vue 组件并查看我们的数据和应用程序发生了什么。
在本章中,我们将涵盖以下内容:
-
创建“hello world”组件
-
创建具有双向数据绑定的输入表单
-
向元素添加事件侦听器
-
从输入中删除 v-model
-
创建动态待办事项列表
-
创建计算属性并探索其工作原理
-
使用自定义过滤器显示更清晰的数据和文本
-
使用 Vuelidate 添加表单验证
-
为列表创建过滤器和排序器
-
创建条件过滤以对列表数据进行排序
-
添加自定义样式和过渡
-
使用vue-devtools
调试您的应用程序
技术要求
在本章中,我们将使用Node.js和Vue CLI。
注意,Windows 用户需要安装一个名为windows-build-tools
的npm
包,以便安装以下所需的软件包。为此,请以管理员身份打开 PowerShell 并执行以下命令:
> npm install -g windows-build-tools
。
要安装Vue CLI,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
创建“hello world”组件
Vue 应用程序是各种组件的组合,由 Vue 框架绑定在一起并进行编排。知道如何制作您的组件很重要。每个组件就像墙上的一块砖,需要以一种方式制作,以便在放置时不需要其他砖块以不同的方式重新塑造。我们将学习如何制作一个基本组件,其中包含一些侧重于组织和清晰代码的重要原则。
准备工作
本教程的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做到这一点...
要启动我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像在“使用 Vue CLI 创建您的第一个项目”中学到的那样,或者开始一个新的项目。
要启动一个新的组件,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> vue create my-component
命令行界面(CLI)将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,空格键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
让我们创建我们的第一个“hello world”组件,按照以下步骤进行:
-
让我们在src/components
文件夹中创建一个名为CurrentTime.vue
的新文件。
-
在这个文件中,我们将从组件的<template>
部分开始。它将是一个显示当前日期格式化的阴影框卡:
<template>
<div class='cardBox'>
<div class='container'>
<h2>Today is:</h2>
<h3>{{ getCurrentDate }}</h3>
</div>
</div>
</template>
- 现在,我们需要创建
<script>
部分。我们将从name
属性开始。这将在使用vue-devtools
调试我们的应用程序时使用,也有助于集成开发环境(IDE)。对于getCurrentDate
计算属性,我们将创建一个computed
属性,它将返回当前日期,由Intl
浏览器函数格式化:
<script>
export default {
name: 'CurrentTime',
computed: {
getCurrentDate() {
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date());
}
}
};
</script>
- 为了为我们的框添加样式,我们需要在
src
文件夹中创建一个style.css
文件,然后将cardBox
样式添加到其中:
.cardBox {
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s linear;
max-width: 33%;
border-radius: 3px;
margin: 20px;
}
.cardBox:hover {
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
}
.cardBox>.container {
padding: 4px 18px;
}
[class*='col-'] {
display: inline-block;
}
@media only screen and (max-width: 600px) {
[class*='col-'] {
width: 100%;
}
.cardBox {
margin: 20px 0;
}
}
@media only screen and (min-width: 600px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 768px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 992px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 1200px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
- 在
App.vue
文件中,我们需要导入我们的组件才能看到它:
<template>
<div id='app'>
<current-time />
</div>
</template>
<script>
import CurrentTime from './components/CurrentTime.vue';
export default {
name: 'app',
components: {
CurrentTime
}
}
</script>
- 在
main.js
文件中,我们需要导入style.css
文件以包含在 Vue 应用程序中:
import Vue from 'vue';
import App from './App.vue';
import './style.css';
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
这是您的组件呈现和运行的方式:
它是如何工作的...
Vue 组件几乎与 Node.js 包一样工作。要在代码中使用它,您需要导入组件,然后在要使用的组件的components
属性中声明它。
就像一堵砖墙一样,Vue 应用程序由调用和使用其他组件的组件组成。
对于我们的组件,我们使用了Intl.DateTimeFormat
函数,这是一个本机函数,可用于将日期格式化和解析为声明的位置。为了获得本地格式,我们使用了navigator
全局变量。
另请参见
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。
您可以在v3.vuejs.org/guide/single-file-component.html
找到有关 Vue 组件的更多信息
创建具有双向数据绑定的输入表单
为了收集网页上的数据,我们使用 HTML 表单输入。在 Vue 中,可以使用双向数据绑定方法,其中输入在文档对象模型(DOM)上的值传递到 JavaScript,反之亦然。
这使得 Web 表单更加动态,使您有可能在保存或发送数据回服务器之前管理、格式化和验证数据。
准备就绪
此处的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
操作步骤...
要启动我们的组件,我们可以使用 Vue CLI 创建 Vue 项目,就像在第二章的“使用 Vue CLI 创建第一个项目”中学到的那样,或者使用“创建'hello world'组件”中的项目。
现在,让我们按照以下步骤创建具有双向数据绑定的输入表单:
-
让我们在src/components
文件夹中创建一个名为TaskInput.vue
的新文件。
-
在这个文件中,我们将创建一个组件,其中将包含一个文本输入和一个显示文本。这个文本将基于文本输入中键入的内容。在组件的<template>
部分,我们需要创建一个 HTML 输入和一个mustache
变量,用于接收和呈现数据:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is: {{ task }}</strong>
<input
type='text'
v-model='task'
class='taskInput' />
</div>
</div>
</template>
- 现在,在组件的
<script>
部分,我们将命名它并将任务添加到data
属性中。由于数据始终需要返回一个Object
,我们将使用箭头函数直接返回一个Object
:
<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
};
</script>
- 我们需要为这个组件添加一些样式。在组件的
<style>
部分,我们需要添加scoped
属性,以便样式仅绑定到组件,不会与其他层叠样式表(CSS)规则混合:
<style scoped>
.tasker{
margin: 20px;
}
.tasker .taskInput {
font-size: 14px;
margin: 0 10px;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.75);
}
.tasker button {
border: 1px solid rgba(0, 0, 0, 0.75);
border-radius: 3px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
</style>
- 现在,我们需要将这个组件导入到我们的
App.vue
文件中:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' />
</div>
</template>
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'app',
components: {
CurrentTime,
TaskInput,
}
}
</script>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
当您创建一个 HTMLinput
元素并为其添加v-model
时,您正在传递一个内置于 Vue 中的指令,该指令检查输入类型并为我们提供输入的糖语法。这处理了变量值和 DOM 的更新。
这个模型被称为双向数据绑定。如果变量被代码改变,DOM 将重新渲染,如果它被 DOM 通过用户输入改变,比如input-form
,那么 JavaScript 代码就可以执行一个函数。
另请参阅
在v3.vuejs.org/guide/forms.html
找到有关表单输入绑定的更多信息
向元素添加事件监听器
在 Vue 中,父子通信最常见的方法是通过 props 和 events。在 JavaScript 中,通常会向 DOM 树的元素添加事件监听器,以便在特定事件上执行函数。在 Vue 中,可以添加监听器并根据需要命名,而不是坚持 JavaScript 引擎上存在的名称。
在这个配方中,我们将学习如何创建自定义事件以及如何发出它们。
准备就绪
这个配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
启动我们的组件,我们可以使用 Vue CLI 创建 Vue 项目,就像在“使用 Vue CLI 创建您的第一个项目”一章中学到的那样,或者使用“使用双向数据绑定创建输入表单”一章中的项目。
按照以下步骤在 Vue 中的元素上添加事件监听器:
-
创建一个新组件或打开TaskInput.vue
文件。
-
在<template>
部分,我们将添加一个按钮元素,并使用v-on
指令为按钮点击事件添加事件监听器。我们将从组件中删除{{ task }}
变量,因为从现在开始它将被发出并不再显示在组件上:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
v-model='task'
class='taskInput' />
<button
v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
- 在组件的
<script>
部分,我们需要添加一个处理点击事件的方法。该方法将被命名为addTask
。该方法将触发一个名为add-task
的事件,并将任务发送到数据中。之后,组件上的任务将被重置:
<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
methods: {
addTask(){
this.$emit('add-task', this.task);
this.task = '';
},
}
};
</script>
- 在
App.vue
文件中,我们需要在组件上添加一个事件监听器绑定。此侦听器将附加到add-task
事件。我们将使用v-on
指令的缩写版本@
。当它被触发时,事件将调用addNewTask
方法,该方法将发送一个警报,说明已添加了一个新任务:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input
class='col-6'
@add-task='addNewTask'
/>
</div>
</template>
- 现在,让我们创建
addNewTask
方法。这将接收任务作为参数,并向用户显示一个警报,显示任务已添加:
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'app',
components: {
CurrentTime,
TaskInput,
},
methods:{
addNewTask(task){
alert(`New task added: ${task}`);
},
},
}
</script>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
HTML 事件通过v-on
事件处理指令由 Vue 读取。当我们将v-on:click
指令附加到按钮时,我们向按钮添加了一个监听器,以便在用户单击按钮时执行一个函数。
该函数在组件方法中声明。该函数在调用时将发出一个事件,表示使用此组件作为子组件的任何组件都可以使用v-on
指令监听它。
另请参阅
您可以在v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息
从输入中删除 v-model
如果我告诉您,在v-model
的魔术背后有很多代码,使我们的魔术糖语法发生?如果我告诉您,兔子洞可以深入到足以控制输入的事件和值发生的一切?
我们将学习如何提取v-model
指令的糖语法,并将其转换为其背后的基本语法。
准备就绪
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做到...
要启动我们的组件,我们可以使用 Vue CLI 创建我们的 Vue 项目,就像在第二章,介绍 TypeScript 和 Vue 生态系统中学到的那样,或者使用“向元素添加事件侦听器”配方中的项目。
在接下来的步骤中,我们将从输入中删除v-model
指令的语法糖:
-
打开TaskInput.vue
文件。
-
在组件的<template>
块中,找到v-model
指令。我们将删除v-model
指令。然后,我们需要向输入添加一个新的绑定,称为v-bind:value
或缩写版本:value
,以及一个事件侦听器到 HTMLinput
元素。我们需要向input
事件添加一个事件侦听器,使用v-on:input
指令或缩写版本@input
。输入绑定将接收任务值作为参数,事件侦听器将接收一个值赋值,其中它将使任务变量等于事件值的值。
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
:value='task'
@input='task = $event.target.value'
class='taskInput'
/>
<button v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
它是如何工作的...
作为一种语法糖,v-model
指令可以自动为您声明绑定和元素的事件侦听器,但副作用是您无法完全控制可以实现的内容。
正如我们所见,绑定的值可以是变量、方法、计算属性或 Vuex getter,例如。对于事件侦听器,它可以是一个函数或一个变量赋值的直接声明。当事件被触发并传递给 Vue 时,$event
变量用于传递事件。在这种情况下,就像在普通 JavaScript 中一样,要捕获输入的值,我们需要使用event.target.value
值。
另请参阅
您可以在v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息
创建一个动态的待办事项列表
每个程序员在学习一种新语言时创建的第一个项目之一就是待办事项列表。这样做可以让我们更多地了解围绕状态和数据操作的语言过程。
我们将使用 Vue 制作我们的待办事项列表。我们将使用我们在之前的配方中学到和创建的内容。
准备工作
这个配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
制作待办事项应用程序涉及一些基本原则——它必须有一个任务列表;这些任务可以标记为已完成和未完成,并且列表可以进行过滤和排序。现在,我们将学习如何将任务添加到任务列表中。
要启动我们的组件,我们可以使用 Vue CLI 创建 Vue 项目,就像在第二章,介绍 TypeScript 和 Vue 生态系统中学到的“使用 Vue CLI 创建你的第一个项目”一节中所述,或者使用“从输入中删除 v-model”一节中的项目。
现在,按照以下步骤使用 Vue 和之前的方法创建一个动态的待办事项列表:
- 在
App.vue
文件中,我们将创建我们的任务数组。每当TaskInput.vue
组件发出消息时,这个任务将被填充。我们将向这个数组添加一个带有任务和创建任务的当前日期的对象。目前,任务完成时的日期将是未定义的。为了做到这一点,在组件的<script>
部分,我们需要创建一个接收任务并将该任务与当前日期添加到taskList
数组中的方法:
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput,
},
data: () => ({
taskList: [],
}),
methods:{
addNewTask(task){
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined,
})
},
},
}
</script>
- 现在,我们需要在
<template>
部分呈现这个列表。我们将使用 Vue 的v-for
指令来迭代任务列表。当我们将这个指令与数组一起使用时,它会给我们访问两个属性——项目本身和项目的索引。我们将使用项目来呈现它,使用索引来创建元素的键以进行呈现。我们需要添加一个复选框,当选中时,调用一个函数来改变任务的状态和任务完成时的显示:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in taskList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
始终重要的是要记住迭代器中的键必须是唯一的。这是因为呈现函数需要知道哪些元素已经改变。在这个例子中,我们将Math.random()
函数添加到索引中以生成一个唯一的键,因为当元素的数量减少时,数组的前几个元素的索引始终是相同的数字。
- 我们需要在
App.vue
的methods
属性上创建changeStatus
函数。这个函数将接收任务的索引作为参数,然后去改变任务数组中的finishedAt
属性,这是我们标记任务完成的标志:
changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
- 现在,我们需要将任务文本添加到屏幕的左侧。在组件的
<style>
部分,我们将使其具有作用域并添加自定义类:
<style scoped>
.taskList li{
text-align: left;
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件渲染并运行的地方:
工作原理...
当我们从组件接收到发射的消息时,我们用更多的数据来填充消息,并将其推送到本地数组变量中。
在模板中,我们遍历这个数组,使其成为任务列表。这显示了我们需要做的任务,标记任务完成时的复选框以及任务完成的时间。
当用户点击复选框时,它会执行一个函数,将当前任务标记为已完成。如果任务已经完成,函数将把finishedAt
属性设置为undefined
。
另请参阅
您可以在v3.vuejs.org/guide/list.html#mapping-an-array-to-elements-with-v-for
找到有关列表渲染的更多信息
您可以在v3.vuejs.org/guide/conditional.html#v-if
找到有关条件渲染的更多信息
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
找到有关Math.random
的更多信息。
创建计算属性并了解其工作原理
想象一下,每次您必须获取操作过的数据时,都需要执行一个函数。想象一下,您需要获取需要经过一些处理的特定数据,并且每次都需要通过函数执行它。这种类型的工作不容易维护。计算属性存在是为了解决这些问题。使用计算属性可以更容易地获取需要预处理甚至缓存的数据,而无需执行任何其他外部记忆函数。
准备工作
此示例的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
操作步骤...
我们将继续我们的待办事项项目,或者您可以按照第二章中学到的'使用 Vue CLI 创建您的第一个项目'中的步骤创建一个新的 Vue 项目,介绍 TypeScript 和 Vue 生态系统。
现在,按照以下步骤创建计算属性并了解其工作原理:
- 在
App.vue
文件的<script>
部分,我们将在data
和method
之间添加一个名为computed
的新属性。这是computed
属性将被放置的地方。我们将创建一个名为displayList
的新计算属性,它将用于在模板上呈现最终列表:
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList(){
return this.taskList;
},
},
methods: {
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
displayList
属性目前只返回变量的缓存值,而不是直接的变量本身。
- 现在,在
<template>
部分,我们需要更改列表的获取位置:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
它是如何工作的...
使用computed
属性将值传递到模板时,该值现在被缓存。这意味着只有在值更新时才会触发渲染过程。同时,我们确保模板不使用变量进行渲染,因此它不能在模板上更改,因为它是变量的缓存副本。
使用这个过程,我们可以获得最佳性能,因为我们不会浪费处理时间重新渲染对数据显示没有影响的更改的 DOM 树。这是因为如果有什么变化,而结果是相同的,computed
属性会缓存结果并不会更新最终结果。
另请参阅
您可以在v3.vuejs.org/guide/computed.html
找到有关计算属性的更多信息。
使用自定义过滤器显示更清晰的数据和文本
有时您可能会发现用户甚至您自己无法阅读 Unix 时间戳或其他DateTime
格式。我们如何解决这个问题?在 Vue 中渲染数据时,可以使用我们称之为过滤器的东西。
想象一系列管道,数据通过这些管道流动。数据以一种形式进入每个管道,以另一种形式退出。这就是 Vue 中的过滤器的样子。您可以在同一变量上放置一系列过滤器,因此它会被格式化,重塑,并最终以不同的数据显示,而代码保持不变。在这些管道中,初始变量的代码是不可变的。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
我们将继续我们的待办事项列表项目,或者您可以像在第二章中学到的那样,使用 Vue CLI 创建一个新的 Vue 项目,介绍 TypeScript 和 Vue 生态系统。
按照以下步骤创建您的第一个自定义 Vue 过滤器:
- 在
App.vue
文件的<script>
部分,在方法中,创建一个formatDate
函数。此函数将接收value
作为参数并进入过滤器管道。我们可以检查value
是否是一个数字,因为我们知道我们的时间是基于 Unix 时间戳格式的。如果它是一个数字,我们将根据当前浏览器位置进行格式化,并返回该格式化的值。如果传递的值不是一个数字,我们只需返回传递的值。
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList() {
return this.taskList;
}
},
methods: {
formatDate(value) {
if (!value) return '';
if (typeof value !== 'number') return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 在组件的
<template>
部分,我们需要将变量传递给过滤器方法。为此,我们需要找到taskItem.finishedAt
属性,并将其作为formatDate
方法的参数。我们将添加一些文本来表示任务在日期开始时已完成:
<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看组件,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
这是您的组件呈现并运行的样子:
工作原理...
过滤器是接收一个值并必须返回一个值以在文件的<template>
部分显示或在 Vue 属性中使用的方法。
当我们将值传递给formatDate
方法时,我们知道它是一个有效的 Unix 时间戳,因此可以调用新的Date
类构造函数,将value
作为参数传递,因为 Unix 时间戳是一个有效的日期构造函数。
我们过滤器背后的代码是Intl.DateTimeFormat
函数,这是一个原生函数,可用于格式化和解析日期到声明的位置。为了获得本地格式,我们使用navigator
全局变量。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。
使用 Vuelidate 添加表单验证
最初,JavaScript 仅用于在将 HTML 表单发送到服务器之前验证这些表单;我们没有任何 JavaScript 框架或今天我们拥有的 JavaScript 生态系统。然而,有一件事仍然是相同的:在将表单发送到服务器之前,应该首先由 JavaScript 引擎进行表单验证。
我们将学习如何使用 Vue 生态系统中最受欢迎的库之一,在发送之前验证我们的输入表单。
准备工作
这个配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
怎么做...
我们将继续我们的待办事项项目,或者您可以使用 Vue CLI 创建一个新的 Vue 项目,就像在第二章的“使用 Vue CLI 创建您的第一个项目”中学到的那样。
现在,按照以下步骤将表单验证添加到您的 Vue 项目和表单组件中:
- 要安装Vuelidate,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install vuelidate --save
- 要将 Vuelidate 插件添加到 Vue 应用程序中,我们需要在
src
文件夹中的main.js
文件中导入并添加它:
import Vue from 'vue';
import App from './App.vue';
import Vuelidate from 'vuelidate';
import './style.css';
Vue.config.productionTip = false
Vue.use(Vuelidate);
new Vue({
render: h => h(App),
}).$mount('#app')
- 在
TaskInput.vue
文件中,我们将向 Vue 对象添加一个新属性。这个属性由安装的新插件解释。在对象的末尾,我们将添加validations
属性,并在该属性内添加模型的名称。模型是插件将检查验证的数据或计算属性的直接名称:
<script>
export default {
name: 'TaskInput',
data: () => ({
task: ''
}),
methods: {
addTask() {
this.$emit('add-task', this.task);
this.task = '';
}
},
validations: {
task: {}
}
};
</script>
- 现在,我们需要导入已经存在于我们想要使用的插件上的规则,这些规则将是
required
和minLength
。导入后,我们将这些规则添加到模型中:
<script>
import { required, minLength } from 'vuelidate/lib/validators';
export default {
name: 'TaskInput',
data: () => ({
task: ''
}),
methods: {
addTask() {
this.$emit('add-task', this.task);
this.task = '';
}
},
validations: {
task: {
required,
minLength: minLength(5),
}
}
};
</script>
- 现在,我们需要在发出事件之前添加验证。我们将使用
$touch
内置函数告诉插件该字段已被用户触摸,并进行验证。如果有任何字段与用户有任何交互,插件将相应地设置标志。如果没有错误,我们将发出事件,并使用$reset
函数重置验证。为此,我们将更改addTask
方法:
addTask() {
this.$v.task.$touch();
if (this.$v.task.$error) return false;
this.$emit('add-task', this.task);
this.task = '';
this.$v.task.$reset();
return true;
}
- 为了提醒用户字段上存在一些错误,我们将使输入更改样式为完整的红色边框,并具有红色文本。为此,我们需要在输入字段上创建一个条件类。这将直接附加到模型的
$error
属性上:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
:value='task'
@input='task = $event.target.value'
class='taskInput'
:class="$v.task.$error ? 'fieldError' : ''"
/>
<button v-on:click='addTask'>Add Task</button>
</div>
</div>
</template>
- 对于类,我们可以在
src
文件夹中的style.css
文件中创建一个fieldError
类:
.fieldError {
border: 2px solid red !important;
color: red;
border-radius: 3px;
}
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> **npm run serve**
这是您的组件呈现并运行的方式:
它是如何工作的...
安装后,Vuelidate 插件会向 Vue 原型添加一个新的$v
属性,并在 Vue 对象中检查一个名为validations
的新对象属性。当定义了此属性并具有一些规则时,插件会在每次更新时检查模型的规则。
使用这个新的 Vue 原型,我们可以在我们的代码中检查我们定义的规则内的错误,并执行函数来告诉插件该字段已被用户触摸以标记为脏字段或重置它。使用这些功能,我们能够根据我们在任务模型上定义的规则添加一个新的条件类。
任务模型是必需的,并且至少有五个字符。如果不满足这些规则,插件将标记模型有错误。我们获取此错误并使用它来向用户显示任务字段有活动错误。当用户满足要求时,错误的显示消失,事件可以被触发。
另请参阅
您可以在vuelidate.netlify.com/
找到有关 Vuelidate 的更多信息。
您可以在v3.vuejs.org/guide/class-and-style.html
找到有关类和样式绑定的更多信息
为列表创建过滤器和排序器
在处理列表时,通常会发现自己有原始数据。有时,您需要对这些数据进行过滤,以便用户可以阅读。为此,我们需要一组计算属性来形成最终的过滤器和排序器。
在这个示例中,我们将学习如何创建一个简单的过滤器和排序器,来控制我们最初的待办任务列表。
准备工作
此示例的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
我们将继续我们的待办事项列表项目,或者您可以像在第二章中学到的那样,使用 Vue CLI 创建一个新的 Vue 项目,介绍 TypeScript 和 Vue 生态系统。
按照以下步骤添加一组过滤器和排序到您的列表中:
- 在
App.vue
文件的<script>
部分,我们将添加新的计算属性;这些将用于排序和过滤。我们将添加三个新的计算属性,baseList
,filteredList
和sortedList
。baseList
属性将是我们的第一个操作。我们将通过Array.map
向任务列表添加一个id
属性。由于 JavaScript 数组从零开始,我们将在数组的索引上添加1
。filteredList
属性将过滤baseList
属性,并返回未完成的任务,sortedList
属性将对filteredList
属性进行排序,以便最后添加的id
属性将首先显示给用户:
<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return [...this.baseList]
.filter(t => !t.finishedAt);
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => b.id - a.id);
},
displayList() {
return this.sortedList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 在
<template>
部分,我们将Task ID
添加为指示器,并更改changeStatus
方法发送参数的方式。因为现在索引是可变的,我们不能将其用作变量;它只是数组上的临时索引。我们需要使用任务id
:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 在
changeStatus
方法中,我们也需要更新我们的函数。由于索引现在从1
开始,我们需要将数组的索引减一,以获取更新前元素的真实索引:
changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
这是您的组件呈现和运行的方式:
它是如何工作的...
computed
属性一起作为列表的缓存工作,并确保对元素的操作没有副作用:
-
在baseList
属性中,我们创建了一个新数组,其中包含相同的任务,但为任务添加了一个新的id
属性。
-
在filteredList
属性中,我们取出了baseList
属性,并且只返回了未完成的任务。
-
在sortedList
属性上,我们按照它们的 ID,按降序对filteredList
属性上的任务进行排序。
当所有操作完成时,displayList
属性将返回被操作的数据的结果。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。
创建条件过滤器以对列表数据进行排序
完成上一个食谱后,您的数据应该已经被过滤和排序,但您可能需要检查过滤后的数据或需要更改排序方式。在这个食谱中,我们将学习如何创建条件过滤器并对列表上的数据进行排序。
使用一些基本原则,可以收集信息并以许多不同的方式显示它。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
我们将继续我们的待办事项列表项目,或者您可以按照在第二章中学到的,在 Vue CLI 中创建一个新的 Vue 项目,介绍 TypeScript 和 Vue 生态系统。
现在,按照以下步骤添加条件过滤器以对列表数据进行排序:
- 在
App.vue
文件的<script>
部分,我们将更新computed
属性,filteredList
,sortedList
和displayList
。我们需要向我们的项目添加三个新变量,hideDone
,reverse
和sortById
。所有三个变量都将是布尔变量,并且默认值为false
。filteredList
属性将检查hideDone
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将显示整个列表而不进行任何过滤。sortedList
属性将检查sortById
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将按任务完成日期对列表进行排序。displayList
属性将检查reverse
变量是否为true
。如果是,它将颠倒显示的列表,但如果不是,它将具有相同的行为:
<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
hideDone: false,
reverse: false,
sortById: false,
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return this.hideDone
? [...this.baseList]
.filter(t => !t.finishedAt)
: [...this.baseList];
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => (
this.sortById
? b.id - a.id
: (a.finishedAt || 0) - (b.finishedAt || 0)
));
},
displayList() {
const taskList = [...this.sortedList];
return this.reverse
? taskList.reverse()
: taskList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
- 在
<template>
部分,我们需要为这些变量添加控制器。我们将创建三个复选框,直接通过v-model
指令与变量链接:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现和运行的方式:
工作原理...
computed
属性一起作为列表的缓存工作,并确保在元素操作中没有任何副作用。通过条件处理,可以通过变量更改过滤和排序规则,并且显示会实时更新:
-
在filteredList
属性处,我们取出了baseList
属性,并返回了未完成的任务。当hideDone
变量为false
时,我们返回整个列表而不进行任何过滤。
-
在sortedList
属性处,我们对filteredList
属性上的任务进行了排序。当sortById
变量为true
时,列表按 ID 降序排序;当为false
时,按任务完成时间升序排序。
-
在displayList
属性处,当reverse
变量为true
时,最终列表被颠倒。
当所有操作都完成时,displayList
属性返回了被操作的数据的结果。
这些computed
属性由用户屏幕上的复选框控制,因此用户可以完全控制他们可以看到什么以及如何看到它。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。
添加自定义样式和过渡效果
在组件中添加样式是一个很好的做法,因为它可以让用户更清楚地看到发生了什么。通过这样做,您可以向用户显示视觉响应,也可以为您的应用程序提供更好的体验。
在这个示例中,我们将学习如何添加一种新的条件类绑定。我们将使用 CSS 效果与每个新的 Vue 更新带来的重新渲染相结合。
准备就绪
此处的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
我们将继续我们的待办事项清单项目,或者您可以像在第二章“使用 Vue CLI 创建您的第一个项目”中学到的那样,使用 Vue CLI 创建一个新的 Vue 项目。
按照以下步骤为您的组件添加自定义样式和过渡效果:
- 在
App.vue
文件中,我们将为已完成的任务的列表项添加一个条件类:
<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
:class="!!taskItem.finishedAt ? 'taskDone' : ''"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
- 在组件的
<style>
部分,我们将为taskDone
的 CSS 类创建 CSS 样式表类。我们需要让列表项之间有一个分隔符;然后,我们将使列表具有条纹样式;当它们被标记为完成时,背景将发生变化。要在行之间添加分隔符和条纹列表或斑马样式,我们需要添加一个 CSS 样式表规则,适用于我们列表的每个even nth-child
:
<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
</style>
- 在
<style>
部分的末尾添加 CSS 动画关键帧,指示背景颜色变化,并将此动画应用于.taskDone
CSS 类,以在任务完成时添加背景效果
<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
@keyframes colorChange {
from{
background-color: inherit;
}
to{
background-color: rgba(0, 160, 24, 0.577);
}
}
.taskList li.taskDone{
animation: colorChange 1s ease;
background-color: rgba(0, 160, 24, 0.577);
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现和运行的地方:
它是如何工作的...
每当我们的应用程序中的新项目被标记为已完成时,displayList
属性都会更新并触发组件的重新渲染。
因此,我们的taskDone
CSS 类附加了一个在渲染时执行的动画,显示绿色背景。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
找到有关 CSS 动画的更多信息。
您可以在v3.vuejs.org/guide/class-and-style.html
找到有关类和样式绑定的更多信息
使用 vue-devtools 调试您的应用程序
vue-devtools
对于每个 Vue 开发人员都是必不可少的。这个工具向我们展示了 Vue 组件、路由、事件和 vuex 的深度。
借助vue-devtools
扩展程序,可以调试我们的应用程序,在更改代码之前尝试新数据,执行函数而无需直接在代码中调用它们,等等。
在这个配方中,我们将学习如何使用 devtools 找到有关您的应用程序的更多信息,以及如何使用它来帮助您的调试过程。
准备就绪
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
您需要在浏览器中安装vue-devtools
扩展程序:
-
Chrome 扩展程序-bit.ly/chrome-vue-devtools
-
Firefox 扩展程序-bit.ly/firefox-vue-devtools
如何做...
我们将继续进行待办事项列表项目,或者您可以按照第二章,介绍 TypeScript 和 Vue 生态系统中学到的内容,使用 Vue CLI 创建一个新的 Vue 项目。
在开发任何 Vue 应用程序时,始终最好使用vue-devtools
进行开发。
按照这些步骤来了解如何使用vue-devtools
以及如何正确调试 Vue 应用程序:
- 要进入
vue-devtools
,首先需要在浏览器中安装它,所以请查看本教程的“准备就绪”部分,获取 Chrome 或 Firefox 的扩展链接。在 Vue 开发应用程序中,进入浏览器开发者检查器模式。一个名为 Vue 的新标签页必须出现:
- 您首先看到的是组件标签页。该标签页显示了您的应用程序组件树。如果单击组件,您将能够查看所有可用数据,计算属性,以及由插件(如
vuelidate
,vue-router
或vuex
)注入的额外数据。您可以编辑数据以实时查看应用程序中的更改:
- 第二个标签页是用于vuex 开发的。该标签页将显示变化的历史记录、当前状态和 getter。可以检查每个变化传递的有效负载,并进行时间旅行变化,以在 vuex 状态中“回到过去”:
- 第三个标签页专门用于应用程序中的事件发射器。在此处显示了应用程序中发射的所有事件。您可以通过单击事件来检查发射的事件。您可以查看事件的名称、类型,事件的来源(在本例中是一个组件),以及有效负载:
- 第四个标签页专门用于vue-router插件。在那里,您可以查看导航历史记录,以及传递给新路由的所有元数据。您可以查看应用程序中所有可用的路由:
- 第五个标签页是性能标签页。在这里,您可以检查组件的加载时间,应用程序运行的每秒帧数,以及实时发生的事件。第一张截图显示了当前应用程序的每秒帧数,以及所选组件的每秒帧数:
第二张截图显示了组件生命周期钩子的性能,以及执行每个钩子所需的时间:
- 第六个标签是您的设置标签;在这里,您可以管理扩展程序,更改外观,内部行为以及在 Vue 插件中的行为方式:
- 最后一个标签是
vue-devtools
的刷新按钮。有时,当hot-module-reload
发生或者应用程序组件树中发生一些复杂事件时,扩展程序可能会失去对发生情况的跟踪。这个按钮强制扩展程序重新加载并再次读取 Vue 应用程序状态。
另请参阅
您可以在github.com/vuejs/vue-devtools
找到有关vue-devtools
的更多信息。
第四章:组件,混合和功能组件
构建 Vue 应用程序就像拼图一样。拼图的每一块都是一个组件,每一块都有一个插槽要填充。
组件在 Vue 开发中扮演着重要的角色。在 Vue 中,您的代码的每一部分都将是一个组件——它可以是布局,页面,容器或按钮,但最终,它都是一个组件。学习如何与它们交互并重用它们是清理代码和提高 Vue 应用性能的关键。组件是最终会在屏幕上呈现出某些东西的代码,无论大小如何。
在本章中,我们将学习如何制作一个可视化组件,可以在许多地方重复使用。我们将使用插槽将数据放入我们的组件中,为了严格快速的渲染,创建功能性组件,实现父子组件之间的直接通信,最后,看看如何异步加载您的组件。
让我们把所有这些部分放在一起,创建一个美丽的拼图,即 Vue 应用程序。
在本章中,我们将涵盖以下配方:
-
创建一个可视化模板组件
-
使用插槽和命名插槽将数据放入您的组件中
-
将数据传递给您的组件并验证数据
-
创建功能性组件
-
访问您的子组件数据
-
创建一个动态注入的组件
-
创建一个依赖注入组件
-
创建一个组件mixin
-
延迟加载您的组件
技术要求
在本章中,我们将使用Node.js和Vue-CLI。
注意 Windows 用户:您需要安装一个名为windows-build-tools
的 NPM 包,以便能够安装以下所需的包。为此,请以管理员身份打开 PowerShell 并执行以下命令:
npm install -g windows-build-tools
要安装Vue-CLI,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
创建一个可视化模板组件
组件可以是数据驱动的,无状态的,有状态的,或者是一个简单的可视化组件。但是什么是可视化组件?可视化组件是一个只有一个目的的组件:可视化操作。
一个可视化组件可以有一个简单的带有一些div
HTML 元素的作用域 CSS,或者它可以是一个更复杂的组件,可以实时计算元素在屏幕上的位置。
我们将创建一个遵循 Material Design 指南的卡片包装组件。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用带有 Vue-CLI 的 Vue 项目,就像我们在第二章中的“使用 Vue CLI 创建您的第一个项目”食谱中所做的那样,介绍 TypeScript 和 Vue 生态系统**,或者我们可以开始一个新的项目。
要启动一个新项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> vue create visual-component
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,空格键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
现在,让我们按照这些步骤创建一个可视化模板组件:
-
让我们在src/components
文件夹中创建一个名为MaterialCardBox.vue
的新文件。
-
在这个文件中,我们将从组件的模板开始。我们需要为卡片创建一个框。通过使用 Material Design 指南,这个框将有阴影和圆角:
<template>
<div class="cardBox elevation_2">
<div class="section">
This is a Material Card Box
</div>
</div>
</template>
- 在我们组件的
<script>
部分中,我们将只添加我们的基本名称:
<script>
export default {
name: 'MaterialCardBox',
};
</script>
- 我们需要创建我们的高程 CSS 样式表规则。为此,请在
style
文件夹中创建一个名为elevation.css
的文件。在那里,我们将创建从0
到24
的高程,以遵循 Material Design 指南上的所有高程:
.elevation_0 {
border: 1px solid rgba(0, 0, 0, 0.12);
}
.elevation_1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2),
0 1px 1px rgba(0, 0, 0, 0.14),
0 2px 1px -1px rgba(0, 0, 0, 0.12);
}
.elevation_2 {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.elevation_3 {
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2),
0 3px 4px rgba(0, 0, 0, 0.14),
0 3px 3px -2px rgba(0, 0, 0, 0.12);
}
.elevation_4 {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 4px 5px rgba(0, 0, 0, 0.14),
0 1px 10px rgba(0, 0, 0, 0.12);
}
.elevation_5 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
}
.elevation_6 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 6px 10px rgba(0, 0, 0, 0.14),
0 1px 18px rgba(0, 0, 0, 0.12);
}
.elevation_7 {
box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2),
0 7px 10px 1px rgba(0, 0, 0, 0.14),
0 2px 16px 1px rgba(0, 0, 0, 0.12);
}
.elevation_8 {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.elevation_9 {
box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.2),
0 9px 12px 1px rgba(0, 0, 0, 0.14),
0 3px 16px 2px rgba(0, 0, 0, 0.12);
}
.elevation_10 {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12);
}
.elevation_11 {
box-shadow: 0 6px 7px -4px rgba(0, 0, 0, 0.2),
0 11px 15px 1px rgba(0, 0, 0, 0.14),
0 4px 20px 3px rgba(0, 0, 0, 0.12);
}
.elevation_12 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 12px 17px 2px rgba(0, 0, 0, 0.14),
0 5px 22px 4px rgba(0, 0, 0, 0.12);
}
.elevation_13 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
}
.elevation_14 {
box-shadow: 0 7px 9px -4px rgba(0, 0, 0, 0.2),
0 14px 21px 2px rgba(0, 0, 0, 0.14),
0 5px 26px 4px rgba(0, 0, 0, 0.12);
}
.elevation_15 {
box-shadow: 0 8px 9px -5px rgba(0, 0, 0, 0.2),
0 15px 22px 2px rgba(0, 0, 0, 0.14),
0 6px 28px 5px rgba(0, 0, 0, 0.12);
}
.elevation_16 {
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
0 16px 24px 2px rgba(0, 0, 0, 0.14),
0 6px 30px 5px rgba(0, 0, 0, 0.12);
}
.elevation_17 {
box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2),
0 17px 26px 2px rgba(0, 0, 0, 0.14),
0 6px 32px 5px rgba(0, 0, 0, 0.12);
}
.elevation_18 {
box-shadow: 0 9px 11px -5px rgba(0, 0, 0, 0.2),
0 18px 28px 2px rgba(0, 0, 0, 0.14),
0 7px 34px 6px rgba(0, 0, 0, 0.12);
}
.elevation_19 {
box-shadow: 0 9px 12px -6px rgba(0, 0, 0, 0.2),
0 19px 29px 2px rgba(0, 0, 0, 0.14),
0 7px 36px 6px rgba(0, 0, 0, 0.12);
}
.elevation_20 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 20px 31px 3px rgba(0, 0, 0, 0.14),
0 8px 38px 7px rgba(0, 0, 0, 0.12);
}
.elevation_21 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 21px 33px 3px rgba(0, 0, 0, 0.14),
0 8px 40px 7px rgba(0, 0, 0, 0.12);
}
.elevation_22 {
box-shadow: 0 10px 14px -6px rgba(0, 0, 0, 0.2),
0 22px 35px 3px rgba(0, 0, 0, 0.14),
0 8px 42px 7px rgba(0, 0, 0, 0.12);
}
.elevation_23 {
box-shadow: 0 11px 14px -7px rgba(0, 0, 0, 0.2),
0 23px 36px 3px rgba(0, 0, 0, 0.14),
0 9px 44px 8px rgba(0, 0, 0, 0.12);
}
.elevation_24 {
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14),
0 9px 46px 8px rgba(0, 0, 0, 0.12);
}
- 为了在组件的
<style>
部分中设置样式,我们需要在<style>
标签内设置scoped
属性,以确保视觉样式不会干扰应用程序中的任何其他组件。我们将使这张卡遵循 Material Design 指南。我们需要导入Roboto
字体系列并将其应用于将包装在此组件内的所有元素:
<style scoped>
@import url('https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap');
@import '../style/elevation.css';
*{
font-family: 'Roboto', sans-serif;
}
.cardBox{
width: 100%;
max-width: 300px;
background-color: #fff;
position: relative;
display: inline-block;
border-radius: 0.25rem;
}
.cardBox > .section {
padding: 1rem;
position: relative;
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
可视化组件是一个将包装任何组件并使用自定义样式放置包装数据的组件。由于此组件与其他组件混合,它可以形成一个新的组件,而无需在代码中重新应用或重写任何样式。
参见
您可以在vue-loader.vuejs.org/guide/scoped-css.html#child-component-root-elements
找到有关作用域 CSS 的更多信息。
您可以在material.io/components/cards/
找到有关 Material Design 卡片的更多信息。
在fonts.google.com/specimen/Roboto
上查看 Roboto 字体系列。
使用插槽和命名插槽在组件中放置数据
有时候,拼图的一些部分会丢失,你会发现自己有一个空白的地方。想象一下,你可以用自己制作的一块填充那个空白的地方,而不是原来随拼图盒子一起的那块。这是 Vue 插槽的一个粗略类比。
Vue 插槽就像是组件中的开放空间,其他组件可以用文本、HTML 元素或其他 Vue 组件填充。您可以在组件中声明插槽的位置和行为方式。
使用这种技术,您可以创建一个组件,并在需要时轻松自定义它。
准备工作
这个配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要开始我们的组件,我们可以像在第二章的使用 Vue CLI 创建您的第一个项目中那样使用 Vue-CLI 创建我们的 Vue 项目,或者使用创建可视化模板组件中的项目。
按照以下说明在组件中创建插槽和命名插槽:
-
让我们打开组件文件夹中的名为MaterialCardBox.vue
的文件。
-
在组件的<template>
部分,我们需要在卡片上添加四个主要部分。这些部分基于 Material Design 卡片解剖学,分别是header
、media
、main section
和action
区域。我们将使用默认插槽来放置main section
,其余部分都将是命名作用域。对于一些命名插槽,我们将添加一个备用配置,如果用户没有在插槽上选择任何设置,将显示该配置:
<template>
<div class="cardBox elevation_2">
<div class="header">
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
Card Header
</h1>
<h2 class="cardSubHeader cardText">
Card Sub Header
</h2>
</div>
</div>
<div class="media">
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
src="https://via.placeholder.com/350x250"
>
</div>
<div
v-if="$slots.default"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot />
</div>
<div
v-if="$slots.action"
class="action"
>
<slot name="action" />
</div>
</div>
</template>
- 现在,我们需要为组件创建文本 CSS 样式表规则。在
style
文件夹中,创建一个名为cardStyles.css
的新文件,在那里我们将添加卡片文本和标题的规则:
h1, h2, h3, h4, h5, h6{
margin: 0;
}
.cardText{
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-decoration: inherit;
text-transform: inherit;
font-size: 0.875rem;
line-height: 1.375rem;
letter-spacing: 0.0071428571em;
}
h1.cardHeader{
font-size: 1.25rem;
line-height: 2rem;
font-weight: 500;
letter-spacing: .0125em;
}
h2.cardSubHeader{
font-size: .875rem;
line-height: 1.25rem;
font-weight: 400;
letter-spacing: .0178571429em;
opacity: .6;
}
- 在组件的
<style>
部分,我们需要创建一些 CSS 样式表来遵循我们的设计指南的规则:
<style scoped>
@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap");
@import "../style/elevation.css";
@import "../style/cardStyles.css";
* {
font-family: "Roboto", sans-serif;
}
.cardBox {
width: 100%;
max-width: 300px;
border-radius: 0.25rem;
background-color: #fff;
position: relative;
display: inline-block;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.cardBox > .header {
padding: 1rem;
position: relative;
display: block;
}
.cardBox > .media {
overflow: hidden;
position: relative;
display: block;
max-width: 100%;
}
.cardBox > .section {
padding: 1rem;
position: relative;
margin-bottom: 1.5rem;
display: block;
}
.cardBox > .action {
padding: 0.5rem;
position: relative;
display: block;
}
.cardBox > .action > *:not(:first-child) {
margin-left: 0.4rem;
}
.noBottomPadding {
padding-bottom: 0 !important;
}
.halfPaddingTop {
padding-top: 0.5rem !important;
}
</style>
- 在
src
文件夹中的App.vue
文件中,我们需要向这些插槽添加元素。这些元素将被添加到每个命名插槽和默认插槽中。我们将更改文件的<template>
部分中的组件。要添加命名插槽,我们需要使用一个名为v-slot:
的指令,然后是我们想要使用的插槽的名称:
<template>
<div id="app">
<MaterialCardBox>
<template v-slot:header>
<strong>Card Title</strong><br>
<span>Card Sub-Title</span>
</template>
<template v-slot:media>
<img src="https://via.placeholder.com/350x150">
</template>
<p>Main Section</p>
<template v-slot:action>
<button>Action Button</button>
<button>Action Button</button>
</template>
</MaterialCardBox>
</div>
</template>
对于默认插槽,我们不需要使用指令;它只需要包装在组件中,以放置在组件的<slot />
部分中。
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件渲染并运行的方式:
它是如何工作的...
插槽是可以放置任何可以呈现到 DOM 中的地方。我们选择插槽的位置,并告诉组件在接收到任何信息时在何处呈现。
在这个教程中,我们使用了命名插槽,它们旨在与需要多个插槽的组件一起使用。要在 Vue 单文件(.vue
)的<template>
部分中向该组件放置任何信息,您需要添加v-slot:
指令,以便 Vue 能够知道在何处放置传递下来的信息。
另请参阅
您可以在vuejs.org/v2/guide/components-slots.html
找到有关 Vue 插槽的更多信息。
您可以在material.io/components/cards/#anatomy
找到有关 Material Design 卡片解剖的更多信息。
向您的组件传递数据并验证数据
您现在知道如何通过插槽将数据放入组件中,但这些插槽是为 HTML DOM 元素或 Vue 组件而设计的。有时,您需要传递诸如字符串、数组、布尔值甚至对象之类的数据。
整个应用程序就像一个拼图,其中每个部分都是一个组件。组件之间的通信是其中的重要部分。向组件传递数据的可能性是连接拼图的第一步,然后验证数据是连接这些部分的最后一步。
在这个教程中,我们将学习如何向组件传递数据并验证传递给组件的数据。
准备工作
先决条件如下:
- Node.js 12+
Node.js 所需的全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以像在第二章“介绍 TypeScript 和 Vue 生态系统”中的使用 Vue CLI 创建您的第一个项目食谱中那样,使用 Vue-CLI 创建我们的 Vue 项目,或者使用使用插槽和命名插槽将数据放入组件食谱中的项目。
按照这些说明将数据传递给组件并进行验证:
-
让我们在src/components
文件夹中打开名为MaterialCardBox.vue
的文件。
-
在组件的<script>
部分,我们创建一个名为props
的新属性。该属性接收组件数据,该数据可以用于视觉操作、代码内的变量或需要执行的函数。在此属性中,我们需要声明属性的名称、类型、是否必需以及验证函数。此函数将在运行时执行,以验证传递的属性是否有效:
<script>
export default {
name: 'MaterialCardBox',
inheritAttrs: false,
props: {
header: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
subHeader: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
mainText: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
showMedia: {
type: Boolean,
required: false,
default: false,
validator: v => typeof v === 'boolean',
},
imgSrc: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
showActions: {
type: Boolean,
required: false,
default: false,
validator: v => typeof v === 'boolean',
},
elevation: {
type: Number,
required: false,
default: 2,
validator: v => typeof v === 'number',
},
},
computed: {},
};
</script>
- 在组件的
<script>
部分的computed
属性中,我们需要创建一组用于呈现卡片的视觉操作规则。这些规则将是showMediaContent
、showActionsButtons
、showHeader
和cardElevation
。每个规则将检查接收到的props
和$slots
对象,以查看是否需要呈现相关的卡片部分:
computed: {
showMediaContent() {
return (this.$slots.media || this.imgSrc) && this.showMedia;
},
showActionsButtons() {
return this.showActions && this.$slots.action;
},
showHeader() {
return this.$slots.header || (this.header || this.subHeader);
},
showMainContent() {
return this.$slots.default || this.mainText;
},
cardElevation() {
return `elevation_${parseInt(this.elevation, 10)}`;
},
},
- 在添加了视觉操作规则之后,我们需要将创建的规则添加到组件的
<template>
部分。它们将影响我们卡片的外观和行为。例如,如果没有定义头部插槽,并且定义了头部属性,我们将显示备用头部。该头部是通过props
传递下来的数据:
<template>
<div
class="cardBox"
:class="cardElevation"
>
<div
v-if="showHeader"
class="header"
>
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
{{ header }}
</h1>
<h2 class="cardSubHeader cardText">
{{ subHeader }}
</h2>
</div>
</div>
<div
v-if="showMediaContent"
class="media"
>
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
:src="imgSrc"
>
</div>
<div
v-if="showMainContent"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot v-if="$slots.default" />
<p
v-else
class="cardText"
>
{{ mainText }}
</p>
</div>
<div
v-if="showActionsButtons"
class="action"
>
<slot
v-if="$slots.action"
name="action"
/>
</div>
</div>
</template>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行:
工作原理...
每个 Vue 组件都是一个具有渲染函数的 JavaScript 对象。当需要在 HTML DOM 中呈现它时,将调用此渲染函数。单文件组件是该对象的抽象。
当我们声明我们的组件具有可以传递的唯一 props 时,它为其他组件或 JavaScript 打开了一个小门,以便将信息放入我们的组件中。然后,我们可以在组件内使用这些值来渲染数据,进行一些计算或制定可视规则。
在我们的情况下,使用单文件组件,我们将这些规则作为 HTML 属性传递,因为 vue-template-compiler
将获取这些属性并将它们转换为 JavaScript 对象。
当这些值传递给我们的组件时,Vue 首先检查传递的属性是否与正确的类型匹配,然后我们在每个值上执行我们的验证函数,以查看它是否与我们期望的匹配。
完成所有这些后,组件的生命周期将继续,我们可以渲染我们的组件。
另请参阅
您可以在 vuejs.org/v2/guide/components-props.html
找到有关 props
的更多信息。
您可以在 vue-loader.vuejs.org/guide/
找到有关 vue-template-compiler
的更多信息。
创建功能组件
功能组件的美妙之处在于它们的简单性。它们是无状态组件,没有任何数据、计算属性,甚至没有生命周期。它们只是在传递的数据发生变化时调用的渲染函数。
您可能想知道这有什么用。嗯,功能组件是 UI 组件的完美伴侣,这些组件不需要在内部保留任何数据,或者只是渲染组件,不需要任何数据操作的可视组件。
顾名思义,它们是简单的函数组件,除了渲染函数外没有其他内容。它们是组件的精简版本,专门用于性能渲染和可视元素。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,请使用 Vue-CLI 创建您的 Vue 项目,就像我们在第二章“引入 TypeScript 和 Vue 生态系统”中的食谱“使用 Vue CLI 创建您的第一个项目”中所做的那样,或者使用“将数据传递给您的组件并验证数据”的食谱中的项目。
现在,按照以下说明创建一个 Vue 功能组件:
-
在src/components
文件夹中创建一个名为MaterialButton.vue
的新文件。
-
在这个组件中,我们需要验证我们将接收的 prop 是否是有效的颜色。为此,在项目中安装is-color
模块。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save is-color
- 在我们组件的
<script>
部分,我们需要创建功能组件将接收的props
对象。由于功能组件只是一个没有状态的渲染函数,<script>
部分被简化为props
、injections
和slots
。将有四个props
对象:backgroundColor
、textColor
、isRound
和isFlat
。在安装组件时,这些不是必需的,因为我们在props
中定义了默认值:
<script>
import isColor from 'is-color';
export default {
name: 'MaterialButton',
props: {
backgroundColor: {
type: String,
required: false,
default: '#fff',
validator: v => typeof v === 'string' && isColor(v),
},
textColor: {
type: String,
required: false,
default: '#000',
validator: v => typeof v === 'string' && isColor(v),
},
isRound: {
type: Boolean,
required: false,
default: false,
},
isFlat: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
- 在我们组件的
<template>
部分,我们首先需要向<template>
标签添加functional
属性,以指示vue-template-compiler
这个组件是一个功能组件。我们需要创建一个按钮 HTML 元素,带有基本的class
属性按钮和一个基于props
对象接收的动态class
属性。与普通组件不同,我们需要指定props
属性以使用功能组件。对于按钮的样式,我们需要创建一个基于props
的动态style
属性。为了直接将所有事件监听器传递给父组件,我们可以调用v-on
指令并传递listeners
属性。这将绑定所有事件监听器,而无需声明每一个。在按钮内部,我们将添加一个用于视觉增强的div
HTML 元素,并添加<slot>
,文本将放置在其中:
<template functional>
<button
tabindex="0"
class="button"
:class="{
round: props.isRound,
isFlat: props.isFlat,
}"
:style="{
background: props.backgroundColor,
color: props.textColor
}"
v-on="listeners"
>
<div
tabindex="-1"
class="button_focus_helper"
/>
<slot/>
</button>
</template>
- 现在,让我们把它弄得漂亮一点。在组件的
<style>
部分,我们需要为这个按钮创建所有的 CSS 样式表规则。我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响我们应用程序中的任何其他元素:
<style scoped>
.button {
user-select: none;
position: relative;
outline: 0;
border: 0;
border-radius: 0.25rem;
vertical-align: middle;
cursor: pointer;
padding: 4px 16px;
font-size: 14px;
line-height: 1.718em;
text-decoration: none;
color: inherit;
background: transparent;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
min-height: 2.572em;
font-weight: 500;
text-transform: uppercase;
}
.button:not(.isFlat){
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
}
.button_focus_helper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
outline: 0;
opacity: 0;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.4s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button_focus_helper:after, .button_focus_helper:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
border-radius: inherit;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button_focus_helper:before {
background: #000;
}
.button_focus_helper:after {
background: #fff;
}
.button:focus .button_focus_helper:before,
.button:hover .button_focus_helper:before {
opacity: .1;
}
.button:focus .button_focus_helper:after,
.button:hover .button_focus_helper:after {
opacity: .6;
}
.button:focus .button_focus_helper,
.button:hover .button_focus_helper {
opacity: 0.2;
}
.round {
border-radius: 50%;
}
</style>
- 运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件渲染并运行的地方:
它是如何工作的...
功能组件就像一个渲染函数一样简单。它们没有任何类型的数据、函数或者对外部世界的访问。
它们最初作为 JavaScript 对象render()
函数在 Vue 中引入;后来,它们被添加到了vue-template-compiler
中,用于 Vue 单文件应用程序。
功能组件通过接收两个参数来工作:createElement
和context
。正如我们在单文件中看到的,我们只能访问元素,因为它们不在 JavaScript 对象的this
属性中。这是因为当上下文传递给渲染函数时,就没有this
属性。
功能组件在 Vue 上提供了最快的渲染速度,因为它不依赖于组件的生命周期来检查渲染;它只是在数据更改时每次渲染。
另请参阅
您可以在vuejs.org/v2/guide/render-function.html#Functional-Components
找到有关功能组件的更多信息。
您可以在www.npmjs.com/package/is-color
找到有关is-color
模块的更多信息。
访问您的子组件数据
通常,父子通信是通过事件或 props 来完成的。但有时,您需要访问存在于子组件或父组件函数中的数据、函数或计算属性。
Vue 提供了一种双向交互的方式,打开了通信和事件的大门,例如 props 和事件监听器。
还有另一种访问组件之间数据的方式:通过直接访问。这可以通过在单文件组件中使用模板时使用特殊属性,或者在 JavaScript 中直接调用对象来完成。这种方法被一些人认为有点懒惰,但有时确实没有其他方法可以做到这一点。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动您的组件,请使用 Vue-CLI 创建您的 Vue 项目,就像我们在第二章的'使用 Vue CLI 创建您的第一个项目'食谱中所做的那样,介绍 TypeScript 和 Vue 生态系统,或者使用'创建功能组件'食谱中的项目。
我们将把这个教程分成四个部分。前三部分将涵盖新组件的创建——StarRatingInput
、StarRatingDisplay
和StarRating
——最后一部分将涵盖数据和函数访问的父子直接操作。
创建星级评分输入
我们将创建一个基于五星级评分系统的星级评分输入。
按照以下步骤创建自定义星级评分输入:
-
在src/components
文件夹中创建一个名为StarRatingInput.vue
的新文件。
-
在组件的<script>
部分,在props
属性中创建一个maxRating
属性,它是一个数字,非必需,并且默认值为5
。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
。在methods
属性中,我们需要创建三个方法:updateRating
、emitFinalVoting
和getStarName
。updateRating
方法将保存评分到数据中,emitFinalVoting
将调用updateRating
并通过final-vote
事件将评分传递给父组件,getStarName
将接收一个值并返回星级的图标名称。
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
- 在组件的
<template>
部分,我们需要创建一个<slot>
组件来放置星级评分之前的文本。我们将根据通过props
属性接收到的maxRating
值创建一个动态星级列表。创建的每个星级都将在mouseenter
、focus
和click
事件上附加一个监听器。当触发mouseenter
和focus
时,将调用updateRating
方法,而click
将调用emitFinalVote
方法。
<template>
<div class="starRating">
<span class="rateThis">
<slot />
</span>
<ul>
<li
v-for="rate in maxRating"
:key="rate"
@mouseenter="updateRating(rate)"
@click="emitFinalVote(rate)"
@focus="updateRating(rate)"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
</div>
</template>
- 我们需要将 Material Design 图标导入我们的应用程序。在
styles
文件夹中创建一个名为materialIcons.css
的新样式文件,并添加font-family
的 CSS 样式规则。
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-
Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons' !important;
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
- 打开
main.js
文件,并将创建的样式表导入其中。css-loader
将处理 JavaScript 文件中导入的.css
文件的处理。这将有助于开发,因为您不需要在其他地方重新导入文件。
import Vue from 'vue';
import App from './App.vue';
import './style/materialIcons.css';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
- 为了给我们的组件添加样式,我们将在
src/style
文件夹中创建一个名为starRating.css
的通用样式文件。在那里,我们将添加StarRatingDisplay
和StarRatingInput
组件之间共享的通用样式。
.starRating {
user-select: none;
display: flex;
flex-direction: row;
}
.starRating * {
line-height: 0.9rem;
}
.starRating .material-icons {
font-size: .9rem !important;
color: orange;
}
ul {
display: inline-block;
padding: 0;
margin: 0;
}
ul > li {
list-style: none;
float: left;
}
- 在组件的
<style>
部分,我们需要创建所有的 CSS 样式表规则。然后,在位于src/components
文件夹中的StarRatingInput.vue
组件文件上,我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响应用程序中的任何其他元素。在这里,我们将导入我们创建的通用样式,并为输入添加新样式:
<style scoped>
@import '../style/starRating.css';
.starRating {
justify-content: space-between;
}
.starRating * {
line-height: 1.7rem;
}
.starRating .material-icons {
font-size: 1.6rem !important;
}
.rateThis {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: 1rem;
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的样子:
创建 StarRatingDisplay 组件
现在我们有了输入,我们需要一种方法来向用户显示所选的选择。按照以下步骤创建StarRatingDisplay
组件:
-
在src/components
文件夹中创建一个名为StarRatingDisplay.vue
的新组件。
-
在组件的<script>
部分,在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。它们三个都将是数字,非必需的,并且有默认值。在methods
属性中,我们需要创建一个名为getStarName
的新方法,它将接收一个值并返回星星的图标名称:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
- 在
<template>
中,我们需要根据通过props
属性接收到的maxRating
值创建一个动态星星列表。在列表之后,我们需要显示我们收到的投票数,如果我们收到任何投票,我们也会显示它们:
<template>
<div class="starRating">
<ul>
<li
v-for="rate in maxRating"
:key="rate"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
<span class="rating">
{{ rating }}
</span>
<span
v-if="votes"
class="votes"
>
({{ votes }})
</span>
</div>
</template>
- 在组件的
<style>
部分,我们需要创建所有的 CSS 样式表规则。我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响应用程序中的任何其他元素。在这里,我们将导入我们创建的通用样式,并为显示添加新样式:
<style scoped>
@import '../style/starRating.css';
.rating, .votes {
display: inline-block;
color: rgba(0,0,0, .65);
font-size: .75rem;
margin-left: .4rem;
}
</style>
- 要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的样子:
创建 StarRating 组件
创建输入和显示后,我们需要将两者合并到一个单独的组件中。这个组件将是我们在应用程序中使用的最终组件。
按照以下步骤创建最终的StarRating
组件:
-
在src/components
文件夹中创建一个名为StarRating.vue
的新文件。
-
在组件的<script>
部分,我们需要导入StarRatingDisplay
和StarRatingInput
组件。在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。它们三个都将是数字,非必需的,并且有一个默认值。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
,并且一个名为voted
的属性,其默认值为false
。在methods
属性中,我们需要添加一个名为vote
的新方法,它将接收rank
作为参数。它将把rating
定义为接收到的值,并将voted
组件的内部变量定义为true
:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
- 在
<template>
部分,我们将放置两个组件,显示评分的输入:
<template>
<div>
<StarRatingInput
v-if="!voted"
:max-rating="maxRating"
@final-vote="vote"
>
Rate this Place
</StarRatingInput>
<StarRatingDisplay
v-else
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
/>
</div>
</template>
子组件上的数据操作
现在我们所有的组件都准备好了,我们需要将它们添加到我们的应用程序中。基本应用程序将访问子组件,并将评分设置为 5 星。
现在,按照以下步骤来理解和操作子组件中的数据:
-
在App.vue
文件中,在组件的<template>
部分,删除MaterialCardBox
组件的main-text
属性,并将其放置为组件的默认插槽。
-
在放置的文本之前,我们将添加StarRating
组件。我们将为其添加一个ref
属性。此属性将指示 Vue 将此组件直接链接到组件的this
对象中的一个特殊属性。在操作按钮中,我们将为点击事件添加监听器——一个用于resetVote
,另一个用于forceVote
。
<template>
<div id="app">
<MaterialCardBox
header="Material Card Header"
sub-header="Card Sub Header"
show-media
show-actions
img-src="https://picsum.photos/300/200"
>
<p>
<StarRating
ref="starRating"
/>
</p>
<p>
The path of the righteous man is beset on all sides by the
iniquities of the selfish and the tyranny of evil men.
</p>
<template v-slot:action>
<MaterialButton
background-color="#027be3"
text-color="#fff"
@click="resetVote"
>
Reset
</MaterialButton>
<MaterialButton
background-color="#26a69a"
text-color="#fff"
is-flat
@click="forceVote"
>
Rate 5 Stars
</MaterialButton>
</template>
</MaterialCardBox>
</div>
</template>
- 在组件的
<script>
部分,我们将创建一个methods
属性,并添加两个新方法:resetVote
和forceVote
。这些方法将访问StarRating
组件并重置数据或将数据设置为 5 星投票:
<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
import StarRating from './components/StarRating.vue';
export default {
name: 'App',
components: {
StarRating,
MaterialButton,
MaterialCardBox,
},
methods: {
resetVote() {
this.$refs.starRating.rank = 0;
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.rank = 5;
this.$refs.starRating.voted = true;
},
},
};
</script>
它是如何工作的...
当ref
属性添加到组件时,Vue 会将对所引用元素的链接添加到 JavaScript 的this
属性对象内的$refs
属性中。从那里,您可以完全访问组件。
这种方法通常用于操作 HTML DOM 元素,而无需调用文档查询选择器函数。
然而,此属性的主要功能是直接访问 Vue 组件,使您能够执行函数并查看组件的计算属性、变量和更改的变量,就像从外部完全访问组件一样。
还有更多...
与父组件可以访问子组件的方式相同,子组件可以通过在this
对象上调用$parent
来访问父组件。事件可以通过调用$root
属性来访问 Vue 应用程序的根元素。
另请参阅
您可以在vuejs.org/v2/guide/components-edge-cases.html#Accessing-the-Parent-Component-Instance
找到有关父子通信的更多信息。
创建动态注入组件
有些情况下,您的组件可以由您收到的变量类型或数据类型来定义;然后,您需要在不需要设置大量 Vue v-if
、v-else-if
和v-else
指令的情况下即时更改组件。
在这些情况下,最好的做法是使用动态组件,当计算属性或函数可以定义要呈现的组件时,并且决定是实时进行的。
如果有两个响应,这些决策有时可能很简单,但在长的 switch case 中可能会更复杂,其中可能有一长串可能要使用的组件。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用 Vue-CLI 创建我们的 Vue 项目,就像我们在第二章中的'使用 Vue CLI 创建您的第一个项目'配方中所做的那样,介绍 TypeScript 和 Vue 生态系统,或者使用'访问您的子组件数据'配方中的项目。
按照以下步骤创建动态注入组件:
-
打开StarRating.vue
组件。
-
在组件的<script>
部分,我们需要创建一个带有名为starComponent
的新计算值的computed
属性。此值将检查用户是否已投票。如果他们没有,它将返回StarRatingInput
组件;否则,它将返回StarRatingDisplay
组件:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
- 在组件的
<template>
部分,我们将删除现有组件,并用一个名为<component>
的特殊组件替换它们。这个特殊组件有一个命名属性,您可以指向任何返回有效 Vue 组件的地方。在我们的例子中,我们将指向计算属性starComponent
。我们将把从这两个组件中定义的所有绑定 props 放在这个新组件中,包括放在<slot>
中的文本:
<template>
<component
:is="starComponent"
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
@final-vote="vote"
>
Rate this Place
</component>
</template>
工作原理...
使用 Vue 特殊的<component>
组件,我们声明了根据计算属性设置的规则应该呈现什么组件。
作为通用组件,您总是需要确保每个可以呈现的组件都存在。最好的方法是使用v-bind
指令与需要定义的 props 和规则,但也可以直接在组件上定义,因为它将作为 prop 传递下去。
另请参阅
您可以在vuejs.org/v2/guide/components.html#Dynamic-Components
找到有关动态组件的更多信息。
创建依赖注入组件
直接从子组件或父组件访问数据而不知道它们是否存在可能非常危险。
在 Vue 中,可以使您的组件的行为像一个接口,并拥有一个在开发过程中不会改变的常见和抽象函数。依赖注入的过程是开发世界中的一个常见范例,并且也已经在 Vue 中实现。
使用内部 Vue 依赖注入有一些利弊,但在开发时,确保子组件知道父组件的期望总是一个好方法。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以像在第二章中的“使用 Vue CLI 创建您的第一个项目”中那样使用 Vue-CLI 创建我们的 Vue 项目,或者使用“创建动态注入组件”中的项目。
现在,按照以下步骤创建一个依赖注入组件:
-
打开StarRating.vue
组件。
-
在组件的<script>
部分,添加一个名为provide
的新属性。在我们的情况下,我们将只添加一个键值来检查组件是否是特定组件的子级。在属性中创建一个对象,其中包含starRating
键和true
值:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
provide: {
starRating: true,
},
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
-
打开StarRatingDisplay.vue
文件。
-
在组件的<script>
部分,我们将添加一个名为inject
的新属性。此属性将接收一个名为starRating
的键的对象,值将是一个具有default()
函数的对象。如果此组件不是StarRating
组件的子级,则此函数将记录错误:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
-
打开StarRatingInput.vue
文件。
-
在组件的<script>
部分,我们将添加一个名为inject
的新属性。此属性将接收一个名为starRating
的键的对象,值将是一个具有default()
函数的对象。如果此组件不是StarRating
组件的子级,则此函数将记录错误:
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingInput need to be a child of
StarRating');
},
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
它是如何工作的...
在运行时,Vue 将检查StarRatingDisplay
和StarRatingInput
组件中的starRating
的注入属性,如果父组件未提供此值,则将在控制台上记录错误。
使用组件注入通常用于在绑定组件之间保持共同接口的方式,例如菜单和项目。项目可能需要存储在菜单中的某些功能或数据,或者我们可能需要检查它是否是菜单的子级。
依赖注入的主要缺点是共享元素上不再具有响应性。因此,它主要用于共享功能或检查组件链接。
另请参阅
您可以在vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
找到有关组件依赖注入的更多信息。
创建一个组件混合
有时您会发现自己一遍又一遍地重写相同的代码。但是,有一种方法可以防止这种情况,并使自己更加高效。
您可以使用所谓的mixin
,这是 Vue 中的一个特殊代码导入,它将外部代码部分连接到当前组件。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用 Vue-CLI 创建我们的 Vue 项目,就像我们在第二章中的'使用 Vue CLI 创建您的第一个项目'中所做的那样,或者使用'创建依赖注入组件'食谱中的项目。
让我们按照以下步骤创建一个组件mixin
:
-
打开StarRating.vue
组件。
-
在<script>
部分,我们需要将props
属性提取到一个名为starRatingDisplay.js
的新文件中,我们需要在mixins
文件夹中创建这个新文件。这个新文件将是我们的第一个mixin
,并且看起来像这样:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
};
- 回到
StarRating.vue
组件,我们需要导入这个新创建的文件,并将其添加到一个名为mixin
的新属性中:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
mixins: [StarRatingDisplayMixin],
provide: {
starRating: true,
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
-
现在,我们将打开StarRatingDisplay.vue
文件。
-
在<script>
部分,我们将inject
属性提取到一个名为starRatingChild.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们inject
属性的mixin
:
export default {
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
};
- 在
StarRatingDisplay.vue
文件中,在<script>
部分,我们将提取methods
属性到一个名为starRatingName.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们getStarName
方法的mixin
:
export default {
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
- 回到
StarRatingDisplay.vue
文件,我们需要导入这些新创建的文件,并将它们添加到一个名为mixin
的新属性中:
<script>
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';
import StarRatingNameMixin from '../mixins/starRatingName';
import StarRatingChildMixin from '../mixins/starRatingChild';
export default {
name: 'StarRatingDisplay',
mixins: [
StarRatingDisplayMixin,
StarRatingNameMixin,
StarRatingChildMixin,
],
};
</script>
-
打开StarRatingInput.vue
文件。
-
在<script>
部分,我们移除inject
属性,并将props
属性提取到一个名为starRatingBase.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们props
属性的mixin
:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
},
};
- 回到
StarRatingInput.vue
文件,我们需要将rating
数据属性重命名为rank
,并且在getStarName
方法中,我们需要添加一个新的常量,该常量将接收rating
属性或rank
数据。最后,我们需要导入starRatingChild
mixin
和starRatingBase
mixin
:
<script>
import StarRatingBaseMixin from '../mixins/starRatingBase';
import StarRatingChildMixin from '../mixins/starRatingChild';
export default {
name: 'StarRatingInput',
mixins: [
StarRatingBaseMixin,
StarRatingChildMixin,
],
data: () => ({
rank: 0,
}),
methods: {
updateRating(value) {
this.rank = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rank);
},
getStarName(rate) {
const rating = (this.rating || this.rank);
if (rate <= rating) {
return 'star';
}
if (Math.fround((rate - rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
它是如何工作的...
mixins
的工作原理就像对象合并一样,但确保不要用导入的属性替换组件中已经存在的属性。
mixins
属性的顺序也很重要,因为它们将被检查并作为for
循环导入,所以最后一个mixin
不会改变任何祖先的属性。
在这里,我们将我们的代码中的许多重复部分拆分成了四个不同的小 JavaScript 文件,这样更容易维护并提高了生产力,而无需重写代码。
另请参阅
您可以在vuejs.org/v2/guide/mixins.html
找到有关 mixins 的更多信息。
惰性加载您的组件
webpack
和 Vue 天生就是一对。当使用webpack
作为 Vue 项目的打包工具时,可以使组件在需要时或异步加载。这通常被称为惰性加载。
准备工作
先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以像在第二章中的'使用 Vue CLI 创建您的第一个项目'配方中那样使用 Vue-CLI 创建我们的 Vue 项目,或者使用'创建组件 mixin'配方中的项目。
现在,按照以下步骤使用惰性加载技术导入您的组件:
-
打开App.vue
文件。
-
在组件的<script>
部分,我们将在脚本顶部获取导入并将它们转换为每个组件的惰性加载函数:
<script>
export default {
name: 'App',
components: {
StarRating: () => import('./components/StarRating.vue'),
MaterialButton: () => import('./components/MaterialButton.vue'),
MaterialCardBox: () =>
import('./components/MaterialCardBox.vue'),
},
methods: {
resetVote() {
this.$refs.starRating.rank = 0;
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.rank = 5;
this.$refs.starRating.voted = true;
},
},
};
</script>
它是如何工作的...
当我们声明一个为每个组件返回import()
函数的函数时,webpack
知道这个导入函数将进行代码拆分,并且它将使组件成为捆绑包中的一个新文件。
import()
函数是由 TC39 提出的一个模块加载语法的建议。这个函数的基本功能是异步加载任何声明为模块的文件,避免了在第一次加载时放置所有文件的需要。
另请参阅
您可以在vuejs.org/v2/guide/components-dynamic-async.html#Async-Components
找到有关异步组件的更多信息。
您可以在github.com/tc39/proposal-dynamic-import
找到有关 TC39 动态导入的更多信息。
第五章:通过 HTTP 请求从网络获取数据
数据现在是日常生活的一部分。如果没有数据,你就不会读到这本书,也不会试图了解更多关于 Vue 的知识。
了解如何在应用程序中获取和发送数据是开发人员的要求,而不仅仅是一个额外的技能。学习的最佳方式是通过实践,并找出它在幕后是如何完成的。
在这一章中,我们将学习如何使用 Fetch API 和当前最流行的 API 库axios
来构建自己的 API 数据操作。
在这一章中,我们将涵盖以下的配方:
-
创建一个 Fetch API 的 HTTP 客户端包装器
-
创建一个随机猫图片或 GIF 组件
-
使用MirageJS
创建本地虚拟 JSON API 服务器
-
使用axios
作为新的 HTTP 客户端
-
创建不同的axios
实例
-
为axios
创建请求和响应拦截器
-
使用axios
和Vuesax
创建 CRUD 接口
技术要求
在这一章中,我们将使用 Node.js 和 Vue CLI。
注意,Windows 用户!你需要安装一个名为windows-build-tools
的 NPM 包,以便能够安装以下所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行以下命令:
> npm install -g windows-build-tools
要安装 Vue CLI,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm install -g @vue/cli @vue/cli-service-global
创建一个 Fetch API 的 HTTP 客户端包装器
Fetch API 是旧的XMLHttpRequest
的子代。它有一个改进的 API 和一个基于Promises
的新而强大的功能集。
Fetch API 既简单又基于两个对象Request
和Response
的通用定义,使其可以在浏览器中的任何地方使用。浏览器的 Fetch API 也可以在window
或service worker
中执行。对于这个 API 的使用没有限制。
在这个配方中,我们将学习如何创建一个 Fetch API 的包装器,使 API 调用更简单。
准备工作
这个配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要开始我们的组件,我们可以使用在第二章中创建的使用 Vue CLI 创建的 Vue 项目,或者我们可以开始一个新的项目。
要开始一个新的项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
创建包装器
首先,我们需要创建一个新的 API 包装器来在这个教程中使用。这将是我们将在所有 HTTP 方法中使用的主要文件。
让我们按照以下步骤创建基本包装器:
-
在src/http
文件夹中创建一个名为baseFetch.js
的新文件。
-
我们将创建一个异步函数,它将作为参数接收url
,method
和options
的三个变量。这将是一个柯里化函数,第二个函数将接收type
作为参数:
export default async (url, method, options = {}) => {
let httpRequest;
if (method.toUpperCase() === 'GET') {
httpRequest = await fetch(url, {
cache: 'reload',
...options,
});
} else {
httpRequest = fetch(url, {
method: method.toUpperCase(),
cache: 'reload',
...options,
});
}
return (type) => {
switch (type.toLocaleLowerCase()) {
case 'json':
return httpRequest.json();
case 'blob':
return httpRequest.blob();
case 'text':
return httpRequest.text();
case 'formdata':
return httpRequest.formData();
default:
return httpRequest.arrayBuffer();
}
}; };
创建 API 方法
现在我们需要制作我们的 HTTP 方法函数。这些函数将使用包装器来执行浏览器的 Fetch API 并返回响应。
按照以下步骤创建每一个 API 方法调用:
-
在src/http
文件夹中创建一个名为fetchApi.js
的新文件。
-
我们需要从我们在第一步创建的文件中导入baseHttp
:
import baseHttp from './baseFetch';
现在在接下来的部分,我们将创建我们包装器中可用的每一个 HTTP 方法。
GET 方法函数
在这些步骤中,我们将创建HTTP GET方法。按照以下每一条指示来创建getHttp
函数:
-
创建一个名为getHttp
的常量。
-
定义一个常量作为一个异步函数,接收三个参数,url
,type
和options
。type
参数将默认值为'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
,'get'
作为第二个参数,options
作为第三个参数,并立即执行带有我们收到的type
参数的函数:
export const getHttp = async (url, type = 'json', options) => (await
baseHttp(url, 'get', options))(type);
POST 方法函数
在这部分,我们将创建HTTP POST方法。按照以下步骤来创建postHttp
函数:
-
创建一个名为postHttp
的常量。
-
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
参数和'post'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
参数执行返回的函数。body
通常是 JSON 或 JavaScript 对象。如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const postHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'post',
{
body,
...options,
}))(type);
PUT 方法函数
现在我们正在创建一个HTTP PUT方法。使用以下步骤创建putHttp
函数:
-
创建一个名为putHttp
的常量。
-
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'put'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
参数执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const putHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'put',
{
body,
...options,
}))(type);
PATCH 方法函数
是时候创建一个HTTP PATCH方法了。按照以下步骤创建patchHttp
函数:
-
创建一个名为patchHttp
的常量。
-
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'patch'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const patchHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'patch',
{
body,
...options,
}))(type);
更新方法函数
在这一部分,我们正在创建一个HTTP UPDATE方法。按照以下步骤创建updateHttp
函数:
-
创建一个名为updateHttp
的常量。
-
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'update'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const updateHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'update',
{
body,
...options,
}))(type);
DELETE 方法函数
在这最后一步,我们将创建一个DELETE HTTP方法。按照以下步骤创建deleteHttp
函数:
-
创建一个名为deleteHttp
的常量。
-
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。类型参数将具有默认值'json'
。
-
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'delete'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const deleteHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'delete',
{
body,
...options,
}))(type);
它是如何工作的...
在这个教程中,我们为window
元素上呈现的Fetch
API 创建了一个包装器。这个包装器由一个柯里化和闭包函数组成,第一个函数接收 Fetch API 的 URL 数据、方法和选项,而结果函数是 Fetch API 的响应转换器。
在包装器中,函数的第一部分将创建我们的fetch
请求。在那里,我们需要检查它是否是GET方法,所以我们只需要用url
参数执行它并省略其他参数。函数的第二部分负责将fetch
响应转换。它将在type
参数之间切换,并根据正确的参数执行检索函数。
要接收请求的最终数据,您始终需要在请求之后调用响应翻译器,就像以下示例中一样:
getHttp('https://jsonplaceholder.typicode.com/todos/1', 'json').then((response) => { console.log(response)); }
这将从 URL 获取数据,并将响应转换为 JSON/JavaScript 对象。
我们制作的第二部分是方法翻译器。我们为每个 REST 动词制作了函数,以便更轻松地使用。 GET 动词没有能力传递任何body
,但所有其他动词都能够在请求中传递body
。
另请参阅
你可以在developer.mozilla.org/en-US/docs/Web/API/Fetch_API
找到有关 Fetch API 的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
找到有关 FormData 的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/API/Body/body
找到有关 Fetch 响应主体的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/API/Headers
找到有关标头的更多信息。
您可以在developer.mozilla.org/
en-US/docs/Web/API/Request找到有关请求的更多信息。
创建一个随机猫图像或 GIF 组件
众所周知,互联网上有许多猫的 GIF 和视频。我相信如果我们删除所有与猫有关的内容,我们将会出现网络黑屏。
了解有关 Fetch API 以及如何在组件内使用它的最佳方法是制作一个随机猫图像或 GIF 组件。
准备工作
此示例的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用在“*将 Fetch API 包装为 HTTP 客户端”配方中使用的 Vue 项目和 Vue CLI,或者我们可以启动一个新的项目。
要启动新的项目,打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows),并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
创建组件
在本教程中,我们将使用第四章组件、混合和功能组件中创建的组件进行视觉元素。您也可以使用简单的 HTML 元素来实现相同的结果。
我们将把这个组件的创建分为三个步骤:<script>
、<template>
和<style>
。
单文件组件<script>
部分
按照以下步骤创建单文件组件的<script>
部分:
-
在src/components
文件夹中创建一个名为RandomCat.vue
的新文件并打开它。
-
从我们在'将 Fetch API 包装为 HTTP 客户端创建包装器'教程中制作的fetchApi
包装器中导入getHttp
函数:
import { getHttp } from '../http/fetchApi';
- 在
component
属性中异步导入MaterialButton
和MaterialCardBox
组件:
components: {
MaterialButton: () => import('./MaterialButton.vue'),
MaterialCardBox: () => import('./MaterialCardBox.vue'), },
- 在
data
属性中,我们需要创建一个名为kittyImage
的新数据值,默认为空字符串:
data: () => ({
kittyImage: '', }),
- 在
methods
属性中,我们需要创建getImage
方法,它将以Blob
的形式获取图片,并将其作为URL.createObjectURL
返回。我们还需要创建newCatImage
方法,它将获取一张新的猫的静态图片,以及newCatGif
方法,它将获取一个新的猫的 GIF:
methods: {
async getImage(url) {
return URL.createObjectURL(await getHttp(url, 'blob'));
},
async newCatImage() {
this.kittyImage = await this.getImage('https://cataas.com/cat');
},
async newCatGif() {
this.kittyImage = await
this.getImage('https://cataas.com/cat/gif');
},
},
- 在
beforeMount
生命周期钩子中,我们需要将其设置为异步,并执行newCatImage
方法:
async beforeMount() {
await this.newCatImage();
},
单文件组件<template>
部分
按照以下步骤创建单文件组件的<template>
部分:
- 首先,我们需要添加带有标题和副标题的
MaterialCardBox
组件,激活media
和action
部分,并为media
和action
创建<template>
命名插槽:
<MaterialCardBox
header="Cat as a Service"
sub-header="Random Cat Image"
show-media
show-actions >
<template
v-slot:media> </template>
<template v-slot:action> </template> </MaterialCardBox>
- 在
<template>
中名为media
的插槽中,我们需要添加一个<img>
元素,它将接收一个 URIBlob
,当kittyImage
变量中有任何数据时,它将显示出来,否则将显示一个加载图标:
<img
v-if="kittyImage"
alt="Meow!"
:src="kittyImage"
style="width: 300px;" >
<p v-else style="text-align: center">
<i class="material-icons">
cached
</i>
</p>
- 在
<template>
中名为action
的插槽中,我们将创建两个按钮,一个用于获取猫的图片,另一个用于获取猫的 GIF,两者都将在@click
指令上有一个事件监听器,调用一个函数来获取相应的图片:
<MaterialButton
background-color="#4ba3c7"
text-color="#fff"
@click="newCatImage" >
<i class="material-icons">
pets
</i> Cat Image
</MaterialButton> <MaterialButton
background-color="#005b9f"
text-color="#fff"
@click="newCatGif" >
<i class="material-icons">
pets
</i> Cat GIF
</MaterialButton>
单文件组件<style>
部分
在组件的<style>
部分中,我们需要设置body font-size
以便基于rem
和em
进行 CSS 样式计算:
<style>
body {
font-size: 14px;
} </style>
启动和运行您的新组件
按照以下步骤将您的组件添加到 Vue 应用程序中:
-
在src
文件夹中的App.vue
文件中打开。
-
在components
属性中,异步导入RandomCat.vue
组件:
<script> export default { name: 'App',
components: {
RandomCat: () => import('./components/RandomCat'),
}, }; </script>
- 在文件的
<template>
部分中,声明导入的组件:
<template>
<div id="app">
<random-cat />
</div> </template>
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
使用getHttp
包装器,组件能够获取 URL 并将其作为Blob
类型检索出来。有了这个响应,我们可以使用URL.createObjectUrl
导航方法,并将Blob
作为参数传递,以获取一个有效的图像 URL,该 URL 可以用作src
属性。
另请参阅
您可以在developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
找到有关URL.createObjectUrl
的更多信息。
您可以在developer.mozilla.org/en-US/docs/Web/API/Body/blob
找到有关Blob
响应类型的更多信息。
使用 MirageJS 创建您的虚假 JSON API 服务器
为了测试、开发或设计而伪造数据总是一个问题。在开发阶段展示应用程序时,您需要有一个大的 JSON 或者制作一个自定义服务器来处理任何数据更改。
现在有一种方法可以帮助开发人员和 UI 设计师在不需要编写外部服务器的情况下实现这一点 - 一个名为 MirageJS 的新工具,它是在浏览器上运行的服务器模拟器。
在这个配方中,我们将学习如何使用 MirageJS 作为模拟服务器并在其上执行 HTTP 请求。
准备工作
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用我们在“将 Fetch API 作为 HTTP 客户端创建包装器”配方中使用的 Vue 项目和 Vue CLI,或者我们可以启动一个新的项目。
要启动一个新项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create visual-component
CLI 将询问一些问题,这将有助于创建项目。您可以使用箭头键进行导航,使用Enter键继续,使用Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
创建模拟服务器
在这个配方中,我们将使用在“将 Fetch API 包装为 HTTP 客户端的创建包装器”配方中制作的fetchApi
包装器的getHttp
函数。
通过下一步和部分来创建您的MirageJS
模拟服务器:
将MirageJS
服务器安装到您的软件包中。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save miragejs
本配方使用的版本是 0.1.32。注意MirageJS
的任何更改,因为目前还没有当前的 LTS 版本。
现在在接下来的部分中,我们将创建每一个由 MirageJS 服务器模拟的 HTTP 方法。
创建模拟数据库
在这一部分,我们将创建一个MirageJS
数据库,用于存储临时数据。按照以下步骤创建它:
-
在src/server
文件夹中创建一个名为db.js
的新文件,用于初始加载的数据。
-
我们需要为这个文件创建一个 JavaScript 对象作为默认导出,其中包含我们希望服务器具有的初始数据:
export default {
users: [
{ name: 'Heitor Ramon Ribeiro',
email: 'heitor@example.com',
age: 31,
country: 'Brazil',
active: true,
},
], };
创建 GET 路由函数
在这一部分,我们将创建一个由MirageJS
服务器模拟的HTTP GET方法。按照以下步骤创建它:
-
对于GET方法,我们需要在src/server
文件夹中创建一个名为get.js
的新文件。
-
对于这个配方,我们将创建一个通用的getFrom
函数,该函数接收一个键作为参数并返回一个函数。这个返回的函数返回一个直接指向本地数据库的指定键:
export const getFrom = key => ({ db }) => db[key]; export default {
getFrom, };
创建 POST 路由函数
在这一部分,我们将创建HTTP POST方法,这将由MirageJS
服务器模拟。按照以下步骤创建它:
-
对于POST方法,我们需要在src/server
文件夹中创建一个名为post.js
的新文件。
-
对于这个配方,我们将创建一个通用的postFrom
函数,该函数接收一个键作为参数并返回一个函数。这个返回的函数将解析 HTTP 请求体的data
属性,并返回服务器模式的内部函数,将数据插入数据库。使用key
参数,模式知道我们正在处理哪个表:
export const postFrom = key => (schema, request) => {
const { data } = typeof request.requestBody === 'string'
? JSON.parse(request.requestBody)
: request.requestBody; return schema.db[key].insert(data); }; export default {
postFrom, };
创建 PATCH 路由函数
在本节中,我们将创建MirageJS
服务器模拟的HTTP PATCH方法。按照以下步骤创建它:
-
对于PATCH方法,我们需要在src/server
文件夹中创建一个名为patch.js
的新文件。
-
对于这个配方,我们将制作一个通用的patchFrom
函数,该函数接收一个键作为参数并返回一个函数。返回的函数将解析 HTTP 请求体的data
属性,并返回一个服务器模式的内部函数,该函数更新具有与数据一起传递的id
属性的特定对象。使用key
参数,模式知道我们正在处理哪个表:
export const patchFrom = key => (schema, request) => {
const { data } = typeof request.requestBody === 'string'
? JSON.parse(request.requestBody)
: request.requestBody; return schema.db[key].update(data.id, data); }; export default {
patchFrom, };
创建 DELETE 路由函数
在本节中,我们将创建MirageJS
服务器模拟的HTTP DELETE方法。按照以下步骤创建它:
-
对于DELETE方法,我们需要在src/server
文件夹中创建一个名为delete.js
的新文件。
-
对于这个配方,我们将制作一个通用的patchFrom
函数,该函数接收一个键作为参数并返回一个函数。返回的函数将解析 HTTP 请求体的data
属性,并返回一个服务器模式的内部函数,该函数删除具有通过路由REST参数传递给服务器的id
属性的特定对象。使用key
参数,模式知道我们正在处理哪个表:
export const deleteFrom = key => (schema, request) =>
schema.db[key].remove(request.params.id); export default {
deleteFrom, };
创建服务器
在本节中,我们将创建MirageJS
服务器和可用的路由。按照以下步骤创建服务器:
-
在src/server
文件夹中创建一个名为server.js
的新文件。
-
接下来,我们需要导入Server
类,baseData
和路由方法:
import { Server } from 'miragejs'; import baseData from './db'; import { getFrom } from './get'; import { postFrom } from './post'; import { patchFrom } from './patch'; import { deleteFrom } from './delete';
- 创建一个全局变量
server
,并将此变量设置为Server
类的新执行:
window.server = new Server({});
- 在
Server
类的构造选项中,添加一个名为seeds
的新属性。此属性是一个接收服务器(srv
)作为参数并执行srv.db.loadData
函数传递baseDate
作为参数的函数:
seeds(srv) {
srv.db.loadData({ ...baseData }); },
- 现在我们需要添加相同的构造选项到一个名为
routes
的新属性中,它将创建模拟服务器路由。这个属性是一个函数,在函数体内,我们需要设置模拟服务器的namespace
和服务器响应的毫秒延迟。将有四个路由。对于创建路由,我们将创建一个名为/users
的新路由,监听POST方法。对于读取路由,我们将创建一个名为/users
的新路由,监听GET方法。对于更新路由,我们将创建一个名为/users/:id
的新路由,监听PATCH方法,最后,对于删除路由,我们将创建一个名为/users
的新路由,监听DELETE方法:
routes() {
this.namespace = 'api'; this.timing = 750; this.get('/users', getFrom('users')); this.post('/users', postFrom('users')); this.patch('/users/:id', patchFrom('users')); this.delete('/users/:id', deleteFrom('users'));
},
添加到应用程序
在这一部分,我们将MirageJS
服务器添加到 Vue 应用程序中。按照以下步骤使服务器对您的 Vue 应用程序可用:
-
在src
文件夹中打开main.js
文件。
-
我们需要将服务器声明为第一个导入声明,这样它就可以在应用程序的初始加载时可用:
import './server/server'; import Vue from 'vue'; import App from './App.vue'; Vue.config.productionTip = false; new Vue({
render: h => h(App), }).$mount('#app');
创建组件
现在我们有了服务器,我们需要测试它。为了这样做,我们将创建一个简单的应用程序,运行每个 HTTP 方法,并显示每个调用的结果。
在接下来的部分,我们将创建一个简单的 Vue 应用程序。
单文件组件<script>
部分
在这部分,我们将创建单文件组件的<script>
部分。按照以下步骤创建它:
-
在src
文件夹中打开App.vue
文件。
-
从我们在'将 Fetch API 包装为 HTTP 客户端的创建包装器'中制作的fetchHttp
包装器中导入getHttp
,postHttp
,patchHttp
和deleteHTTP
方法:
import {
getHttp,
postHttp,
patchHttp,
deleteHttp, } from './http/fetchApi';
- 在
data
属性中,我们需要创建三个新属性来使用,response
,userData
和userId
:
data: () => ({
response: undefined,
userData: '',
userId: undefined,
}),
- 在
methods
属性中,我们需要创建四个新方法,getAllUsers
,createUser
,updateUser
和deleteUser
:
methods: {
async getAllUsers() {
},
async createUser() {
},
async updateUser() {
},
async deleteUser() {
}, },
- 在
getAllUsers
方法中,我们将设置响应数据属性为api/users
路由的getHttp
函数的结果:
async getAllUsers() {
this.response = await getHttp(`${window.location.href}api/users`); },
- 在
createUser
方法中,我们将接收一个data
参数,这将是一个对象,我们将把它传递给api/users
路由上的postHttp
,然后执行getAllUsers
方法:
async createUser(data) {
await postHttp(`${window.location.href}api/users`, { data });
await this.getAllUsers(); },
- 对于
updateUser
方法,我们将接收一个data
参数,这将是一个对象,我们将其传递给patchHttp
,在api/users/:id
路由上使用对象上的id
属性作为路由上的:id
。之后,我们将执行getAllUsers
方法:
async updateUser(data) {
await patchHttp(`${window.location.href}api/users/${data.id}`,
{ data });
await this.getAllUsers(); },
- 最后,在
deleteUser
方法中,我们接收用户id
作为参数,该参数是一个数字,然后我们将其传递给deleteHttp
,在api/users/:id
路由上使用 ID 作为:id
。之后,我们执行getAllUsers
方法:
async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`, {}, 'text');
await this.getAllUsers(); },
单文件组件部分
在这部分,我们将创建单文件组件的<template>
部分。按照以下步骤创建它:
- 在模板的顶部,我们需要添加
response
属性,包裹在一个<pre>
HTML 元素中:
<h3>Response</h3> <pre>{{ response }}</pre>
- 对于用户的创建和更新,我们需要创建一个带有
v-model
指令绑定到userData
属性的textarea
HTML 输入:
<hr/> <h1> Create / Update User </h1> <label for="userData">
User JSON:
<textarea
id="userData"
v-model="userData"
rows="10"
cols="40"
style="display: block;"
></textarea> </label>
- 要发送这些数据,我们需要创建两个按钮,两者都在单击事件上绑定了事件侦听器,使用
@click
指令分别指向createUser
和updateUser
,并在执行时传递userData
:
<button
style="margin: 20px;"
@click="createUser(JSON.parse(userData))" >
Create User
</button> <button
style="margin: 20px;"
@click="updateUser(JSON.parse(userData))" >
Update User
</button>
- 要执行DELETE方法,我们需要创建一个类型为
number
的输入 HTML 元素,并将v-model
指令绑定到userId
属性:
<h1> Delete User </h1> <label for="userData">
User Id:
<input type="number" step="1" v-model="userId"> </label>
- 最后,要执行此操作,我们需要创建一个按钮,该按钮将在单击事件上绑定一个事件侦听器,使用
@click
指令,将其指向deleteUser
方法,并在执行时传递userId
属性:
<button
style="margin: 20px;"
@click="deleteUser(userId)" >
Delete User
</button>
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
MirageJS
的工作原理类似于拦截应用程序上发生的每个 HTTP 请求的拦截器。服务器拦截浏览器上的所有XHR (XMLHttpRequest)执行,并检查路由,以查看它是否与服务器创建的任何一个路由匹配。如果匹配,服务器将根据相应的路由执行函数。
作为具有基本 CRUD 功能的简单 REST 服务器,服务器具有类似模式的数据库结构,有助于创建用于存储数据的虚拟数据库。
另请参阅
您可以在github.com/miragejs/miragejs
找到有关 MirageJS 的更多信息。
使用 axios 作为新的 HTTP 客户端
当您需要一个用于 HTTP 请求的库时,毫无疑问axios
是您应该选择的。这个库被超过 150 万个开源项目和无数个闭源项目使用,是 HTTP 库之王。
它构建成适用于大多数浏览器,并提供了最完整的选项集之一-您可以自定义请求中的一切。
在这个食谱中,我们将学习如何将我们的 Fetch API 包装器更改为axios
并开始围绕它工作。
准备就绪
这个食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用在'使用 MirageJS 创建您的虚拟 JSON API 服务器'食谱中制作的 Vue 项目,或者我们可以开始一个新的项目。
要开始一个新的项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 会询问一些问题,这些问题将有助于创建项目。您可以使用箭头键进行导航,Enter键继续,Spacebar键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
从 Fetch API 更改为 Axios
在接下来的步骤中,我们将为 HTTP 包装器中使用的 Fetch API 更改为axios
库。按照以下步骤正确更改它:
- 在您的包中安装
axios
。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save axios
此食谱中使用的版本是 0.19.0。注意axios
的更改,因为该库尚无 LTS 版本。
-
打开src/http
文件夹中的baseFetch.js
文件。
-
简化该方法,使其接收三个参数,url
、method
和options
,并返回一个axios
方法,使用传递给实例构造函数的方法调用 HTTP 请求:
import axios from 'axios';
export default async (url, method, options = {}) => axios({
method: method.toUpperCase(),
url,
...options,
});
更改 GET 方法函数
在这部分中,我们正在更改HTTP GET方法。按照以下说明更改getHttp
函数:
-
打开src/http
文件夹中的fetchApi.js
文件。
-
在getHttp
函数中,我们将添加一个新的参数 param,并删除柯里化函数:
export const getHttp = async (
url,
params,
options, ) => baseHttp(url,
'get',
{
...options,
params,
});
更改 POST 方法函数
在这部分中,我们正在更改HTTP POST方法。按照以下说明更改postHttp
函数:
-
打开http
文件夹中的fetchApi.js
文件。
-
在postHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const postHttp = async (
url,
data,
options, ) => baseHttp(url,
'post',
{
data,
...options,
});
更改 PUT 方法函数
在这部分,我们正在更改HTTP PUT方法。按照以下说明更改putHttp
函数:
-
打开http
文件夹内的fetchApi.js
文件。
-
在putHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const putHttp = async (
url,
data,
options, ) => baseHttp(url,
'put',
{
data,
...options,
});
更改 PATCH 方法函数
在这部分,我们正在更改HTTP PATCH方法。按照以下说明更改patchHttp
函数:
-
打开http
文件夹内的fetchApi.js
文件。
-
在patchHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const patchHttp = async (
url,
data,
options, ) => baseHttp(url,
'patch',
{
data,
...options,
});
更改 UPDATE 方法函数
在这部分,我们正在更改HTTP UPDATE方法。按照以下说明更改updateHttp
函数:
-
打开http
文件夹内的fetchApi.js
文件。
-
在updateHttp
函数中,我们将添加一个新的参数 param,并删除柯里化函数:
export const updateHttp = async (
url,
data,
options, ) => baseHttp(url,
'update',
{
data,
...options,
});
更改 DELETE 方法函数
在这部分,我们正在更改HTTP DELETE方法。按照以下说明更改deleteHttp
函数:
-
打开http
文件夹内的fetchApi.js
文件。
-
在deleteHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const deleteHttp = async (
url,
data,
options, ) => baseHttp(url,
'delete',
{
data,
...options,
});
更改组件
在这部分,我们将改变组件与新函数的工作方式。按照以下说明正确更改它:
-
打开src
文件夹内的App.vue
文件。
-
在getAllUsers
方法中,我们需要更改响应的定义方式,因为axios
给我们提供了一个完全不同的响应对象,而不是 Fetch API:
async getAllUsers() {
const { data } = await getHttp(`${window.location.href}api/users`);
this.response = data;
},
- 在
deleteUser
方法中,我们可以直接将 URL 作为参数传递:
async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`);
await this.getAllUsers();
},
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
它是如何工作的...
当我们为 Fetch API 创建包装器时,我们使用了一种将 API 抽象成另一个接口的技术,这使得从 Fetch API 更改到axios
库成为可能。通过这样做,我们能够改进方法并简化函数的调用和处理方式。例如,GET 方法现在可以接收一个名为params的新参数,这些参数是 URL 查询参数的对象,将自动注入到 URL 中。
我们还必须更改响应的解释方式,因为axios
比 Fetch API 具有更健壮和完整的响应对象,后者只返回获取的响应本身。
另请参阅
您可以在github.com/axios/axios
找到有关axios
的更多信息。
创建不同的 axios 实例
使用axios
时,您可以运行多个实例,而它们互不干扰。例如,您可以有一个指向版本 1 的用户 API 的实例,另一个指向版本 2 的支付 API,两者共享相同的命名空间。
在这里,我们将学习如何创建各种axios
实例,因此您可以在不受问题或干扰的情况下使用尽可能多的 API 命名空间。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
操作步骤...
要启动我们的组件,我们可以使用在“使用 axios 作为新的 HTTP 客户端”食谱中使用的 Vue CLI 创建的 Vue 项目,或者我们可以启动一个新的项目。
要启动一个新的实例,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键进行导航,Enter键继续,Spacebar键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
更改 HTTP 函数
创建多个axios
实例时,调用axios
库的过程会发生变化。因此,我们需要更改 HTTP 包装器实例化axios
库的方式。
在接下来的部分中,我们将改变 HTTP 包装器与创建新的axios
实例的工作方式,并使其可用于应用程序。
更改 HTTP Fetch 包装器
在接下来的步骤中,我们将创建一个新的自定义axios
实例,该实例将用于 HTTP 包装器。按照以下说明将新实例添加到应用程序中:
-
在src/http
文件夹中打开baseFetch.js
文件。
-
我们需要创建一个名为createAxios
的新工厂函数,以便每次执行时生成一个新的axios
实例:
export function createAxios(options = {}) {
return axios.create({
...options,
}); }
- 现在,我们需要创建
localApi
常量,其值将是createAxios
工厂的执行结果:
const localApi = createAxios();
- 对于
JSONPlaceHolder
,我们将创建一个名为jsonPlaceholderApi
的常量,该常量将被导出,其值将是createAxios
工厂的执行。我们还将传递一个对象作为参数,其中包含定义的baseURL
属性:
export const jsonPlaceholderApi = createAxios({
baseURL: 'https://jsonplaceholder.typicode.com/', });
- 在
export default
函数中,我们需要从axios
更改为localApi
:
export default async (url, method, options = {}) => localApi({
method: method.toUpperCase(),
url,
...options, });
更改 HTTP 方法函数
在这部分,我们将改变 HTTP 方法如何与新的axios
实例一起工作。按照说明正确执行:
-
在src/http
文件夹中打开fetchApi.js
文件。
-
我们将从baseFetch
导入jsonPlaceholderApi
函数作为额外导入的值:
import baseHttp, { jsonPlaceholderApi } from './baseFetch';
- 我们需要创建一个名为
getTodos
的新常量,该常量将被导出。此常量将是一个函数,将接收userId
作为参数,并返回axios
的 GET 函数,其中userId
参数将在名为params
的属性的配置对象中接收:
export const getTodos = async userId => jsonPlaceholderApi.get('todos',
{
params: {
userId,
},
});
更改 MirageJS 服务器
在这部分,我们将更改MirageJS
服务器如何与新创建的axios
实例一起工作。按照说明正确执行:
-
在src/server
文件夹中打开server.js
文件。
-
在构造函数对象的routes
属性上,我们需要添加一个passthrough
声明,这将指示 MirageJS 不会拦截对该 URL 的所有调用:
import { Server } from 'miragejs'; import baseData from './db'; import { getFrom } from './get'; import { postFrom } from './post'; import { patchFrom } from './patch'; import { deleteFrom } from './delete'; window.server = new Server({
seeds(srv) {
srv.db.loadData({ ...baseData });
}, routes() {
this.passthrough();
this.passthrough('https://jsonplaceholder.typicode.com/**'); this.namespace = 'api'; this.timing = 750; this.get('/users', getFrom('users')); this.post('/users', postFrom('users')); this.patch('/users/:id', patchFrom('users')); this.delete('/users/:id', deleteFrom('users'));
}, });
更改组件
在包装函数、MirageJS
服务器方法和 HTTP 方法更改后,我们需要将组件更改为已实现的新库。
在接下来的部分,我们将更改组件以匹配已实现的新库。
单文件组件<script>
部分
在这部分,我们将更改单文件组件的<script>
部分。按照以下步骤执行:
-
在src
文件夹中打开App.vue
文件。
-
我们需要按以下方式导入新的getTodos
函数:
import {
getHttp,
postHttp,
patchHttp,
deleteHttp,
getTodos, } from './http/fetchApi';
- 在
Vue
对象的data
属性中,我们需要创建一个名为userTodo
的新属性,其默认值为一个空数组:
data: () => ({
response: undefined,
userData: '',
userId: undefined,
userTodo: [], }),
- 在
methods
属性中,我们需要创建一个名为getUserTodo
的新方法,该方法接收userId
参数。此方法将获取用户的待办事项列表,并将响应属性分配给userTodo
属性:
async getUserTodo(userId) {
this.userTodo = await getTodos(userId); },
单文件组件<template>
部分
在这部分,我们将更改单文件组件的<template>
部分。按照以下步骤执行:
-
在src
文件夹中打开App.vue
文件。
-
在模板底部,我们需要创建一个新的input
HTML 元素,使用v-model
指令绑定到userId
属性:
<h1> Get User ToDos </h1> <label for="userData">
User Id:
<input type="number" step="1" v-model="userId"> </label>
- 要获取项目列表,我们需要创建一个按钮,该按钮绑定了点击事件的事件监听器,使用
@click
指令,目标是getUserTodo
,并在执行中传递userId
:
<button
style="margin: 20px;"
@click="getUserTodo(userId)" >
Fetch Data
</button>
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的地方:
它是如何工作的...
当我们创建axios
的新实例时,会创建一个新对象,并定义新的配置、标头、拦截器和操纵器。这是因为axios
声明create
函数与new Class
相同。它是相同的接口但不同的对象。
利用这个可能性,我们能够创建两个连接驱动程序,一个用于本地 API,另一个用于JSONPlaceHolder
API,它有一个不同的baseURL
。
由于 MirageJS 服务器集成,所有 HTTP 请求都被 MirageJS 拦截,因此我们需要在路由构造函数中添加一个指令,指示 MirageJS 不会拦截的路由。
另请参阅
您可以在jsonplaceholder.typicode.com/
找到有关 JSONPlaceHolder API 的更多信息。
您可以在github.com/axios/axios#creating-an-instance
找到有关axios
实例的更多信息。
您可以在github.com/miragejs/miragejs
找到有关 MirageJS 的更多信息。
为 axios 创建请求和响应拦截器
在我们的应用程序中使用axios
作为主要的 HTTP 操作器,允许我们使用请求和响应拦截器。这些用于在将数据发送到服务器之前或在接收数据时操纵数据,然后将其发送回 JavaScript 代码之前对其进行操纵。
拦截器最常用的方式是在 JWT 令牌验证和刷新接收特定错误或 API 错误操纵的请求时使用。
在这个示例中,我们将学习如何创建一个请求拦截器来检查POST、PATCH和DELETE方法以及一个响应错误操纵器。
准备工作
此示例的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用我们在'创建不同的 axios 实例'食谱中制作的 Vue CLI 项目,也可以启动一个新的项目。
要开始一个新的,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
创建拦截器
在接下来的步骤中,我们将创建一个axios
拦截器,它将作为中间件工作。按照说明正确执行:
- 安装
Sweet Alert
包。要做到这一点,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save sweetalert2
-
在src/http
文件夹中创建一个名为interceptors.js
的新文件并打开它。
-
然后,我们导入 Sweet Alert 包:
import Swal from 'sweetalert2';
- 我们需要创建一个包含将被拦截的POST方法的数组的常量:
const postMethods = ['post', 'patch'];
- 我们需要创建一个名为
requestInterceptor
的函数并导出它。这个函数将接收一个参数config
,它是一个axios
配置对象。我们需要检查请求方法是否包含在我们之前创建的数组中,以及数据主体的data
属性是否有一个id
属性。如果任何检查未通过,我们将抛出一个Error
,否则,我们将返回config
:
export function requestInterceptor(config) {
if (
postMethods.includes(config.method.toLocaleLowerCase()) &&
Object.prototype.hasOwnProperty.call('id', config.data.data) &&
!config.data.data.id)
{
throw new Error('You need to pass an ID for this request');
} return config; }
- 对于响应拦截器,我们需要创建一个名为
responseInterceptor
的新函数,它返回响应,因为我们在这个拦截器中不会改变任何东西:
export function responseInterceptor(response) {
return response; }
- 为了捕获错误,我们需要创建一个
errorInterceptor
函数,它将被导出。这个函数接收一个error
作为参数,并显示一个sweetalert2
警报错误消息,并返回一个带有error
的Promise.reject
:
export function errorInterceptor(error) {
Swal.fire({
type: 'error',
title: 'Error!',
text: error.message,
}); return Promise.reject(error); }
将拦截器添加到 HTTP 方法函数中
在接下来的步骤中,我们将向 HTTP 方法函数添加axios
拦截器。按照以下步骤正确执行:
-
在src/http
文件夹中打开baseFetch.js
文件。
-
我们需要导入刚刚创建的三个拦截器:
import {
errorInterceptor,
requestInterceptor,
responseInterceptor, } from './interceptors';
- 在创建
localApi
实例之后,我们声明了请求和响应拦截器的使用:
localApi.interceptors
.request.use(requestInterceptor, errorInterceptor); localApi.interceptors
.response.use(responseInterceptor, errorInterceptor);
- 在创建
jsonPlaceholderApi
实例之后,我们声明了请求和响应拦截器的使用:
jsonPlaceholderApi.interceptors
.request.use(requestInterceptor, errorInterceptor); jsonPlaceholderApi.interceptors
.response.use(responseInterceptor, errorInterceptor);
工作原理...
axios
执行的每个请求都会通过拦截器集中的任何一个。响应也是一样。如果在拦截器上抛出任何错误,它将自动传递给错误处理程序,因此请求根本不会被执行,或者响应将作为错误发送到 JavaScript 代码。
我们检查了每个POST,PATCH和DELETE方法所做的每个请求,以查看在请求体数据中是否有id
属性。如果没有,我们向用户抛出错误,告诉他们需要为请求传递一个 ID。
另请参阅
您可以在sweetalert2.github.io
找到有关 Sweet Alert 2 的更多信息。
您可以在github.com/axios/axios#interceptors
找到有关axios
请求拦截器的更多信息。
使用 Axios 和 Vuesax 创建 CRUD 界面
在处理数据时,我们总是需要做一些事情:CRUD 过程。无论您正在开发什么类型的应用程序,都需要 CRUD 界面以便在服务器上输入和操作任何数据,管理面板,应用程序的后端,甚至客户端。
在这里,我们将学习如何使用Vuesax
框架和axios
进行 HTTP 请求来创建一个简单的 CRUD 界面。
准备工作
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,请使用我们在“为 axios 创建请求和响应拦截器”配方中使用的 Vue CLI 的 Vue 项目,或者启动一个新项目。
要启动一个新项目,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
将 Vuesax 添加到应用程序
在接下来的步骤中,我们将介绍如何将Vuesax
UI 库添加到您的 Vue 应用程序中。按照这些说明正确执行:
- 打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save vuesax material-icons
-
在src
文件夹中创建一个名为style.css
的文件并打开它。
-
导入vuesax
,material-icon
和Open Sans
字体样式表:
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,
800i&display=swap'); @import url('~vuesax/dist/vuesax.css'); @import url('~material-icons/iconfont/material-icons.css'); * {
font-family: 'Open Sans', sans-serif; }
-
打开src
文件夹中的main.js
文件。
-
导入style.css
文件和Vuesax
。之后,您需要通知 Vue 使用Vuesax
:
import './server/server'; import Vue from 'vue'; import App from './App.vue'; import Vuesax from 'vuesax'; import './style.css'; Vue.use(Vuesax); Vue.config.productionTip = false; new Vue({
render: h => h(App), }).$mount('#app');
创建组件路由
我们将在五个部分中继续这个过程:List
,Create
,Read
,Update
和Delete
。我们的应用将是一个动态组件应用程序,因此我们将创建五个组件,每个部分一个。这些组件将类似于我们的页面。
首先,我们需要将App.vue
更改为我们的主路由管理器,并创建一个用于更改组件的混合器。
单文件组件<script>
部分
在这部分,我们将创建单文件组件的<script>
部分。按照以下说明正确创建组件:
-
在src
文件夹中打开App.vue
。
-
导入将在此处创建的每个组件:
import List from './components/list'; import Create from './components/create'; import View from './components/view'; import Update from './components/update';
- 在
data
属性中,创建两个新值:componentIs
默认值为'list'
,userId
默认值为0
:
data: () => ({
componentIs: 'list',
userId: 0, }),
- 我们需要向 Vue 对象添加一个名为
provide
的新属性。该属性将是一个函数,因此提供给组件的值可以是响应式的:
provide () {
const base = {}; Object.defineProperty(base, 'userId', {
enumerable: true,
get: () => Number(this.userId),
}); return base; },
- 在
computed
属性中,我们需要创建一个名为component
的新属性。这将是一个 switch case,根据componentIs
属性返回我们的组件:
computed: {
component() {
switch (this.componentIs) {
case 'list':
return List;
case 'create':
return Create;
case 'view':
return View;
case 'edit':
return Update;
default:
return undefined;
}
} },
- 最后,在方法中,我们需要创建一个
changeComponent
方法,将当前组件更新为新组件:
methods: {
changeComponent(payload) {
this.componentIs = payload.component;
this.userId = Number(payload.userId);
}, },
单文件组件<template>
部分
在这部分,我们将创建单文件组件的<template>
部分。按照以下说明正确创建组件:
- 在
div#app
HTML 元素中,我们需要添加一个vs-row
组件:
<div id="app">
<vs-row></vs-row> </div>
- 在
vs-row
组件中,我们需要添加一个vs-col
组件,具有以下属性:vs-type
定义为flex
,vs-justify
定义为left
,vs-align
定义为left
,vs-w
定义为12
:
<vs-col
vs-type="flex"
vs-justify="left"
vs-align="left"
vs-w="12"> </vs-col>
- 最后,在
vs-col
组件内部,我们将添加一个动态组件,该组件具有一个is
属性,指向计算属性component
,并将事件侦听器指向将执行changeComponent
方法的"change-component"
事件:
<component
:is="component"
@change-component="changeComponent" />
创建路由混合器
在这部分,我们将创建组件混合器,以便在其他组件中重复使用。按照以下说明正确创建组件:
-
在src/mixin
文件夹中创建一个名为changeComponent.js
的新文件并打开它。
-
这个混合器将有一个名为changeComponent
的方法,它将发出一个名为'change-component'
的事件,其中包含需要呈现的新组件的名称和userId
:
export default {
methods: {
changeComponent(component, userId = 0) {
this.$emit('change-component', { component, userId });
},
}
}
创建列表组件
列表组件将是索引组件。它将列出应用程序中的用户,并具有其他 CRUD 操作的所有链接。
单文件组件<script>
部分
在这部分,我们将创建单文件组件的<script>
部分。按照这些说明正确创建组件:
-
在src/components
文件夹中创建一个名为list.vue
的新文件并打开它。
-
从fetchApi
导入getHttp
和deleteHttp
,以及changeComponent
混合器:
import {
getHttp,
deleteHttp,
} from '../http/fetchApi';
import changeComponent from '../mixin/changeComponent';
- 在组件的
mixins
属性中,我们需要添加导入的changeComponent
混合器:
mixins: [changeComponent],
- 在组件的
data
属性中,我们添加了一个名为userList
的新属性,其默认为空数组:
data: () => ({
userList: [], }),
- 对于方法,我们创建了
getAllUsers
和deleteUsers
方法。在getAllUsers
方法中,我们获取用户列表,并将userList
的值设置为getHttp
函数执行的响应。deleteUser
方法将执行deleteHttp
函数,然后执行getAllUsers
方法:
methods: {
async getAllUsers() {
const { data } = await getHttp(`${window.location.href}api/users`);
this.userList = data;
},
async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`);
await this.getAllUsers();
}, }
- 最后,我们将
beforeMount
生命周期钩子异步化,调用getAllUsers
方法:
async beforeMount() {
await this.getAllUsers(); },
单文件组件部分
在这部分,我们将创建单文件组件的<template>
部分。按照这些说明正确创建组件:
- 创建一个带有
style
属性定义为margin: 20px
的vs-card
组件:
<vs-card style="margin: 20px;"
>
</vs-card>
- 在
vs-card
组件内部,为header
创建一个动态的<template>
,其中包含一个<h3>
标签和您的标题:
<template slot="header">
<h3>
Users
</h3> </template>
- 之后,创建一个
vs-row
组件,其中包含一个vs-col
组件,具有以下属性:vs-type
定义为flex
,vs-justify
定义为left
,vs-align
定义为left
,vs-w
定义为12
:
<vs-row>
<vs-col
vs-type="flex"
vs-justify="left"
vs-align="left"
vs-w="12">
</vs-col>
</vs-row>
- 在
vs-col
组件内部,我们需要创建一个vs-table
组件。该组件将具有指向userList
变量的data
属性,并将search
,stripe
和pagination
属性定义为 true。max-items
属性将被定义为10
,style
属性将具有width: 100%; padding: 20px;
的值:
<vs-table
:data="userList"
search
stripe pagination max-items="10"
style="width: 100%; padding: 20px;" ></vs-table>
- 对于表头,我们需要创建一个名为
thead
的动态 <template>
,并为每一列创建一个带有 sort-key
属性定义为相应对象键属性和显示为您想要的名称的 vs-th
组件:
<template slot="thead">
<vs-th sort-key="id">
#
</vs-th>
<vs-th sort-key="name">
Name
</vs-th>
<vs-th sort-key="email">
Email
</vs-th>
<vs-th sort-key="country">
Country
</vs-th>
<vs-th sort-key="phone">
Phone
</vs-th>
<vs-th sort-key="Birthday">
Birthday
</vs-th>
<vs-th>
Actions
</vs-th> </template>
- 对于表格主体,我们需要创建一个动态的
<template>
,其中定义了一个 slot-scope
属性作为 data
属性。在这个 <template>
中,我们需要创建一个 vs-tr
组件,它将迭代数据属性,并为表格头部设置的每一列创建一个 vs-td
组件。每个 vs-td
组件都有一个设置为相应列数据对象属性的数据属性,并且内容将是相同的数据渲染。最后一列是操作列,将有三个按钮,一个用于读取,另一个用于更新,最后一个用于删除。读取按钮将在 "click"
事件上有一个指向 changeComponent
的事件监听器,更新按钮也是如此。删除按钮的 "click"
事件监听器将指向 deleteUser
方法:
<template slot-scope="{data}">
<vs-tr :key="index" v-for="(tr, index) in data">
<vs-td :data="data[index].id">
{{data[index].id}}
</vs-td>
<vs-td :data="data[index].name">
{{data[index].name}}
</vs-td>
<vs-td :data="data[index].email">
<a :href="`mailto:${data[index].email}`">
{{data[index].email}}
</a>
</vs-td>
<vs-td :data="data[index].country">
{{data[index].country}}
</vs-td>
<vs-td :data="data[index].phone">
{{data[index].phone}}
</vs-td>
<vs-td :data="data[index].birthday">
{{data[index].birthday}}
</vs-td>
<vs-td :data="data[index].id">
<vs-button
color="primary"
type="filled"
icon="remove_red_eye"
size="small"
@click="changeComponent('view', data[index].id)"
/>
<vs-button
color="success"
type="filled"
icon="edit"
size="small"
@click="changeComponent('edit', data[index].id)"
/>
<vs-button
color="danger"
type="filled"
icon="delete"
size="small"
@click="deleteUser(data[index].id)"
/>
</vs-td>
</vs-tr> </template>
- 最后,在卡片页脚中,我们需要创建一个名为
footer
的动态 <template>
。在这个 <template>
中,我们将添加一个带有 vs-justify
属性定义为 flex-start
的 vs-row
组件,并插入一个带有 color
属性定义为 primary
、type
属性定义为 filled
、icon
属性定义为 fiber_new
和 size
属性定义为 small
的 vs-button
。@click
事件监听器将以参数 'create'
和 0
目标 changeComponent
方法:
<template slot="footer">
<vs-row vs-justify="flex-start">
<vs-button
color="primary"
type="filled"
icon="fiber_new"
size="small"
@click="changeComponent('create', 0)"
>
Create User
</vs-button>
</vs-row> </template>
单文件组件
<template>
结构,我们将使用 Vue 对象的template
属性,我们可以将字符串或模板字符串作为值传递,这将由 Vue 脚本插值并呈现在屏幕上:使用“创建基本文件”部分的基本示例,创建一个名为template.html
的新文件并打开它。
在空的<script>
HTML 元素中,通过对象解构Vue
全局常量,创建常量defineComponent
和createApp
:
const {
defineComponent, createApp, } = Vue;
component
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,其中有三个属性:data
、methods
和template
:const component = defineComponent({
data: () => ({}),
methods: {},
template: `` });
data
属性中,将其定义为一个单例函数,返回一个 JavaScript 对象,其中有一个名为count
的属性,并且默认值为0
:data: () => ({
count: 0 }),
methods
属性中,创建一个名为addOne
的属性,这是一个函数,将通过1
增加count
的值:methods: {
addOne() {
this.count += 1;
}, },
template
属性中,在模板字符串中,创建一个带有标题的h1
HTML 元素。然后,作为兄弟元素,创建一个带有绑定到click
事件的事件监听器的button
HTML 元素,当执行时触发addOne
函数:template: `
<h1> This is a Vue 3 Root Element! </h1>
<button @click="addOne"> Pressed {{ count }} times. </button> `
createApp
函数,将component
常量作为参数传递。然后,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:createApp(component)
.mount('#app');
<template>
结构,我们将使用 Vue 对象的template
属性,我们可以将字符串或模板字符串作为值传递,Vue 脚本将对其进行插值处理并在屏幕上呈现:使用“创建基本文件”部分的基本示例,创建一个名为render.html
的新文件并打开它。
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,从Vue
全局常量中调用defineComponent
、h
和createApp
方法:
const {
defineComponent,
h, createApp, } = Vue;
component
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,该对象有三个属性:data
、methods
和render
:const component = defineComponent({
data: () => ({}),
methods: {},
render() {}, });
data
属性中,将其定义为一个单例函数,返回一个具有名为count
且默认值为0
的 JavaScript 对象:data: () => ({
count: 0 }),
methods
属性中,创建一个名为addOne
的属性,它是一个函数,将count
的值增加1
:methods: {
addOne() {
this.count += 1;
}, },
render
属性中,执行以下步骤:创建一个名为h1
的常量,并将其定义为h
函数,将'h1'
作为第一个参数传递,将要使用的标题作为第二个参数。
创建一个名为button
的常量,它将是h
函数,将"button"
作为第一个参数传递,将一个具有onClick
属性且值为this.addOne
的 JavaScript 对象作为第二个参数传递,将button
的内容作为第三个参数。
返回一个数组,第一个值为h1
常量,第二个值为button
常量:
render() {
const h1 = h('h1', 'This is a Vue 3 Root Element!');
const button = h('button', {
onClick: this.addOne,
}, `Pressed ${this.count} times.`); return [
h1,
button,
]; },
createApp
函数,将component
常量作为参数传递,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:createApp(component)
.mount('#app');
defineComponent
执行,并且作为参数传递的 JavaScript 对象几乎保持与 Vue 2 中的旧结构相同。在示例中,我们使用了相同的属性data
、render
、methods
和template
,这些属性都存在于 Vue 2 中。<template>
结构的示例中,我们不必创建包装元素来封装应用程序组件的内容,并且可以直接在组件上有两个根元素。render
函数示例中,发生了相同的行为,但最终示例使用了新的暴露的h
API,它不再是render
函数的参数。在按钮创建中出现了一个重大变化;我们必须在数据 JavaScript 对象内部使用onClick
属性,而不是on
属性和click
方法。这是因为 Vue 3 的 VNode 的新数据结构。input
HTML 元素。使用创建基本文件部分的基本示例,创建一个名为component.html
的新文件并打开它。
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,调用Vue
全局常量的defineComponent
和createApp
方法:
const {
defineComponent, createApp, } = Vue;
nameInput
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,具有四个属性:name
、props
、template
和inheritAttrs
。然后,我们将inheritAttrs
的值定义为false
:const nameInput = defineComponent({
name: 'NameInput',
props: {},
inheritAttrs: false,
template: `` });
props
属性中,添加一个名为modelValue
的属性,并将其定义为String
:props: {
modelValue: String, },
创建一个label
HTML 元素,并将一个input
HTML 元素作为子元素添加。
在input
HTML 元素中,将v-bind
指令定义为一个 JavaScript 对象,其中包含this.$attrs
的解构值。
将变量属性value
定义为接收到的 prop 的modelValue
。
将input
属性type
设置为"text"
。
将匿名函数添加到change
事件监听器中,该函数接收一个event
作为参数,然后发出一个名为"update:modeValue"
的事件,载荷为event.target.value
:
template: ` <label>
<input
v-bind="{ ...$attrs, }"
:value="modelValue" type="text" @change="(event) => $emit('update:modelValue',
event.target.value)"
/> </label>`
appComponent
的常量,定义为defineComponent
方法,传递一个 JavaScript 对象作为参数,其中包含两个属性,data
和template
:const component = defineComponent({
data: () => ({}),
template: ``, });
data
属性中,将其定义为一个单例函数,返回一个具有名为name
的属性的 JavaScript 对象,其默认值为''
:data: () => ({
name: '' }),
创建一个NameInput
组件,其中v-model
指令绑定到name
数据属性。
创建一个带有值"border:0; border-bottom: 2px solid red;"
的style
属性。
创建一个带有值"name-input"
的data-test
属性:
template: ` <name-input
v-model="name" style="border:0; border-bottom: 2px solid red;"
data-test="name-input" />`
app
的常量,并将其定义为createApp
函数,将component
常量作为参数传递。然后,调用app.component
函数,将要注册的组件的名称作为第一个参数传递,组件作为第二个参数传递。最后,调用app.mount
函数,将"#app"
作为参数传递:const app = createApp(component); app.component('NameInput', nameInput); app.mount('#app');
defineComponent
函数,传递一个 JavaScript 对象作为参数。这个对象保持了几乎与 Vue 2 相同的组件声明结构。在示例中,我们使用了相同的属性,data
,methods
,props
和template
,这些属性都存在于 V2 中。inheritAttrs
属性来阻止将属性自动应用于组件上的所有元素,仅将其应用于具有v-bind
指令和解构this.$attrs
对象的元素。createApp
API 创建应用程序,然后执行app.component
函数在应用程序上全局注册组件,然后渲染我们的应用程序。reactivity
API。reactivity
和watch
API 创建一个简单的 JavaScript 动画。reactivity
API 创建一个应用程序,以在屏幕上呈现动画:使用“创建基本文件”部分中的基本示例,创建一个名为reactivity.html
的新文件并打开它。
在<head>
标签中,添加一个新的<meta>
标签,属性为chartset
定义为"utf-8"
:
<meta charset="utf-8"/>
<body>
标签中,删除div#app
HTML 元素,并创建一个div
HTML 元素,id
定义为marathon
,style
属性定义为"font-size: 50px;"
:<div
id="marathon"
style="font-size: 50px;" > </div>
<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,调用Vue
全局常量中的reactivity
和watch
方法:const {
reactive,
watch, } = Vue;
mod
的常量,定义为一个函数,接收两个参数a
和b
。然后返回一个算术运算,a
模b
:const mod = (a, b) => (a % b);
maxRoadLength
的常量,其值为50
。然后,创建一个名为competitor
的常量,其值为reactivity
函数,传递一个 JavaScript 对象作为参数,其中position
属性定义为0
,speed
定义为1
:const maxRoadLength = 50; const competitor = reactive({
position: 0,
speed: 1, });
watch
函数,传递一个匿名函数作为参数。在函数内部,执行以下操作:创建一个名为street
的常量,并将其定义为一个大小为maxRoadLength
的Array
,并用*'_'*
.填充它。
创建一个名为marathonEl
的常量,并将其定义为 HTML DOM 节点#marathon
。
选择数组索引中competitor.position
的street
元素,并将其定义为*"![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/c8b07311-36a4-4df3-98fd-3b68200deed3.png)"*
,如果competitor.position
是偶数,或者如果数字是奇数,则定义为*"![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/562ed724-a630-4193-a9c6-4e143a9690e2.png)"*
。
将marathonEl.innertHTML
定义为*""*
和street.reverse().join('')
:
watch(() => {
const street = Array(maxRoadLength).fill('_');
const marathonEl = document.getElementById('marathon');
street[competitor.position] = (competitor.position % 2 === 1)
? '![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/c8b07311-36a4-4df3-98fd-3b68200deed3.png)'
: '![](https://gitee.com/OpenDocCN/freelearn-vue-zh/raw/master/docs/vue3-cb/img/562ed724-a630-4193-a9c6-4e143a9690e2.png)'; marathonEl.innerHTML = '';
marathonEl.innerHTML = street.reverse().join(''); });
setInterval
函数,将一个匿名函数作为参数传递。在函数内部,将competitor.position
定义为mod
函数,将competitor.position
加上competitor.speed
作为第一个参数,将maxRoadLength
作为第二个参数:setInterval(() => {
competitor.position = mod(competitor.position +competitor.speed,
maxRoadLength) }, 100);
reactive
和watch
API,我们能够创建一个具有 Vue 框架中的响应性的应用程序,但不使用 Vue 应用程序。competitor
,它的工作方式与 Vue 的data
属性相同。然后,我们创建了一个watch
函数,它的工作方式与watch
属性相同,但是作为匿名函数使用。在watch
函数中,我们为竞争者开辟了一条跑道,并创建了一个简单的动画,使用两个不同的表情符号,根据在道路上的位置进行更改,以模拟屏幕上的动画。setInterval
函数,每 100 毫秒改变竞争者在道路上的位置:使用“创建基本文件”部分的基本示例,创建一个名为component.html
的新文件并打开它。
在空的<script>
HTML 元素中,使用对象解构方法创建将要使用的函数的常量,从Vue
全局常量中调用createApp
、defineComponent
、setup
、ref
、onMounted
和onUnmounted
方法:
const {
createApp,
defineComponent,
setup,
ref,
onMounted,
onUnmounted, } = Vue;
fetchLocation
函数,在其中创建一个名为watcher
的let
变量。然后,创建一个名为geoLocation
的常量,并将其定义为navigator.geolocation
。接下来,创建一个名为gpsTime
的常量,并将其定义为ref
函数,将Date.now()
函数作为参数传递。最后,创建一个名为coordinates
的常量,并将其定义为ref
函数,将一个 JavaScript 对象作为参数传递,其中的属性accuracy
、latitude
、longitude
、altitude
、altitudeAccuracy
、heading
和speed
都定义为0
:function fetchLocation() {
let watcher;
const geoLocation = navigator.geolocation;
const gpsTime = ref(Date.now());
const coordinates = ref({
accuracy: 0,
latitude: 0,
longitude: 0,
altitude: 0,
altitudeAccuracy: 0,
heading: 0,
speed: 0,
}); }
fetchLocation
函数内部,在常量创建之后,创建一个名为setPosition
的函数,带有一个名为payload
的参数。在函数内部,将gpsTime.value
定义为payload.timestamp
参数,将coordinates.value
定义为payload.coords
参数:function setPosition(payload) {
gpsTime.value = payload.timestamp
coordinates.value = payload.coords
}
setPosition
函数之后,调用onMounted
函数,将一个匿名函数作为参数传递。在函数内部,检查浏览器是否可用geoLocation
API,并将watcher
定义为geoLocation.watchPostion
函数,将setPosition
函数作为参数传递:onMounted(() => {
if (geoLocation) watcher = geoLocation.watchPosition(setPosition); });
onMounted
函数后,创建一个onUnmounted
函数,将一个匿名函数作为参数传递。在函数内部,检查watcher
是否已定义,然后执行geoLocation.clearWatch
函数,将watcher
作为参数传递:onUnmounted(() => {
if (watcher) geoLocation.clearWatch(watcher); });
fetchLocation
函数中,返回一个 JavaScript 对象,并将coordinates
和gpsTime
常量作为属性/值传递:return {
coordinates,
gpsTime, };
appComponent
的常量,并将其定义为defineComponent
函数,将一个具有setup
和template
属性的 JavaScript 对象作为参数传递:const appComponent = defineComponent({
setup() {},
template: `` });
setup
函数中,创建一个常量,这是一个对象解构,包括fetchLocation
函数的coordinates
和gpsTime
属性:setup() {
const {
coordinates,
gpsTime,
} = fetchLocation(); }
setup
函数内部,创建另一个名为formatOptions
的常量,并将其定义为一个具有year
、month
、day
、hour
和minute
属性的 JavaScript 对象,其值均为'numeric'
。然后,将属性hour12
定义为true
:const formatOptions = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
};
formatOptions
常量之后,创建一个名为formatDate
的常量,并将其定义为一个函数,该函数接收一个名为date
的参数。然后,返回一个新的Intl.DateTimeFormat
函数,将navigator.language
作为第一个参数,将formatOption
常量作为第二个参数。然后,原型链连接format
函数,传递date
参数:const formatDate = (date) => (new
Intl.DateTimeFormat(navigator.language,
formatOptions).format(date));
setup
函数的末尾,返回一个 JavaScript 对象,其属性定义为coordinates
、gpsTime
和formatDate
常量:return {
coordinates,
gpsTime,
formatDate };
template
属性中,进行以下操作:创建一个带有文本“我的地理位置在{{ formatDate(new Date(gpsTime) }}”的h1
HTML 元素。
创建一个ul
HTML 元素,并添加三个li
HTML 元素作为子元素。
在第一个子元素中,添加文本“纬度:{{ coordinates.latitude }}”。
在第二个子元素中,添加文本“经度:{{ coordinates.longitude }}”。
在第三个子元素中,添加文本“海拔:{{ coordinates.altitude }}”:
template: `
<h1>My Geo Position at {{formatDate(new
Date(gpsTime))}}</h1>
<ul>
<li>Latitude: {{ coordinates.latitude }}</li>
<li>Longitude: {{ coordinates.longitude }}</li>
<li>Altitude: {{ coordinates.altitude }}</li>
</ul> `
createApp
函数,传递appComponent
常量作为参数。然后,原型链连接mount
函数,并将div
HTML 元素的id
属性("#app")
作为函数的参数:createApp(appComponent)
.mount('#app');
createApp
、defineComponent
、setup
、ref
、onMounted
和onUnmounted
- 作为常量,我们将使用它们来创建组件。然后,我们创建了fetchLocation
函数,它负责获取用户的地理位置数据,并将其作为响应式数据返回,当用户更改位置时可以自动更新。navigator.geolocation
API,它能够获取用户当前的 GPS 位置。利用浏览器提供的数据,我们能够用它来定义由 Vue ref
API 创建的变量。setup
函数创建了组件,因此渲染知道我们正在使用新的组合 API 作为组件创建方法。在setup
函数内部,我们导入了fetchLocation
函数的动态变量,并创建了一个方法,用于在模板上使用日期格式化。createApp
公开的 API 创建了应用程序,并挂载了 Vue 应用程序。developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation
找到有关Navigator.geolocation
的更多信息。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。创建一个 TypeScript 项目
理解 TypeScript
创建你的第一个 TypeScript 类
使用 Vue CLI 创建你的第一个项目
使用 Vue UI 向 Vue CLI 项目添加插件
将 TypeScript 添加到 Vue CLI 项目中
使用vue-class-component
创建你的第一个 TypeScript Vue 组件
使用vue-class-component
创建自定义 mixin
使用vue-class-component
创建自定义函数装饰器
将自定义钩子添加到vue-class-component
将vue-property-decorator
添加到vue-class-component
windows-build-tools
的 npm 包,以便安装以下所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行以下命令:> npm install -g windows-build-tools
。> npm install -g @vue/cli @vue/cli-service-global
> npm install -g typescript
npm
项目。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:> npm init -y
> npm install typescript --only=dev
.ts
文件并进行编译:> tsc --init
tsconfig.json
文件。这是一个编译器设置文件。在这里,您可以定义目标,开发中可用的 JavaScript 库,目标 ECMAScript 版本,模块生成等等。tsconfig.json
文件的compilerOption
属性中添加文档对象模型(DOM)库,这样在开发时就可以访问 window 和 document 对象。index.ts
文件。让我们在index.ts
文件中创建一些简单的代码,以便在终端中记录一个数学计算:function sum(a: number, b: number): number {
return a + b;
}
const firstNumber: number = 10;
const secondNumber: number = 20;
console.log(sum(firstNumber, secondNumber));
a
和b
,它们的类型都设置为number
,并且函数预计返回一个number
。我们创建了两个变量,firstNumber
和secondNumber
,在这种情况下都设置为number
类型——分别是10
和20
,因此,将它们传递给函数是有效的。如果我们将它们设置为其他类型,比如字符串、布尔值、浮点数或数组,编译器会在变量和函数执行的静态类型检查方面抛出错误。> tsc ./index.ts
index.js
中看到最终文件。如果我们查看文件内部,最终代码将类似于这样:function sum(a, b) {
return a + b;
}
var firstNumber = 10;
var secondNumber = 20;
console.log(sum(firstNumber, secondNumber));
tsconfig.json
文件中定义的配置。tsconfig.json
的文件会在我们的文件夹中创建。这个文件协调了编译器和开发过程中的静态类型检查的所有规则。所有的开发都基于这个文件中定义的规则。每个环境都依赖于需要导入的特定规则和库。www.typescriptlang.org/docs/home.html
找到有关 TypeScript 的更多信息。www.typescriptlang.org/docs/handbook/migrating-from-javascript.html
。www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
找到一个关于 TypeScript 的 5 分钟课程。npm
项目。打开 Terminal(macOS 或 Linux)或 Command Prompt/PowerShell(Windows)并执行以下命令:> npm init -y
> npm install typescript --only=dev
字符串
数字
布尔
数组
元组
枚举
任意
空
对象
(")
或单引号(')
括起来,或者用反引号(
)`,通常称为模板字符串。const myText: string = 'My Simple Text';
const myTextAgain: string = "My Simple Text";
const greeting: string = `Welcome back ${myName}!`;
const myAge: number = 31;
const hexNumber: number = 0xf010d;
const binaryNumber: number = 0b1011;
const octalNumber: number = 0o744;
const isTaskDone: boolean = false;
const isGreaterThen: boolean = 10 > 5;
[]
(方括号)来表示它是一个声明类型的数组:const primeNumbers: number[] = [1, 3, 5, 7, 11];
Array<type>
声明进行通用声明。这不是最常用的方式,但根据您正在开发的代码,您可能需要使用它:const switchInstructions: Array<boolean> = [true, false, false, true];
let person: [string, number];
person = ['Heitor', 31];
console.log(`My name is ${person[0]} and I am ${person[1]} years old`);
0
的初始值开始,并以最终索引号的值结束;或者,您可以通过传递枚举值的索引来获取枚举的名称:enum ErrorLevel {
Info,
Debug,
Warning,
Error,
Critical,
}
console.log(ErrorLevel.Error); // 3
console.log(ErrorLevel[3]); // Error
enum Color {
Red = '#FF0000',
Blue = '#0000FF',
Green = '#00FF00',
}
enum Languages {
JavaScript = 1,
PHP,
Python,
Java = 10,
Ruby,
Rust,
TypeScript,
}
console.log(Color.Red) // '#FF0000'
console.log(Languages.TypeScript) // 13
let maybeIs: any = 4;
maybeIs = 'a string?';
maybeIs = true;
function logThis(str: string): void{
console.log(str);
}
interface IPerson {
name: string;
age: number;
}
const person: IPerson = {
name: 'Heitor',
age: 31,
};
function greetingUser(user: {name: string, lastName: string}) {
console.log(`Hello, ${user.name} ${user.lastName}`);
}
type Person = {
name: string,
age: number,
};
const person: Person = {
name: 'Heitor',
age: 31,
};
console.log(`My name is ${person.name}, I am ${person.age} years old`);
const sumOfValues: (a:number, b:number): number = (a: number, b: number): number => a + b;
const complexFunction: (a: number) => (b:number) => number = (a: number): (b: number) => number => (b: number): number => a + b;
function foo(a: number, b:number): number{
return a + b;
}
function greetingStudent(student: {name: string}){
console.log(`Hello ${student.name}`);
}
const newStudent = {name: 'Heitor'};
greetingStudent(newStudent);
interface IStudent {
name: string;
course?: string;
readonly university: string;
}
function greetingStudent(student: IStudent){
console.log(`Hello ${student.name}`);
if(student.course){
console.log(`Welcome to the ${student.course}` semester`);
}
}
const newStudent: IStudent = { name: 'Heitor', university: 'UDF' };
greetingStudent(newStudent);
course
,在它上面声明了一个?
。这表示这个属性可以是 null 或 undefined。这被称为可选属性。tsconfig.json
文件中设置标志:{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
@expression
的形式使用,其中表达式是一个在运行时将被调用的函数。function classSeal(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@classSeal
class Animal {
sound: string;
constructor(sound: string) {
this.sound = sound;
}
emitSound() {
return "The animal says, " + this.sound;
}
}
www.typescriptlang.org/docs/handbook/basic-types.html
找到有关 TypeScript 基本类型的更多信息。www.typescriptlang.org/docs/handbook/functions.html
找到有关 TypeScript 函数的更多信息。www.typescriptlang.org/docs/handbook/enums.html
找到有关 TypeScript 枚举的更多信息。www.typescriptlang.org/docs/handbook/advanced-types.html
找到有关 TypeScript 高级类型的更多信息。www.typescriptlang.org/docs/handbook/decorators.html
找到有关 TypeScript 装饰器的更多信息。rmolinamir.github.io/typescript-cheatsheet/#types
上查看 TypeScript 类型的速查表。npm
项目。要做到这一点,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:> npm init -y
> npm install typescript --only=dev
Animal
类。这个类可以有一些基本属性,比如它的name
,它是否发出sound
,它的family
,以及这种动物所吃的基本food chain
。food chain
。我们需要确保它是一个不可枚举的列表,并且每个使用它的文件最终都有相同的值。我们只需要调用一个常量变量:export enum FoodChainType {
Carnivorous = 'carnivorous',
Herbivorous = 'herbivorous',
Omnivorous = 'omnivorous',
}
interface
。我们知道我们的动物有一个name
,可以发出一个sound
,可以成为一个family
的一部分,并且属于food chain
类别。在类中使用接口,我们在类和将要暴露的内容之间建立了一个合同,有助于开发过程:interface IAnimal {
name: string;
sound?: string;
family: string;
foodChainType: FoodChainType;
}
Animal
类。每个类都可以有它的构造函数。类构造函数可以很简单,只包含一些变量作为参数,也可以更复杂,有一个对象作为参数。如果你的构造函数将有任何参数,需要一个接口或声明每个参数的类型。在这种情况下,我们的构造函数将是一个对象,只有一个参数,与Animal
相同,所以它将扩展IAnimal
接口:interface IAnimalConstructor extends IAnimal {
}
IBasicAnimal
接口。为此,我们需要添加一些我们的类将具有的公共元素,并也声明这些元素。我们需要实现函数来显示它是什么动物以及它发出什么声音。现在,我们有了一个包含所有动物属性的基本类。它具有类和构造函数的单独接口。食物链的枚举以一种易于阅读的方式声明,因此该库的 JavaScript 导入可以无问题执行:interface IBasicAnimal extends IAnimal {
whoAmI: () => void;
makeSound: () => void;
}
export class Animal implements IBasicAnimal {
public name: string;
public sound: string;
public family: string;
public foodChainType: FoodChainType;
constructor(params: IAnimalConstructor) {
this.name = params.name;
this.sound = params.sound || '';
this.family = params.family;
this.foodChainType = params.foodChainType;
}
public whoAmI(): void {
console.log(`I am a ${this.name}, my family is ${this.family}.
My diet is ${this.foodChainType}.`);
if (this.sound) {
console.log([...Array(2).fill(this.sound)].join(', '));
}
}
public makeSound(): void {
console.log(this.sound);
}
}
Animal
转换成Dog
:import {Animal, FoodChainType} from './Animal';
class Dog extends Animal {
constructor() {
super({
name: 'Dog',
sound: 'Wof!',
family: 'Canidae',
foodChainType: FoodChainType.Carnivorous,
});
}n
}
www.typescriptlang.org/docs/handbook/classes.html
找到有关 TypeScript 类的更多信息。rmolinamir.github.io/typescript-cheatsheet/#classes
上查看 TypeScript 类的速查表。热模块重载
。@vue/cli
@vue/cli-service-global
> vue create my-first-project
? Please pick a preset: (Use arrow keys) default (babel, eslint) ❯ **Manually select features**
有两种方法可以启动一个新项目。默认方法是一个基本的babel
和eslint
项目,没有任何插件或配置,还有手动
模式,您可以选择更多的模式、插件、linters 和选项。我们将选择手动
。
现在,我们被问及我们将在项目中需要的功能。这些功能是一些 Vue 插件,如 Vuex 或 Router(Vue-Router)、测试工具、linters 等:
? Check the features needed for your project: (Use arrow keys) ❯ Babel
TypeScript Progressive Web App (PWA) Support Router Vuex CSS Pre-processors ❯ Linter / Formatter
Unit Testing ❯ **E2E Testing**
CSS 预处理器
并按Enter继续:? Check the features needed for your project: (Use arrow keys) ❯ Babel
TypeScript Progressive Web App (PWA) Support Router Vuex ❯ CSS Pre-processors ❯ **Linter / Formatter**
Unit Testing **E2E Testing**
Sass
、Less
和Stylus
。由您选择哪种最适合您:? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules
are supported by default): (Use arrow keys) Sass/SCSS (with dart-sass) Sass/SCSS (with node-sass) **Less** ❯ Stylus
AirBnB
、Standard
和Prettier
之间进行选择,并使用基本配置。那些在ESLint
中导入的规则可以随时进行自定义,没有任何问题,并且有一个完美的规则适合您的需求。您知道什么对您最好:? Pick a linter / formatter config: (Use arrow keys) ESLint with error prevention only ❯ **ESLint + Airbnb config** ESLint + Standard config
ESLint + Prettier
? Pick additional lint features: (Use arrow keys) **Lint on save** ❯ Lint and fix on commit
package.json
文件中:? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) ❯ **In dedicated config files** In package.json
? Save this as a preset for future projects? (y/N) n
npm run serve
—用于在本地运行开发服务器
npm run build
—用于构建和缩小应用程序以进行部署
npm run lint
—对代码执行 lint
npm
脚本都被命名为任务,在这些任务中,您可以获得实时统计数据,例如资产、模块和依赖项的大小;错误或警告的数量;以及更深入的网络数据,以微调您的应用程序。> vue ui
cli.vuejs.org/guide/
找到有关 Vue CLI 项目的更多信息。cli.vuejs.org/dev-guide/plugin-dev.html
找到有关 Vue CLI 插件开发的更多信息。@vue/cli
@vue/cli-service-global
> vue ui
main.js
文件已更改,并且vuex(store)
和vue-router(router)
插件现在已导入并注入到 Vue 实例中:npm
或yarn
配合使用,自动安装项目中的软件包,然后在可能的情况下注入 Vue 实例所需的条件。@vue/cli
@vue/cli-service-global
> vue ui
@vue/cli-plugin-typescript
:使用类样式组件语法?使用 TypeScript 的vue-class-component
插件。
与 TypeScript 一起使用 Babel(现代模式所需,自动检测的 polyfill,转译 JSX)?激活 Babel 以在 TypeScript 编译器之外转译 TypeScript。
使用 ESLint?将 ESLint 用作.ts
和.tsx
文件的检查器。
将所有.js 文件转换为.ts 文件?在安装过程中自动将所有.js
文件转换为.ts
文件。
允许编译.js 文件?激活tsconfig.json
标志以接受编译器中的.js
文件。
选择您的选项后,点击完成安装。
现在,您的项目是一个 TypeScript Vue 项目,所有文件都已配置好,准备好进行编码:
github.com/typescript-eslint/typescript-eslint
找到有关 TypeScript ESLint 的更多信息github.com/vuejs/vue-class-component
找到有关vue-class-component
的更多信息。this
关键字有着密切的关系,因此开发 TypeScript 组件会有点混乱。vue-class-component
插件使用 ECMAScript 装饰器提案将静态类型的值直接传递给 Vue 组件,并使编译器更容易理解发生了什么。@vue/cli
@vue/cli-service-global
vue-class-component
创建你的第一个 Vue 组件:在src/components
文件夹内创建一个名为Counter.vue
的新文件。
现在,让我们开始制作 Vue 组件的脚本部分。我们将创建一个包含数字数据的类,两个方法——一个用于增加,另一个用于减少——最后,一个计算属性来格式化最终数据:
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class Counter extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
<template>
<div>
<fieldset>
<legend>{{ formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button @click="decrease">Decrease</button>
</fieldset>
</div>
</template>
App.vue
文件中,我们需要导入刚刚创建的组件:<template>
<div id="app">
<Counter />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
@Component({
components: {
Counter,
},
})
export default class App extends Vue {
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
npm run serve
命令时,你将看到你的组件在屏幕上运行和执行:vue-class-component
插件利用装饰器的新提案来向 TypeScript 类注入和传递一些属性。github.com/vuejs/vue-class-component
找到更多关于vue-class-component
的信息。mixin
是一种在其他 Vue 对象中重用相同代码的方式,就像将mixin
的所有属性混合到组件中一样。mixin
属性,然后是组件值,因此组件始终是最后且有效的值。此合并以深度模式进行,并且已在框架内声明了特定的方式,但可以通过特殊配置进行更改。@vue/cli
@vue/cli-service-global
vue-class-component
创建自定义 mixin:我们需要在src/components
文件夹中创建一个名为CounterByTen.vue
的新组件。
现在,让我们开始制作 Vue 组件的脚本部分。我们将创建一个类,其中将有一个类型为数字的变量和默认值为0
;两种方法,一种是增加10
,另一种是减少10
;最后,一个计算属性来格式化最终数据:
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class CounterByTen extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
App.vue
文件中,我们需要导入刚刚创建的组件:<template>
<div id="app">
<Counter />
<hr />
<CounterByTen />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
import CounterByTen from './components/CounterByTen.vue';
@Component({
components: {
Counter,
CounterByTen,
},
})
export default class App extends Vue {
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
在src/mixins
文件夹中创建一个名为defaultNumber.ts
的文件。
为了编写我们的 mixin,我们将从vue-class-component
插件中导入Component
和Vue
修饰符,作为 mixin 的基础。我们需要采用类似的代码并将其放入 mixin 中:
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class DefaultNumber extends Vue {
valueNumber: number = 0;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
}
src/components
文件夹中的Counter.vue
组件并导入它。为此,我们需要从vue-class-component
中导入一个特殊的导出,称为mixins
,并将其与我们想要扩展的 mixin 扩展。这将删除Vue
和Component
装饰器,因为它们已经在 mixin 上声明了:<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import DefaultNumber from '../mixins/defaultNumber';
@Component
export default class CounterByTen extends mixins(DefaultNumber) {
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
npm run serve
命令或在命令提示符/PowerShell(Windows)上运行时,您将看到您的组件在屏幕上运行和执行:vue-class-component
时,需要在 mixins 上声明Vue
和Component
装饰器,因为将使用 mixin 的类已经具有此扩展,因为它扩展了此 mixin。vue-class-component
mixins 的更多信息,请访问github.com/vuejs/vue-class-component#using-mixins
。v3.vuejs.org/guide/mixins.html
@vue/cli
@vue/cli-service-global
vue-class-component
创建自定义函数装饰器:在src/decorators
文件夹中创建一个名为componentMount.js
的文件。
我们需要从vue-class-component
中导入createDecorator
函数,以便在基于vue-class-component
的组件上使用它,并开始编写我们的装饰器:
import { createDecorator } from 'vue-class-component';
import componentMountLogger from './componentLogger';
export default createDecorator((options) => {
options.mixins = [...options.mixins, componentMountLogger];
});
createDecorator
函数就像 Vue vm (View-Model)的扩展,因此它不会有 ECMAScript 装饰器的属性,但会作为 Vue 装饰器的功能。componentLogger.js
文件。这个函数将获取在“装饰”组件中设置的所有数据值,并对其添加一个监视器。每当它改变时,这个监视器将记录新值和旧值。这个函数只有在调试数据设置为true
时才会执行:export default {
mounted() {
if (this.debug) {
const componentName = this.name || '';
console.log(`The ${componentName} was mounted
successfully.`);
const dataKeys = Object.keys(this.$data);
if (dataKeys.length) {
console.log('The base data are:');
console.table(dataKeys);
dataKeys.forEach((key) => {
this.$watch(key, (newValue, oldValue) => {
console.log(`The new value for ${key} is:
${newValue}`);
console.log(`The old value for ${key} is:
${oldValue}`);
}, {
deep: true,
});
});
}
}
},
};
src/components
文件夹中的Counter.vue
组件文件中,并向其添加调试器数据:<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button@click="decrease">Decrease</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import componentMount from '../decorators/componentMount';
@Component
@componentMount
export default class Counter extends Vue {
valueNumber: number = 0;
debug: boolean = true;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
createDecorator
函数是一个工厂函数,它扩展了 Vue vm(View Model),产生了 Vue 组件的扩展,比如一个 Vue mixin。Vue mixin 是 Vue 组件的一个属性,可以用来在组件之间共享和重用代码。true
时才会附加。这个调试器将记录当前数据,并为数据的更改设置监视器,每次数据更改时都会在控制台上显示日志。no-param-reassign
规则是必需的,因为装饰器使用选项作为传递值的引用。github.com/vuejs/vue-class-component#create-custom-decorators
找到有关使用vue-class-component
创建自定义装饰器的更多信息。www.typescriptlang.org/docs/handbook/decorators.html
找到有关 ECMAScript 装饰器的更多信息。vue-router
与导航守卫,例如beforeRouterEnter
和beforeRouterLeave
函数钩子。@vue/cli
@vue/cli-service-global
vue-class-component
为您的 Vue 项目添加自定义钩子:vue-router
添加到项目中。这可以在创建 Vue CLI 项目时完成,也可以在创建项目后的 Vue UI 界面中完成。vue-router
。请注意,选择History选项将在部署时需要特殊的服务器配置。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),执行npm run serve
命令,您将看到vue-router
正在工作,并且有两个工作路由器:home
和about
。
让我们开始创建并命名我们的钩子以注册到主应用程序。为此,我们需要在src/classComponentsHooks
文件夹中创建一个vue-router.js
文件:
import Component from 'vue-class-component';
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
]);
main.ts
文件中,因为它需要在应用程序最终构建之前被调用:import './classComponentsHooks/vue-router';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App),
}).$mount('#app');
现在,我们已经在vue-class-component
中注册了这些钩子,并且它们可以在 TypeScript 组件中使用。
我们需要在src/views
文件夹中创建一个名为Secure.vue
的新路由位置。安全页面将有一个输入密码,vuejs
。当用户输入此密码时,路由守卫将授予权限,用户可以看到页面。如果密码错误,用户将被带回到主页。当他们离开页面时,警报将向用户显示一条消息:
<template>
<div class="secure">
<h1>This is an secure page</h1>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Route, RawLocation } from 'vue-router';
type RouteNext = (to?: RawLocation | false | ((vm: Vue) => any) |
void) => void;
@Component
export default class Home extends Vue {
beforeRouteEnter(to: Route, from: Route, next: RouteNext) {
const securePassword = 'vuejs';
const userPassword = prompt('What is the password?');
if (userPassword === securePassword) {
next();
} else if (!userPassword) {
next('/');
}
}
beforeRouteLeave(to: Route, from: Route, next: RouteNext) {
alert('Bye!');
next();
}
}
</script>
router.ts
文件中,以便在 Vue 应用程序中调用它:import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
component: () => import('./views/About.vue'),
},
{
path: '/secure',
name: 'secure',
component: () => import('./views/Secure.vue'),
},
],
});
App.vue
文件中,这样我们就会得到一个集成了钩子的组件:<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/secure">Secure</router-link>
</div>
<router-view/>
</div>
</template>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
#nav
padding 30px
a
font-weight bold
color #2c3e50
&.router-link-exact-active
color #42b983
</style>
main.ts
文件的第一行导入自定义钩子。vue-class-component
已经将所有这些自定义导入转换为组件装饰器的基本方法。vue-router
导航守卫的钩子。这些钩子在每次路由进入或离开时都会被调用。我们没有使用的前两个参数to
和from
是携带有关未来路由和过去路由的信息的参数。next
函数总是必需的,因为它执行路由更改。如果在函数中没有传递参数,路由将继续使用被调用的路由,但如果想要即时更改路由,可以传递参数来改变用户将要前往的位置。router.vuejs.org/guide/advanced/navigation-guards.html
中了解更多关于 vue-router 导航守卫的信息。github.com/vuejs/vue-class-component#adding-custom-hooks
中了解更多关于 vue-class-component 钩子的信息。vue-class-component
中以 TypeScript 装饰器的形式缺失。因此,社区制作了一个名为vue-property-decorator
的库,这个库得到了 Vue 核心团队的全力支持。props
、watch
、model
、inject
等。@vue/cli
@vue/cli-service-global
vue-property-decorator
添加到 Vue基于类的组件
中:vue-property-decorator
添加到我们的项目中。打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:> npm install -S vue-property-decorator
import {
Vue,
Component,
Prop,
} from 'vue-property-decorator';
@Component
export default class DefaultNumber extends Vue {
valueNumber: number = 0;
@Prop(Number) readonly value: number | undefined;
get formattedNumber() {
return `Your total number is: ${this.valueNumber}`;
}
}
src/mixins
文件夹内创建一个名为numberWatcher.ts
的新文件:import {
Watch,
Mixins,
} from 'vue-property-decorator';
import DefaultNumber from './defaultNumber';
export default class NumberWatchers extends Mixins(DefaultNumber) {
@Watch('valueNumber')
onValueNumberChanged(val: number) {
this.$emit('input', val);
}
@Watch('value', { immediate: true })
onValueChanged(val: number) {
this.valueNumber = val;
}
}
v-model
指令的工作原理类似于糖语法,它是 Vue $emit
函数和 Vue props
函数的组合。当值发生变化时,组件需要使用'input'
名称进行$emit
,并且组件需要在props
函数中有一个value
键,这将是从父组件传递到子组件的值。Counter.vue
组件,将导入的 mixin 从defaultNumber.ts
文件更改为numberWatcher.ts
:<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase</button>
<button @click="decrease">Decrease</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import NumberWatcher from '../mixins/numberWatcher';
@Component
export default class Counter extends mixins(NumberWatcher) {
increase() {
this.valueNumber += 1;
}
decrease() {
this.valueNumber -= 1;
}
}
</script>
CounterByTen.vue
组件,并添加新创建的 mixin:<template>
<div>
<fieldset>
<legend>{{ this.formattedNumber }}</legend>
<button @click="increase">Increase By Ten</button>
<button @click="decrease">Decrease By Ten</button>
</fieldset>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component, { mixins } from 'vue-class-component';
import NumberWatcher from '../mixins/numberWatcher';
@Component
export default class CounterByTen extends mixins(NumberWatcher) {
increase() {
this.valueNumber += 10;
}
decrease() {
this.valueNumber -= 10;
}
}
</script>
App.vue
组件。这一次,我们将在组件中存储一个变量,该变量将传递给两个子组件,当组件发出更新事件时,此变量将自动更改,也会更新其他组件:<template>
<div id="app">
<Counter
v-model="amount"
/>
<hr />
<CounterByTen
v-model="amount"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Counter from './components/Counter.vue';
import CounterByTen from './components/CounterByTen.vue';
@Component({
components: {
Counter,
CounterByTen,
},
})
export default class App extends Vue {
amount: number = 0;
}
</script>
<style lang="stylus">
#app
font-family 'Avenir', Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
margin-top 60px
</style>
vue-class-components
中注入装饰器,vue-property-decorator
帮助 TypeScript 编译器检查 Vue 代码的类型和静态分析。@Watch
和@Prop
装饰器。vue-property-decorator
相同,但用于vuex
插件,名为vuex-class
。vue-property-decorator
相同的过程。它在组件中创建一个 inject 装饰器。这些装饰器帮助 TypeScript 编译器在开发过程中检查类型。github.com/ktsn/vuex-class/
找到有关这个库的更多信息。github.com/kaorun343/vue-property-decorator
找到有关vue-property-decorator
的更多信息。vue-devtools
深入了解 Vue 组件并查看我们的数据和应用程序发生了什么。创建“hello world”组件
创建具有双向数据绑定的输入表单
向元素添加事件侦听器
从输入中删除 v-model
创建动态待办事项列表
创建计算属性并探索其工作原理
使用自定义过滤器显示更清晰的数据和文本
使用 Vuelidate 添加表单验证
为列表创建过滤器和排序器
创建条件过滤以对列表数据进行排序
添加自定义样式和过渡
使用vue-devtools
调试您的应用程序
windows-build-tools
的npm
包,以便安装以下所需的软件包。为此,请以管理员身份打开 PowerShell 并执行以下命令:> npm install -g windows-build-tools
。> npm install -g @vue/cli @vue/cli-service-global
@vue/cli
@vue/cli-service-global
> vue create my-component
default
选项:? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
让我们在src/components
文件夹中创建一个名为CurrentTime.vue
的新文件。
在这个文件中,我们将从组件的<template>
部分开始。它将是一个显示当前日期格式化的阴影框卡:
<template>
<div class='cardBox'>
<div class='container'>
<h2>Today is:</h2>
<h3>{{ getCurrentDate }}</h3>
</div>
</div>
</template>
<script>
部分。我们将从name
属性开始。这将在使用vue-devtools
调试我们的应用程序时使用,也有助于集成开发环境(IDE)。对于getCurrentDate
计算属性,我们将创建一个computed
属性,它将返回当前日期,由Intl
浏览器函数格式化:<script>
export default {
name: 'CurrentTime',
computed: {
getCurrentDate() {
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date());
}
}
};
</script>
src
文件夹中创建一个style.css
文件,然后将cardBox
样式添加到其中:.cardBox {
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s linear;
max-width: 33%;
border-radius: 3px;
margin: 20px;
}
.cardBox:hover {
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2);
}
.cardBox>.container {
padding: 4px 18px;
}
[class*='col-'] {
display: inline-block;
}
@media only screen and (max-width: 600px) {
[class*='col-'] {
width: 100%;
}
.cardBox {
margin: 20px 0;
}
}
@media only screen and (min-width: 600px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 768px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 992px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
@media only screen and (min-width: 1200px) {
.col-1 {width: 8.33%;}
.col-2 {width: 16.66%;}
.col-3 {width: 25%;}
.col-4 {width: 33.33%;}
.col-5 {width: 41.66%;}
.col-6 {width: 50%;}
.col-7 {width: 58.33%;}
.col-8 {width: 66.66%;}
.col-9 {width: 75%;}
.col-10 {width: 83.33%;}
.col-11 {width: 91.66%;}
.col-12 {width: 100%;}
}
App.vue
文件中,我们需要导入我们的组件才能看到它:<template>
<div id='app'>
<current-time />
</div>
</template>
<script>
import CurrentTime from './components/CurrentTime.vue';
export default {
name: 'app',
components: {
CurrentTime
}
}
</script>
main.js
文件中,我们需要导入style.css
文件以包含在 Vue 应用程序中:import Vue from 'vue';
import App from './App.vue';
import './style.css';
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
> npm run serve
components
属性中声明它。Intl.DateTimeFormat
函数,这是一个本机函数,可用于将日期格式化和解析为声明的位置。为了获得本地格式,我们使用了navigator
全局变量。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。v3.vuejs.org/guide/single-file-component.html
找到有关 Vue 组件的更多信息@vue/cli
@vue/cli-service-global
让我们在src/components
文件夹中创建一个名为TaskInput.vue
的新文件。
在这个文件中,我们将创建一个组件,其中将包含一个文本输入和一个显示文本。这个文本将基于文本输入中键入的内容。在组件的<template>
部分,我们需要创建一个 HTML 输入和一个mustache
变量,用于接收和呈现数据:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is: {{ task }}</strong>
<input
type='text'
v-model='task'
class='taskInput' />
</div>
</div>
</template>
<script>
部分,我们将命名它并将任务添加到data
属性中。由于数据始终需要返回一个Object
,我们将使用箭头函数直接返回一个Object
:<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
};
</script>
<style>
部分,我们需要添加scoped
属性,以便样式仅绑定到组件,不会与其他层叠样式表(CSS)规则混合:<style scoped>
.tasker{
margin: 20px;
}
.tasker .taskInput {
font-size: 14px;
margin: 0 10px;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.75);
}
.tasker button {
border: 1px solid rgba(0, 0, 0, 0.75);
border-radius: 3px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
</style>
App.vue
文件中:<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' />
</div>
</template>
<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'app',
components: {
CurrentTime,
TaskInput,
}
}
</script>
> npm run serve
input
元素并为其添加v-model
时,您正在传递一个内置于 Vue 中的指令,该指令检查输入类型并为我们提供输入的糖语法。这处理了变量值和 DOM 的更新。input-form
,那么 JavaScript 代码就可以执行一个函数。v3.vuejs.org/guide/forms.html
找到有关表单输入绑定的更多信息@vue/cli
@vue/cli-service-global
创建一个新组件或打开TaskInput.vue
文件。
在<template>
部分,我们将添加一个按钮元素,并使用v-on
指令为按钮点击事件添加事件监听器。我们将从组件中删除{{ task }}
变量,因为从现在开始它将被发出并不再显示在组件上:
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
v-model='task'
class='taskInput' />
<button
v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
<script>
部分,我们需要添加一个处理点击事件的方法。该方法将被命名为addTask
。该方法将触发一个名为add-task
的事件,并将任务发送到数据中。之后,组件上的任务将被重置:<script>
export default {
name: 'TaskInput',
data: () => ({
task: '',
}),
methods: {
addTask(){
this.$emit('add-task', this.task);
this.task = '';
},
}
};
</script>
App.vue
文件中,我们需要在组件上添加一个事件监听器绑定。此侦听器将附加到add-task
事件。我们将使用v-on
指令的缩写版本@
。当它被触发时,事件将调用addNewTask
方法,该方法将发送一个警报,说明已添加了一个新任务:<template>
<div id='app'>
<current-time class='col-4' />
<task-input
class='col-6'
@add-task='addNewTask'
/>
</div>
</template>
addNewTask
方法。这将接收任务作为参数,并向用户显示一个警报,显示任务已添加:<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'app',
components: {
CurrentTime,
TaskInput,
},
methods:{
addNewTask(task){
alert(`New task added: ${task}`);
},
},
}
</script>
> npm run serve
v-on
事件处理指令由 Vue 读取。当我们将v-on:click
指令附加到按钮时,我们向按钮添加了一个监听器,以便在用户单击按钮时执行一个函数。v-on
指令监听它。v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息v-model
的魔术背后有很多代码,使我们的魔术糖语法发生?如果我告诉您,兔子洞可以深入到足以控制输入的事件和值发生的一切?v-model
指令的糖语法,并将其转换为其背后的基本语法。@vue/cli
@vue/cli-service-global
v-model
指令的语法糖:打开TaskInput.vue
文件。
在组件的<template>
块中,找到v-model
指令。我们将删除v-model
指令。然后,我们需要向输入添加一个新的绑定,称为v-bind:value
或缩写版本:value
,以及一个事件侦听器到 HTMLinput
元素。我们需要向input
事件添加一个事件侦听器,使用v-on:input
指令或缩写版本@input
。输入绑定将接收任务值作为参数,事件侦听器将接收一个值赋值,其中它将使任务变量等于事件值的值。
<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
:value='task'
@input='task = $event.target.value'
class='taskInput'
/>
<button v-on:click='addTask'>
Add Task
</button>
</div>
</div>
</template>
> npm run serve
v-model
指令可以自动为您声明绑定和元素的事件侦听器,但副作用是您无法完全控制可以实现的内容。$event
变量用于传递事件。在这种情况下,就像在普通 JavaScript 中一样,要捕获输入的值,我们需要使用event.target.value
值。v3.vuejs.org/guide/events.html
找到有关事件处理的更多信息@vue/cli
@vue/cli-service-global
App.vue
文件中,我们将创建我们的任务数组。每当TaskInput.vue
组件发出消息时,这个任务将被填充。我们将向这个数组添加一个带有任务和创建任务的当前日期的对象。目前,任务完成时的日期将是未定义的。为了做到这一点,在组件的<script>
部分,我们需要创建一个接收任务并将该任务与当前日期添加到taskList
数组中的方法:<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput,
},
data: () => ({
taskList: [],
}),
methods:{
addNewTask(task){
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined,
})
},
},
}
</script>
<template>
部分呈现这个列表。我们将使用 Vue 的v-for
指令来迭代任务列表。当我们将这个指令与数组一起使用时,它会给我们访问两个属性——项目本身和项目的索引。我们将使用项目来呈现它,使用索引来创建元素的键以进行呈现。我们需要添加一个复选框,当选中时,调用一个函数来改变任务的状态和任务完成时的显示:<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in taskList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
Math.random()
函数添加到索引中以生成一个唯一的键,因为当元素的数量减少时,数组的前几个元素的索引始终是相同的数字。App.vue
的methods
属性上创建changeStatus
函数。这个函数将接收任务的索引作为参数,然后去改变任务数组中的finishedAt
属性,这是我们标记任务完成的标志:changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
<style>
部分,我们将使其具有作用域并添加自定义类:<style scoped>
.taskList li{
text-align: left;
}
</style>
> npm run serve
finishedAt
属性设置为undefined
。v3.vuejs.org/guide/list.html#mapping-an-array-to-elements-with-v-for
找到有关列表渲染的更多信息v3.vuejs.org/guide/conditional.html#v-if
找到有关条件渲染的更多信息developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
找到有关Math.random
的更多信息。@vue/cli
@vue/cli-service-global
App.vue
文件的<script>
部分,我们将在data
和method
之间添加一个名为computed
的新属性。这是computed
属性将被放置的地方。我们将创建一个名为displayList
的新计算属性,它将用于在模板上呈现最终列表:<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList(){
return this.taskList;
},
},
methods: {
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex){
const task = this.taskList[taskIndex];
if(task.finishedAt){
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
displayList
属性目前只返回变量的缓存值,而不是直接的变量本身。<template>
部分,我们需要更改列表的获取位置:<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'>
{{ taskItem.finishedAt }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
> npm run serve
computed
属性将值传递到模板时,该值现在被缓存。这意味着只有在值更新时才会触发渲染过程。同时,我们确保模板不使用变量进行渲染,因此它不能在模板上更改,因为它是变量的缓存副本。computed
属性会缓存结果并不会更新最终结果。v3.vuejs.org/guide/computed.html
找到有关计算属性的更多信息。DateTime
格式。我们如何解决这个问题?在 Vue 中渲染数据时,可以使用我们称之为过滤器的东西。@vue/cli
@vue/cli-service-global
App.vue
文件的<script>
部分,在方法中,创建一个formatDate
函数。此函数将接收value
作为参数并进入过滤器管道。我们可以检查value
是否是一个数字,因为我们知道我们的时间是基于 Unix 时间戳格式的。如果它是一个数字,我们将根据当前浏览器位置进行格式化,并返回该格式化的值。如果传递的值不是一个数字,我们只需返回传递的值。<script>
import CurrentTime from './components/CurrentTime.vue';
import TaskInput from './components/TaskInput';
export default {
name: 'TodoApp',
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: []
}),
computed: {
displayList() {
return this.taskList;
}
},
methods: {
formatDate(value) {
if (!value) return '';
if (typeof value !== 'number') return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(
browserLocale,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
<template>
部分,我们需要将变量传递给过滤器方法。为此,我们需要找到taskItem.finishedAt
属性,并将其作为formatDate
方法的参数。我们将添加一些文本来表示任务在日期开始时已完成:<template>
<div id='app'>
<current-time class='col-4' />
<task-input class='col-6' @add-task='addNewTask' />
<div class='col-12'>
<div class='cardBox'>
<div class='container'>
<h2>My Tasks</h2>
<ul class='taskList'>
<li
v-for='(taskItem, index) in displayList'
:key='`${index}_${Math.random()}`'
>
<input type='checkbox'
:checked='!!taskItem.finishedAt'
@input='changeStatus(index)'
/>
{{ taskItem.task }}
<span v-if='taskItem.finishedAt'> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
> npm run serve
<template>
部分显示或在 Vue 属性中使用的方法。formatDate
方法时,我们知道它是一个有效的 Unix 时间戳,因此可以调用新的Date
类构造函数,将value
作为参数传递,因为 Unix 时间戳是一个有效的日期构造函数。Intl.DateTimeFormat
函数,这是一个原生函数,可用于格式化和解析日期到声明的位置。为了获得本地格式,我们使用navigator
全局变量。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
找到有关Intl.DateTimeFormat
的更多信息。@vue/cli
@vue/cli-service-global
> npm install vuelidate --save
src
文件夹中的main.js
文件中导入并添加它:import Vue from 'vue';
import App from './App.vue';
import Vuelidate from 'vuelidate';
import './style.css';
Vue.config.productionTip = false
Vue.use(Vuelidate);
new Vue({
render: h => h(App),
}).$mount('#app')
TaskInput.vue
文件中,我们将向 Vue 对象添加一个新属性。这个属性由安装的新插件解释。在对象的末尾,我们将添加validations
属性,并在该属性内添加模型的名称。模型是插件将检查验证的数据或计算属性的直接名称:<script>
export default {
name: 'TaskInput',
data: () => ({
task: ''
}),
methods: {
addTask() {
this.$emit('add-task', this.task);
this.task = '';
}
},
validations: {
task: {}
}
};
</script>
required
和minLength
。导入后,我们将这些规则添加到模型中:<script>
import { required, minLength } from 'vuelidate/lib/validators';
export default {
name: 'TaskInput',
data: () => ({
task: ''
}),
methods: {
addTask() {
this.$emit('add-task', this.task);
this.task = '';
}
},
validations: {
task: {
required,
minLength: minLength(5),
}
}
};
</script>
$touch
内置函数告诉插件该字段已被用户触摸,并进行验证。如果有任何字段与用户有任何交互,插件将相应地设置标志。如果没有错误,我们将发出事件,并使用$reset
函数重置验证。为此,我们将更改addTask
方法:addTask() {
this.$v.task.$touch();
if (this.$v.task.$error) return false;
this.$emit('add-task', this.task);
this.task = '';
this.$v.task.$reset();
return true;
}
$error
属性上:<template>
<div class='cardBox'>
<div class='container tasker'>
<strong>My task is:</strong>
<input
type='text'
:value='task'
@input='task = $event.target.value'
class='taskInput'
:class="$v.task.$error ? 'fieldError' : ''"
/>
<button v-on:click='addTask'>Add Task</button>
</div>
</div>
</template>
src
文件夹中的style.css
文件中创建一个fieldError
类:.fieldError {
border: 2px solid red !important;
color: red;
border-radius: 3px;
}
> **npm run serve**
$v
属性,并在 Vue 对象中检查一个名为validations
的新对象属性。当定义了此属性并具有一些规则时,插件会在每次更新时检查模型的规则。vuelidate.netlify.com/
找到有关 Vuelidate 的更多信息。v3.vuejs.org/guide/class-and-style.html
找到有关类和样式绑定的更多信息@vue/cli
@vue/cli-service-global
App.vue
文件的<script>
部分,我们将添加新的计算属性;这些将用于排序和过滤。我们将添加三个新的计算属性,baseList
,filteredList
和sortedList
。baseList
属性将是我们的第一个操作。我们将通过Array.map
向任务列表添加一个id
属性。由于 JavaScript 数组从零开始,我们将在数组的索引上添加1
。filteredList
属性将过滤baseList
属性,并返回未完成的任务,sortedList
属性将对filteredList
属性进行排序,以便最后添加的id
属性将首先显示给用户:<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return [...this.baseList]
.filter(t => !t.finishedAt);
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => b.id - a.id);
},
displayList() {
return this.sortedList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskIndex) {
const task = this.taskList[taskIndex];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
<template>
部分,我们将Task ID
添加为指示器,并更改changeStatus
方法发送参数的方式。因为现在索引是可变的,我们不能将其用作变量;它只是数组上的临时索引。我们需要使用任务id
:<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
changeStatus
方法中,我们也需要更新我们的函数。由于索引现在从1
开始,我们需要将数组的索引减一,以获取更新前元素的真实索引:changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
> npm run serve
computed
属性一起作为列表的缓存工作,并确保对元素的操作没有副作用:在baseList
属性中,我们创建了一个新数组,其中包含相同的任务,但为任务添加了一个新的id
属性。
在filteredList
属性中,我们取出了baseList
属性,并且只返回了未完成的任务。
在sortedList
属性上,我们按照它们的 ID,按降序对filteredList
属性上的任务进行排序。
displayList
属性将返回被操作的数据的结果。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。@vue/cli
@vue/cli-service-global
App.vue
文件的<script>
部分,我们将更新computed
属性,filteredList
,sortedList
和displayList
。我们需要向我们的项目添加三个新变量,hideDone
,reverse
和sortById
。所有三个变量都将是布尔变量,并且默认值为false
。filteredList
属性将检查hideDone
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将显示整个列表而不进行任何过滤。sortedList
属性将检查sortById
变量是否为true
。如果是,它将具有相同的行为,但如果不是,它将按任务完成日期对列表进行排序。displayList
属性将检查reverse
变量是否为true
。如果是,它将颠倒显示的列表,但如果不是,它将具有相同的行为:<script>
import CurrentTime from "./components/CurrentTime.vue";
import TaskInput from "./components/TaskInput";
export default {
name: "TodoApp",
components: {
CurrentTime,
TaskInput
},
data: () => ({
taskList: [],
hideDone: false,
reverse: false,
sortById: false,
}),
computed: {
baseList() {
return [...this.taskList]
.map((t, index) => ({
...t,
id: index + 1
}));
},
filteredList() {
return this.hideDone
? [...this.baseList]
.filter(t => !t.finishedAt)
: [...this.baseList];
},
sortedList() {
return [...this.filteredList]
.sort((a, b) => (
this.sortById
? b.id - a.id
: (a.finishedAt || 0) - (b.finishedAt || 0)
));
},
displayList() {
const taskList = [...this.sortedList];
return this.reverse
? taskList.reverse()
: taskList;
}
},
methods: {
formatDate(value) {
if (!value) return "";
if (typeof value !== "number") return value;
const browserLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const intlDateTime = new Intl.DateTimeFormat(browserLocale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
});
return intlDateTime.format(new Date(value));
},
addNewTask(task) {
this.taskList.push({
task,
createdAt: Date.now(),
finishedAt: undefined
});
},
changeStatus(taskId) {
const task = this.taskList[taskId - 1];
if (task.finishedAt) {
task.finishedAt = undefined;
} else {
task.finishedAt = Date.now();
}
}
}
};
</script>
<template>
部分,我们需要为这些变量添加控制器。我们将创建三个复选框,直接通过v-model
指令与变量链接:<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
> npm run serve
computed
属性一起作为列表的缓存工作,并确保在元素操作中没有任何副作用。通过条件处理,可以通过变量更改过滤和排序规则,并且显示会实时更新:在filteredList
属性处,我们取出了baseList
属性,并返回了未完成的任务。当hideDone
变量为false
时,我们返回整个列表而不进行任何过滤。
在sortedList
属性处,我们对filteredList
属性上的任务进行了排序。当sortById
变量为true
时,列表按 ID 降序排序;当为false
时,按任务完成时间升序排序。
在displayList
属性处,当reverse
变量为true
时,最终列表被颠倒。
displayList
属性返回了被操作的数据的结果。computed
属性由用户屏幕上的复选框控制,因此用户可以完全控制他们可以看到什么以及如何看到它。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
找到有关Array.prototype.map
的更多信息。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
找到有关Array.prototype.filter
的更多信息。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
找到有关Array.prototype.sort
的更多信息。@vue/cli
@vue/cli-service-global
App.vue
文件中,我们将为已完成的任务的列表项添加一个条件类:<template>
<div id="app">
<current-time class="col-4" />
<task-input class="col-6" @add-task="addNewTask" />
<div class="col-12">
<div class="cardBox">
<div class="container">
<h2>My Tasks</h2>
<hr />
<div class="col-4">
<input
v-model="hideDone"
type="checkbox"
id="hideDone"
name="hideDone"
/>
<label for="hideDone">
Hide Done Tasks
</label>
</div>
<div class="col-4">
<input
v-model="reverse"
type="checkbox"
id="reverse"
name="reverse"
/>
<label for="reverse">
Reverse Order
</label>
</div>
<div class="col-4">
<input
v-model="sortById"
type="checkbox"
id="sortById"
name="sortById"
/>
<label for="sortById">
Sort By Id
</label>
</div>
<ul class="taskList">
<li
v-for="(taskItem, index) in displayList"
:key="`${index}_${Math.random()}`"
:class="!!taskItem.finishedAt ? 'taskDone' : ''"
>
<input type="checkbox"
:checked="!!taskItem.finishedAt"
@input="changeStatus(taskItem.id)"
/>
#{{ taskItem.id }} - {{ taskItem.task }}
<span v-if="taskItem.finishedAt"> |
Done at:
{{ formatDate(taskItem.finishedAt) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style>
部分,我们将为taskDone
的 CSS 类创建 CSS 样式表类。我们需要让列表项之间有一个分隔符;然后,我们将使列表具有条纹样式;当它们被标记为完成时,背景将发生变化。要在行之间添加分隔符和条纹列表或斑马样式,我们需要添加一个 CSS 样式表规则,适用于我们列表的每个even nth-child
:<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
</style>
<style>
部分的末尾添加 CSS 动画关键帧,指示背景颜色变化,并将此动画应用于.taskDone
CSS 类,以在任务完成时添加背景效果<style scoped>
.taskList li {
list-style: none;
text-align: left;
padding: 5px 10px;
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.taskList li:last-child {
border-bottom: 0px;
}
.taskList li:nth-child(even){
background-color: rgba(0,0,0,0.05);
}
@keyframes colorChange {
from{
background-color: inherit;
}
to{
background-color: rgba(0, 160, 24, 0.577);
}
}
.taskList li.taskDone{
animation: colorChange 1s ease;
background-color: rgba(0, 160, 24, 0.577);
}
</style>
> npm run serve
displayList
属性都会更新并触发组件的重新渲染。taskDone
CSS 类附加了一个在渲染时执行的动画,显示绿色背景。developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
找到有关 CSS 动画的更多信息。v3.vuejs.org/guide/class-and-style.html
找到有关类和样式绑定的更多信息vue-devtools
对于每个 Vue 开发人员都是必不可少的。这个工具向我们展示了 Vue 组件、路由、事件和 vuex 的深度。vue-devtools
扩展程序,可以调试我们的应用程序,在更改代码之前尝试新数据,执行函数而无需直接在代码中调用它们,等等。@vue/cli
@vue/cli-service-global
vue-devtools
扩展程序:Chrome 扩展程序-bit.ly/chrome-vue-devtools
Firefox 扩展程序-bit.ly/firefox-vue-devtools
vue-devtools
进行开发。vue-devtools
以及如何正确调试 Vue 应用程序:vue-devtools
,首先需要在浏览器中安装它,所以请查看本教程的“准备就绪”部分,获取 Chrome 或 Firefox 的扩展链接。在 Vue 开发应用程序中,进入浏览器开发者检查器模式。一个名为 Vue 的新标签页必须出现:vuelidate
,vue-router
或vuex
)注入的额外数据。您可以编辑数据以实时查看应用程序中的更改:vue-devtools
的刷新按钮。有时,当hot-module-reload
发生或者应用程序组件树中发生一些复杂事件时,扩展程序可能会失去对发生情况的跟踪。这个按钮强制扩展程序重新加载并再次读取 Vue 应用程序状态。github.com/vuejs/vue-devtools
找到有关vue-devtools
的更多信息。创建一个可视化模板组件
使用插槽和命名插槽将数据放入您的组件中
将数据传递给您的组件并验证数据
创建功能性组件
访问您的子组件数据
创建一个动态注入的组件
创建一个依赖注入组件
创建一个组件mixin
延迟加载您的组件
windows-build-tools
的 NPM 包,以便能够安装以下所需的包。为此,请以管理员身份打开 PowerShell 并执行以下命令:npm install -g windows-build-tools
> npm install -g @vue/cli @vue/cli-service-global
div
HTML 元素的作用域 CSS,或者它可以是一个更复杂的组件,可以实时计算元素在屏幕上的位置。@vue/cli
@vue/cli-service-global
> vue create visual-component
default
选项:? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
让我们在src/components
文件夹中创建一个名为MaterialCardBox.vue
的新文件。
在这个文件中,我们将从组件的模板开始。我们需要为卡片创建一个框。通过使用 Material Design 指南,这个框将有阴影和圆角:
<template>
<div class="cardBox elevation_2">
<div class="section">
This is a Material Card Box
</div>
</div>
</template>
<script>
部分中,我们将只添加我们的基本名称:<script>
export default {
name: 'MaterialCardBox',
};
</script>
style
文件夹中创建一个名为elevation.css
的文件。在那里,我们将创建从0
到24
的高程,以遵循 Material Design 指南上的所有高程:.elevation_0 {
border: 1px solid rgba(0, 0, 0, 0.12);
}
.elevation_1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2),
0 1px 1px rgba(0, 0, 0, 0.14),
0 2px 1px -1px rgba(0, 0, 0, 0.12);
}
.elevation_2 {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.elevation_3 {
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2),
0 3px 4px rgba(0, 0, 0, 0.14),
0 3px 3px -2px rgba(0, 0, 0, 0.12);
}
.elevation_4 {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 4px 5px rgba(0, 0, 0, 0.14),
0 1px 10px rgba(0, 0, 0, 0.12);
}
.elevation_5 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
}
.elevation_6 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 6px 10px rgba(0, 0, 0, 0.14),
0 1px 18px rgba(0, 0, 0, 0.12);
}
.elevation_7 {
box-shadow: 0 4px 5px -2px rgba(0, 0, 0, 0.2),
0 7px 10px 1px rgba(0, 0, 0, 0.14),
0 2px 16px 1px rgba(0, 0, 0, 0.12);
}
.elevation_8 {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.elevation_9 {
box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.2),
0 9px 12px 1px rgba(0, 0, 0, 0.14),
0 3px 16px 2px rgba(0, 0, 0, 0.12);
}
.elevation_10 {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12);
}
.elevation_11 {
box-shadow: 0 6px 7px -4px rgba(0, 0, 0, 0.2),
0 11px 15px 1px rgba(0, 0, 0, 0.14),
0 4px 20px 3px rgba(0, 0, 0, 0.12);
}
.elevation_12 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 12px 17px 2px rgba(0, 0, 0, 0.14),
0 5px 22px 4px rgba(0, 0, 0, 0.12);
}
.elevation_13 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
}
.elevation_14 {
box-shadow: 0 7px 9px -4px rgba(0, 0, 0, 0.2),
0 14px 21px 2px rgba(0, 0, 0, 0.14),
0 5px 26px 4px rgba(0, 0, 0, 0.12);
}
.elevation_15 {
box-shadow: 0 8px 9px -5px rgba(0, 0, 0, 0.2),
0 15px 22px 2px rgba(0, 0, 0, 0.14),
0 6px 28px 5px rgba(0, 0, 0, 0.12);
}
.elevation_16 {
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
0 16px 24px 2px rgba(0, 0, 0, 0.14),
0 6px 30px 5px rgba(0, 0, 0, 0.12);
}
.elevation_17 {
box-shadow: 0 8px 11px -5px rgba(0, 0, 0, 0.2),
0 17px 26px 2px rgba(0, 0, 0, 0.14),
0 6px 32px 5px rgba(0, 0, 0, 0.12);
}
.elevation_18 {
box-shadow: 0 9px 11px -5px rgba(0, 0, 0, 0.2),
0 18px 28px 2px rgba(0, 0, 0, 0.14),
0 7px 34px 6px rgba(0, 0, 0, 0.12);
}
.elevation_19 {
box-shadow: 0 9px 12px -6px rgba(0, 0, 0, 0.2),
0 19px 29px 2px rgba(0, 0, 0, 0.14),
0 7px 36px 6px rgba(0, 0, 0, 0.12);
}
.elevation_20 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 20px 31px 3px rgba(0, 0, 0, 0.14),
0 8px 38px 7px rgba(0, 0, 0, 0.12);
}
.elevation_21 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2),
0 21px 33px 3px rgba(0, 0, 0, 0.14),
0 8px 40px 7px rgba(0, 0, 0, 0.12);
}
.elevation_22 {
box-shadow: 0 10px 14px -6px rgba(0, 0, 0, 0.2),
0 22px 35px 3px rgba(0, 0, 0, 0.14),
0 8px 42px 7px rgba(0, 0, 0, 0.12);
}
.elevation_23 {
box-shadow: 0 11px 14px -7px rgba(0, 0, 0, 0.2),
0 23px 36px 3px rgba(0, 0, 0, 0.14),
0 9px 44px 8px rgba(0, 0, 0, 0.12);
}
.elevation_24 {
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14),
0 9px 46px 8px rgba(0, 0, 0, 0.12);
}
<style>
部分中设置样式,我们需要在<style>
标签内设置scoped
属性,以确保视觉样式不会干扰应用程序中的任何其他组件。我们将使这张卡遵循 Material Design 指南。我们需要导入Roboto
字体系列并将其应用于将包装在此组件内的所有元素:<style scoped>
@import url('https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap');
@import '../style/elevation.css';
*{
font-family: 'Roboto', sans-serif;
}
.cardBox{
width: 100%;
max-width: 300px;
background-color: #fff;
position: relative;
display: inline-block;
border-radius: 0.25rem;
}
.cardBox > .section {
padding: 1rem;
position: relative;
}
</style>
> npm run serve
vue-loader.vuejs.org/guide/scoped-css.html#child-component-root-elements
找到有关作用域 CSS 的更多信息。material.io/components/cards/
找到有关 Material Design 卡片的更多信息。fonts.google.com/specimen/Roboto
上查看 Roboto 字体系列。@vue/cli
@vue/cli-service-global
让我们打开组件文件夹中的名为MaterialCardBox.vue
的文件。
在组件的<template>
部分,我们需要在卡片上添加四个主要部分。这些部分基于 Material Design 卡片解剖学,分别是header
、media
、main section
和action
区域。我们将使用默认插槽来放置main section
,其余部分都将是命名作用域。对于一些命名插槽,我们将添加一个备用配置,如果用户没有在插槽上选择任何设置,将显示该配置:
<template>
<div class="cardBox elevation_2">
<div class="header">
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
Card Header
</h1>
<h2 class="cardSubHeader cardText">
Card Sub Header
</h2>
</div>
</div>
<div class="media">
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
src="https://via.placeholder.com/350x250"
>
</div>
<div
v-if="$slots.default"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot />
</div>
<div
v-if="$slots.action"
class="action"
>
<slot name="action" />
</div>
</div>
</template>
style
文件夹中,创建一个名为cardStyles.css
的新文件,在那里我们将添加卡片文本和标题的规则:h1, h2, h3, h4, h5, h6{
margin: 0;
}
.cardText{
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-decoration: inherit;
text-transform: inherit;
font-size: 0.875rem;
line-height: 1.375rem;
letter-spacing: 0.0071428571em;
}
h1.cardHeader{
font-size: 1.25rem;
line-height: 2rem;
font-weight: 500;
letter-spacing: .0125em;
}
h2.cardSubHeader{
font-size: .875rem;
line-height: 1.25rem;
font-weight: 400;
letter-spacing: .0178571429em;
opacity: .6;
}
<style>
部分,我们需要创建一些 CSS 样式表来遵循我们的设计指南的规则:<style scoped>
@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap");
@import "../style/elevation.css";
@import "../style/cardStyles.css";
* {
font-family: "Roboto", sans-serif;
}
.cardBox {
width: 100%;
max-width: 300px;
border-radius: 0.25rem;
background-color: #fff;
position: relative;
display: inline-block;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.cardBox > .header {
padding: 1rem;
position: relative;
display: block;
}
.cardBox > .media {
overflow: hidden;
position: relative;
display: block;
max-width: 100%;
}
.cardBox > .section {
padding: 1rem;
position: relative;
margin-bottom: 1.5rem;
display: block;
}
.cardBox > .action {
padding: 0.5rem;
position: relative;
display: block;
}
.cardBox > .action > *:not(:first-child) {
margin-left: 0.4rem;
}
.noBottomPadding {
padding-bottom: 0 !important;
}
.halfPaddingTop {
padding-top: 0.5rem !important;
}
</style>
src
文件夹中的App.vue
文件中,我们需要向这些插槽添加元素。这些元素将被添加到每个命名插槽和默认插槽中。我们将更改文件的<template>
部分中的组件。要添加命名插槽,我们需要使用一个名为v-slot:
的指令,然后是我们想要使用的插槽的名称:<template>
<div id="app">
<MaterialCardBox>
<template v-slot:header>
<strong>Card Title</strong><br>
<span>Card Sub-Title</span>
</template>
<template v-slot:media>
<img src="https://via.placeholder.com/350x150">
</template>
<p>Main Section</p>
<template v-slot:action>
<button>Action Button</button>
<button>Action Button</button>
</template>
</MaterialCardBox>
</div>
</template>
<slot />
部分中。> npm run serve
.vue
)的<template>
部分中向该组件放置任何信息,您需要添加v-slot:
指令,以便 Vue 能够知道在何处放置传递下来的信息。vuejs.org/v2/guide/components-slots.html
找到有关 Vue 插槽的更多信息。material.io/components/cards/#anatomy
找到有关 Material Design 卡片解剖的更多信息。@vue/cli
@vue/cli-service-global
让我们在src/components
文件夹中打开名为MaterialCardBox.vue
的文件。
在组件的<script>
部分,我们创建一个名为props
的新属性。该属性接收组件数据,该数据可以用于视觉操作、代码内的变量或需要执行的函数。在此属性中,我们需要声明属性的名称、类型、是否必需以及验证函数。此函数将在运行时执行,以验证传递的属性是否有效:
<script>
export default {
name: 'MaterialCardBox',
inheritAttrs: false,
props: {
header: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
subHeader: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
mainText: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
showMedia: {
type: Boolean,
required: false,
default: false,
validator: v => typeof v === 'boolean',
},
imgSrc: {
type: String,
required: false,
default: '',
validator: v => typeof v === 'string',
},
showActions: {
type: Boolean,
required: false,
default: false,
validator: v => typeof v === 'boolean',
},
elevation: {
type: Number,
required: false,
default: 2,
validator: v => typeof v === 'number',
},
},
computed: {},
};
</script>
<script>
部分的computed
属性中,我们需要创建一组用于呈现卡片的视觉操作规则。这些规则将是showMediaContent
、showActionsButtons
、showHeader
和cardElevation
。每个规则将检查接收到的props
和$slots
对象,以查看是否需要呈现相关的卡片部分: computed: {
showMediaContent() {
return (this.$slots.media || this.imgSrc) && this.showMedia;
},
showActionsButtons() {
return this.showActions && this.$slots.action;
},
showHeader() {
return this.$slots.header || (this.header || this.subHeader);
},
showMainContent() {
return this.$slots.default || this.mainText;
},
cardElevation() {
return `elevation_${parseInt(this.elevation, 10)}`;
},
},
<template>
部分。它们将影响我们卡片的外观和行为。例如,如果没有定义头部插槽,并且定义了头部属性,我们将显示备用头部。该头部是通过props
传递下来的数据:<template>
<div
class="cardBox"
:class="cardElevation"
>
<div
v-if="showHeader"
class="header"
>
<slot
v-if="$slots.header"
name="header"
/>
<div v-else>
<h1 class="cardHeader cardText">
{{ header }}
</h1>
<h2 class="cardSubHeader cardText">
{{ subHeader }}
</h2>
</div>
</div>
<div
v-if="showMediaContent"
class="media"
>
<slot
v-if="$slots.media"
name="media"
/>
<img
v-else
:src="imgSrc"
>
</div>
<div
v-if="showMainContent"
class="section cardText"
:class="{
noBottomPadding: $slots.action,
halfPaddingTop: $slots.media,
}"
>
<slot v-if="$slots.default" />
<p
v-else
class="cardText"
>
{{ mainText }}
</p>
</div>
<div
v-if="showActionsButtons"
class="action"
>
<slot
v-if="$slots.action"
name="action"
/>
</div>
</div>
</template>
> npm run serve
vue-template-compiler
将获取这些属性并将它们转换为 JavaScript 对象。vuejs.org/v2/guide/components-props.html
找到有关 props
的更多信息。vue-loader.vuejs.org/guide/
找到有关 vue-template-compiler
的更多信息。@vue/cli
@vue/cli-service-global
在src/components
文件夹中创建一个名为MaterialButton.vue
的新文件。
在这个组件中,我们需要验证我们将接收的 prop 是否是有效的颜色。为此,在项目中安装is-color
模块。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save is-color
<script>
部分,我们需要创建功能组件将接收的props
对象。由于功能组件只是一个没有状态的渲染函数,<script>
部分被简化为props
、injections
和slots
。将有四个props
对象:backgroundColor
、textColor
、isRound
和isFlat
。在安装组件时,这些不是必需的,因为我们在props
中定义了默认值:<script>
import isColor from 'is-color';
export default {
name: 'MaterialButton',
props: {
backgroundColor: {
type: String,
required: false,
default: '#fff',
validator: v => typeof v === 'string' && isColor(v),
},
textColor: {
type: String,
required: false,
default: '#000',
validator: v => typeof v === 'string' && isColor(v),
},
isRound: {
type: Boolean,
required: false,
default: false,
},
isFlat: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
部分,我们首先需要向<template>
标签添加functional
属性,以指示vue-template-compiler
这个组件是一个功能组件。我们需要创建一个按钮 HTML 元素,带有基本的class
属性按钮和一个基于props
对象接收的动态class
属性。与普通组件不同,我们需要指定props
属性以使用功能组件。对于按钮的样式,我们需要创建一个基于props
的动态style
属性。为了直接将所有事件监听器传递给父组件,我们可以调用v-on
指令并传递listeners
属性。这将绑定所有事件监听器,而无需声明每一个。在按钮内部,我们将添加一个用于视觉增强的div
HTML 元素,并添加<slot>
,文本将放置在其中:<template functional>
<button
tabindex="0"
class="button"
:class="{
round: props.isRound,
isFlat: props.isFlat,
}"
:style="{
background: props.backgroundColor,
color: props.textColor
}"
v-on="listeners"
>
<div
tabindex="-1"
class="button_focus_helper"
/>
<slot/>
</button>
</template>
<style>
部分,我们需要为这个按钮创建所有的 CSS 样式表规则。我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响我们应用程序中的任何其他元素:<style scoped>
.button {
user-select: none;
position: relative;
outline: 0;
border: 0;
border-radius: 0.25rem;
vertical-align: middle;
cursor: pointer;
padding: 4px 16px;
font-size: 14px;
line-height: 1.718em;
text-decoration: none;
color: inherit;
background: transparent;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
min-height: 2.572em;
font-weight: 500;
text-transform: uppercase;
}
.button:not(.isFlat){
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2),
0 2px 2px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
transition: 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button:not(.isFlat):focus:before,
.button:not(.isFlat):active:before,
.button:not(.isFlat):hover:before {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 5px 8px rgba(0, 0, 0, 0.14),
0 1px 14px rgba(0, 0, 0, 0.12);
}
.button_focus_helper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
outline: 0;
opacity: 0;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.4s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button_focus_helper:after, .button_focus_helper:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
border-radius: inherit;
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1),
opacity 0.6s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.button_focus_helper:before {
background: #000;
}
.button_focus_helper:after {
background: #fff;
}
.button:focus .button_focus_helper:before,
.button:hover .button_focus_helper:before {
opacity: .1;
}
.button:focus .button_focus_helper:after,
.button:hover .button_focus_helper:after {
opacity: .6;
}
.button:focus .button_focus_helper,
.button:hover .button_focus_helper {
opacity: 0.2;
}
.round {
border-radius: 50%;
}
</style>
> npm run serve
render()
函数在 Vue 中引入;后来,它们被添加到了vue-template-compiler
中,用于 Vue 单文件应用程序。createElement
和context
。正如我们在单文件中看到的,我们只能访问元素,因为它们不在 JavaScript 对象的this
属性中。这是因为当上下文传递给渲染函数时,就没有this
属性。vuejs.org/v2/guide/render-function.html#Functional-Components
找到有关功能组件的更多信息。www.npmjs.com/package/is-color
找到有关is-color
模块的更多信息。@vue/cli
@vue/cli-service-global
StarRatingInput
、StarRatingDisplay
和StarRating
——最后一部分将涵盖数据和函数访问的父子直接操作。在src/components
文件夹中创建一个名为StarRatingInput.vue
的新文件。
在组件的<script>
部分,在props
属性中创建一个maxRating
属性,它是一个数字,非必需,并且默认值为5
。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
。在methods
属性中,我们需要创建三个方法:updateRating
、emitFinalVoting
和getStarName
。updateRating
方法将保存评分到数据中,emitFinalVoting
将调用updateRating
并通过final-vote
事件将评分传递给父组件,getStarName
将接收一个值并返回星级的图标名称。
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
<template>
部分,我们需要创建一个<slot>
组件来放置星级评分之前的文本。我们将根据通过props
属性接收到的maxRating
值创建一个动态星级列表。创建的每个星级都将在mouseenter
、focus
和click
事件上附加一个监听器。当触发mouseenter
和focus
时,将调用updateRating
方法,而click
将调用emitFinalVote
方法。<template>
<div class="starRating">
<span class="rateThis">
<slot />
</span>
<ul>
<li
v-for="rate in maxRating"
:key="rate"
@mouseenter="updateRating(rate)"
@click="emitFinalVote(rate)"
@focus="updateRating(rate)"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
</div>
</template>
styles
文件夹中创建一个名为materialIcons.css
的新样式文件,并添加font-family
的 CSS 样式规则。@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ-
Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons' !important;
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
main.js
文件,并将创建的样式表导入其中。css-loader
将处理 JavaScript 文件中导入的.css
文件的处理。这将有助于开发,因为您不需要在其他地方重新导入文件。import Vue from 'vue';
import App from './App.vue';
import './style/materialIcons.css';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
src/style
文件夹中创建一个名为starRating.css
的通用样式文件。在那里,我们将添加StarRatingDisplay
和StarRatingInput
组件之间共享的通用样式。.starRating {
user-select: none;
display: flex;
flex-direction: row;
}
.starRating * {
line-height: 0.9rem;
}
.starRating .material-icons {
font-size: .9rem !important;
color: orange;
}
ul {
display: inline-block;
padding: 0;
margin: 0;
}
ul > li {
list-style: none;
float: left;
}
<style>
部分,我们需要创建所有的 CSS 样式表规则。然后,在位于src/components
文件夹中的StarRatingInput.vue
组件文件上,我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响应用程序中的任何其他元素。在这里,我们将导入我们创建的通用样式,并为输入添加新样式:<style scoped>
@import '../style/starRating.css';
.starRating {
justify-content: space-between;
}
.starRating * {
line-height: 1.7rem;
}
.starRating .material-icons {
font-size: 1.6rem !important;
}
.rateThis {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: 1rem;
}
</style>
> npm run serve
StarRatingDisplay
组件:在src/components
文件夹中创建一个名为StarRatingDisplay.vue
的新组件。
在组件的<script>
部分,在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。它们三个都将是数字,非必需的,并且有默认值。在methods
属性中,我们需要创建一个名为getStarName
的新方法,它将接收一个值并返回星星的图标名称:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
<template>
中,我们需要根据通过props
属性接收到的maxRating
值创建一个动态星星列表。在列表之后,我们需要显示我们收到的投票数,如果我们收到任何投票,我们也会显示它们:<template>
<div class="starRating">
<ul>
<li
v-for="rate in maxRating"
:key="rate"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
<span class="rating">
{{ rating }}
</span>
<span
v-if="votes"
class="votes"
>
({{ votes }})
</span>
</div>
</template>
<style>
部分,我们需要创建所有的 CSS 样式表规则。我们需要向<style>
添加scoped
属性,以便所有的 CSS 样式表规则不会影响应用程序中的任何其他元素。在这里,我们将导入我们创建的通用样式,并为显示添加新样式:<style scoped>
@import '../style/starRating.css';
.rating, .votes {
display: inline-block;
color: rgba(0,0,0, .65);
font-size: .75rem;
margin-left: .4rem;
}
</style>
> npm run serve
StarRating
组件:在src/components
文件夹中创建一个名为StarRating.vue
的新文件。
在组件的<script>
部分,我们需要导入StarRatingDisplay
和StarRatingInput
组件。在props
属性中,我们需要创建三个新属性:maxRating
,rating
和votes
。它们三个都将是数字,非必需的,并且有一个默认值。在data
属性中,我们需要创建我们的rating
属性,其默认值为0
,并且一个名为voted
的属性,其默认值为false
。在methods
属性中,我们需要添加一个名为vote
的新方法,它将接收rank
作为参数。它将把rating
定义为接收到的值,并将voted
组件的内部变量定义为true
:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
<template>
部分,我们将放置两个组件,显示评分的输入:<template>
<div>
<StarRatingInput
v-if="!voted"
:max-rating="maxRating"
@final-vote="vote"
>
Rate this Place
</StarRatingInput>
<StarRatingDisplay
v-else
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
/>
</div>
</template>
在App.vue
文件中,在组件的<template>
部分,删除MaterialCardBox
组件的main-text
属性,并将其放置为组件的默认插槽。
在放置的文本之前,我们将添加StarRating
组件。我们将为其添加一个ref
属性。此属性将指示 Vue 将此组件直接链接到组件的this
对象中的一个特殊属性。在操作按钮中,我们将为点击事件添加监听器——一个用于resetVote
,另一个用于forceVote
。
<template>
<div id="app">
<MaterialCardBox
header="Material Card Header"
sub-header="Card Sub Header"
show-media
show-actions
img-src="https://picsum.photos/300/200"
>
<p>
<StarRating
ref="starRating"
/>
</p>
<p>
The path of the righteous man is beset on all sides by the
iniquities of the selfish and the tyranny of evil men.
</p>
<template v-slot:action>
<MaterialButton
background-color="#027be3"
text-color="#fff"
@click="resetVote"
>
Reset
</MaterialButton>
<MaterialButton
background-color="#26a69a"
text-color="#fff"
is-flat
@click="forceVote"
>
Rate 5 Stars
</MaterialButton>
</template>
</MaterialCardBox>
</div>
</template>
<script>
部分,我们将创建一个methods
属性,并添加两个新方法:resetVote
和forceVote
。这些方法将访问StarRating
组件并重置数据或将数据设置为 5 星投票:<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
import StarRating from './components/StarRating.vue';
export default {
name: 'App',
components: {
StarRating,
MaterialButton,
MaterialCardBox,
},
methods: {
resetVote() {
this.$refs.starRating.rank = 0;
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.rank = 5;
this.$refs.starRating.voted = true;
},
},
};
</script>
ref
属性添加到组件时,Vue 会将对所引用元素的链接添加到 JavaScript 的this
属性对象内的$refs
属性中。从那里,您可以完全访问组件。this
对象上调用$parent
来访问父组件。事件可以通过调用$root
属性来访问 Vue 应用程序的根元素。vuejs.org/v2/guide/components-edge-cases.html#Accessing-the-Parent-Component-Instance
找到有关父子通信的更多信息。v-if
、v-else-if
和v-else
指令的情况下即时更改组件。@vue/cli
@vue/cli-service-global
打开StarRating.vue
组件。
在组件的<script>
部分,我们需要创建一个带有名为starComponent
的新计算值的computed
属性。此值将检查用户是否已投票。如果他们没有,它将返回StarRatingInput
组件;否则,它将返回StarRatingDisplay
组件:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
<template>
部分,我们将删除现有组件,并用一个名为<component>
的特殊组件替换它们。这个特殊组件有一个命名属性,您可以指向任何返回有效 Vue 组件的地方。在我们的例子中,我们将指向计算属性starComponent
。我们将把从这两个组件中定义的所有绑定 props 放在这个新组件中,包括放在<slot>
中的文本:<template>
<component
:is="starComponent"
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
@final-vote="vote"
>
Rate this Place
</component>
</template>
<component>
组件,我们声明了根据计算属性设置的规则应该呈现什么组件。v-bind
指令与需要定义的 props 和规则,但也可以直接在组件上定义,因为它将作为 prop 传递下去。vuejs.org/v2/guide/components.html#Dynamic-Components
找到有关动态组件的更多信息。@vue/cli
@vue/cli-service-global
打开StarRating.vue
组件。
在组件的<script>
部分,添加一个名为provide
的新属性。在我们的情况下,我们将只添加一个键值来检查组件是否是特定组件的子级。在属性中创建一个对象,其中包含starRating
键和true
值:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
provide: {
starRating: true,
},
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
打开StarRatingDisplay.vue
文件。
在组件的<script>
部分,我们将添加一个名为inject
的新属性。此属性将接收一个名为starRating
的键的对象,值将是一个具有default()
函数的对象。如果此组件不是StarRating
组件的子级,则此函数将记录错误:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
打开StarRatingInput.vue
文件。
在组件的<script>
部分,我们将添加一个名为inject
的新属性。此属性将接收一个名为starRating
的键的对象,值将是一个具有default()
函数的对象。如果此组件不是StarRating
组件的子级,则此函数将记录错误:
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
inject: {
starRating: {
default() {
console.error('StarRatingInput need to be a child of
StarRating');
},
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
StarRatingDisplay
和StarRatingInput
组件中的starRating
的注入属性,如果父组件未提供此值,则将在控制台上记录错误。vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
找到有关组件依赖注入的更多信息。mixin
,这是 Vue 中的一个特殊代码导入,它将外部代码部分连接到当前组件。@vue/cli
@vue/cli-service-global
mixin
:打开StarRating.vue
组件。
在<script>
部分,我们需要将props
属性提取到一个名为starRatingDisplay.js
的新文件中,我们需要在mixins
文件夹中创建这个新文件。这个新文件将是我们的第一个mixin
,并且看起来像这样:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
};
StarRating.vue
组件,我们需要导入这个新创建的文件,并将其添加到一个名为mixin
的新属性中:<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
mixins: [StarRatingDisplayMixin],
provide: {
starRating: true,
},
data: () => ({
rank: 0,
voted: false,
}),
computed: {
starComponent() {
if (!this.voted) return StarRatingInput;
return StarRatingDisplay;
},
},
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
现在,我们将打开StarRatingDisplay.vue
文件。
在<script>
部分,我们将inject
属性提取到一个名为starRatingChild.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们inject
属性的mixin
:
export default {
inject: {
starRating: {
default() {
console.error('StarRatingDisplay need to be a child of
StarRating');
},
},
},
};
StarRatingDisplay.vue
文件中,在<script>
部分,我们将提取methods
属性到一个名为starRatingName.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们getStarName
方法的mixin
:export default {
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
StarRatingDisplay.vue
文件,我们需要导入这些新创建的文件,并将它们添加到一个名为mixin
的新属性中:<script>
import StarRatingDisplayMixin from '../mixins/starRatingDisplay';
import StarRatingNameMixin from '../mixins/starRatingName';
import StarRatingChildMixin from '../mixins/starRatingChild';
export default {
name: 'StarRatingDisplay',
mixins: [
StarRatingDisplayMixin,
StarRatingNameMixin,
StarRatingChildMixin,
],
};
</script>
打开StarRatingInput.vue
文件。
在<script>
部分,我们移除inject
属性,并将props
属性提取到一个名为starRatingBase.js
的新文件中,该文件将被创建在mixins
文件夹中。这将是我们props
属性的mixin
:
export default {
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
},
};
StarRatingInput.vue
文件,我们需要将rating
数据属性重命名为rank
,并且在getStarName
方法中,我们需要添加一个新的常量,该常量将接收rating
属性或rank
数据。最后,我们需要导入starRatingChild
mixin
和starRatingBase
mixin
:<script>
import StarRatingBaseMixin from '../mixins/starRatingBase';
import StarRatingChildMixin from '../mixins/starRatingChild';
export default {
name: 'StarRatingInput',
mixins: [
StarRatingBaseMixin,
StarRatingChildMixin,
],
data: () => ({
rank: 0,
}),
methods: {
updateRating(value) {
this.rank = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rank);
},
getStarName(rate) {
const rating = (this.rating || this.rank);
if (rate <= rating) {
return 'star';
}
if (Math.fround((rate - rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
mixins
的工作原理就像对象合并一样,但确保不要用导入的属性替换组件中已经存在的属性。mixins
属性的顺序也很重要,因为它们将被检查并作为for
循环导入,所以最后一个mixin
不会改变任何祖先的属性。vuejs.org/v2/guide/mixins.html
找到有关 mixins 的更多信息。webpack
和 Vue 天生就是一对。当使用webpack
作为 Vue 项目的打包工具时,可以使组件在需要时或异步加载。这通常被称为惰性加载。@vue/cli
@vue/cli-service-global
打开App.vue
文件。
在组件的<script>
部分,我们将在脚本顶部获取导入并将它们转换为每个组件的惰性加载函数:
<script>
export default {
name: 'App',
components: {
StarRating: () => import('./components/StarRating.vue'),
MaterialButton: () => import('./components/MaterialButton.vue'),
MaterialCardBox: () =>
import('./components/MaterialCardBox.vue'),
},
methods: {
resetVote() {
this.$refs.starRating.rank = 0;
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.rank = 5;
this.$refs.starRating.voted = true;
},
},
};
</script>
import()
函数的函数时,webpack
知道这个导入函数将进行代码拆分,并且它将使组件成为捆绑包中的一个新文件。import()
函数是由 TC39 提出的一个模块加载语法的建议。这个函数的基本功能是异步加载任何声明为模块的文件,避免了在第一次加载时放置所有文件的需要。vuejs.org/v2/guide/components-dynamic-async.html#Async-Components
找到有关异步组件的更多信息。github.com/tc39/proposal-dynamic-import
找到有关 TC39 动态导入的更多信息。axios
来构建自己的 API 数据操作。创建一个 Fetch API 的 HTTP 客户端包装器
创建一个随机猫图片或 GIF 组件
使用MirageJS
创建本地虚拟 JSON API 服务器
使用axios
作为新的 HTTP 客户端
创建不同的axios
实例
为axios
创建请求和响应拦截器
使用axios
和Vuesax
创建 CRUD 接口
windows-build-tools
的 NPM 包,以便能够安装以下所需的包。要做到这一点,以管理员身份打开 PowerShell 并执行以下命令:> npm install -g windows-build-tools
> npm install -g @vue/cli @vue/cli-service-global
XMLHttpRequest
的子代。它有一个改进的 API 和一个基于Promises
的新而强大的功能集。Request
和Response
的通用定义,使其可以在浏览器中的任何地方使用。浏览器的 Fetch API 也可以在window
或service worker
中执行。对于这个 API 的使用没有限制。@vue/cli
@vue/cli-service-global
> vue create http-project
default
选项:? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
在src/http
文件夹中创建一个名为baseFetch.js
的新文件。
我们将创建一个异步函数,它将作为参数接收url
,method
和options
的三个变量。这将是一个柯里化函数,第二个函数将接收type
作为参数:
export default async (url, method, options = {}) => {
let httpRequest;
if (method.toUpperCase() === 'GET') {
httpRequest = await fetch(url, {
cache: 'reload',
...options,
});
} else {
httpRequest = fetch(url, {
method: method.toUpperCase(),
cache: 'reload',
...options,
});
}
return (type) => {
switch (type.toLocaleLowerCase()) {
case 'json':
return httpRequest.json();
case 'blob':
return httpRequest.blob();
case 'text':
return httpRequest.text();
case 'formdata':
return httpRequest.formData();
default:
return httpRequest.arrayBuffer();
}
}; };
在src/http
文件夹中创建一个名为fetchApi.js
的新文件。
我们需要从我们在第一步创建的文件中导入baseHttp
:
import baseHttp from './baseFetch';
getHttp
函数:创建一个名为getHttp
的常量。
定义一个常量作为一个异步函数,接收三个参数,url
,type
和options
。type
参数将默认值为'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
,'get'
作为第二个参数,options
作为第三个参数,并立即执行带有我们收到的type
参数的函数:
export const getHttp = async (url, type = 'json', options) => (await
baseHttp(url, 'get', options))(type);
postHttp
函数:创建一个名为postHttp
的常量。
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
参数和'post'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
参数执行返回的函数。body
通常是 JSON 或 JavaScript 对象。如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const postHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'post',
{
body,
...options,
}))(type);
putHttp
函数:创建一个名为putHttp
的常量。
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'put'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
参数执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const putHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'put',
{
body,
...options,
}))(type);
patchHttp
函数:创建一个名为patchHttp
的常量。
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'patch'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const patchHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'patch',
{
body,
...options,
}))(type);
updateHttp
函数:创建一个名为updateHttp
的常量。
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。type
参数将具有默认值'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'update'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const updateHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'update',
{
body,
...options,
}))(type);
deleteHttp
函数:创建一个名为deleteHttp
的常量。
将一个异步函数分配给该常量,该函数接收四个参数,url
、body
、type
和options
。类型参数将具有默认值'json'
。
在这个函数返回中,我们将执行baseHttp
函数,传递我们收到的url
和'delete'
作为第二个参数。在第三个参数中,我们将传递一个带有body
变量和我们收到的解构options
参数的对象。由于baseHttp
的柯里化属性,我们将使用收到的type
执行返回的函数。body
通常是 JSON 或 JavaScript 对象,但如果这个请求将是文件上传,body
需要是一个FormData
对象:
export const deleteHttp = async (
url,
body,
type = 'json',
options,
) => (await baseHttp(url,
'delete',
{
body,
...options,
}))(type);
window
元素上呈现的Fetch
API 创建了一个包装器。这个包装器由一个柯里化和闭包函数组成,第一个函数接收 Fetch API 的 URL 数据、方法和选项,而结果函数是 Fetch API 的响应转换器。fetch
请求。在那里,我们需要检查它是否是GET方法,所以我们只需要用url
参数执行它并省略其他参数。函数的第二部分负责将fetch
响应转换。它将在type
参数之间切换,并根据正确的参数执行检索函数。getHttp('https://jsonplaceholder.typicode.com/todos/1', 'json').then((response) => { console.log(response)); }
body
,但所有其他动词都能够在请求中传递body
。developer.mozilla.org/en-US/docs/Web/API/Fetch_API
找到有关 Fetch API 的更多信息。developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
找到有关 FormData 的更多信息。developer.mozilla.org/en-US/docs/Web/API/Body/body
找到有关 Fetch 响应主体的更多信息。 developer.mozilla.org/en-US/docs/Web/API/Headers
找到有关标头的更多信息。developer.mozilla.org/
en-US/docs/Web/API/Request找到有关请求的更多信息。@vue/cli
@vue/cli-service-global
> vue create http-project
default
选项:? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
<script>
、<template>
和<style>
。<script>
部分<script>
部分:在src/components
文件夹中创建一个名为RandomCat.vue
的新文件并打开它。
从我们在'将 Fetch API 包装为 HTTP 客户端创建包装器'教程中制作的fetchApi
包装器中导入getHttp
函数:
import { getHttp } from '../http/fetchApi';
component
属性中异步导入MaterialButton
和MaterialCardBox
组件:components: {
MaterialButton: () => import('./MaterialButton.vue'),
MaterialCardBox: () => import('./MaterialCardBox.vue'), },
data
属性中,我们需要创建一个名为kittyImage
的新数据值,默认为空字符串:data: () => ({
kittyImage: '', }),
methods
属性中,我们需要创建getImage
方法,它将以Blob
的形式获取图片,并将其作为URL.createObjectURL
返回。我们还需要创建newCatImage
方法,它将获取一张新的猫的静态图片,以及newCatGif
方法,它将获取一个新的猫的 GIF:methods: {
async getImage(url) {
return URL.createObjectURL(await getHttp(url, 'blob'));
},
async newCatImage() {
this.kittyImage = await this.getImage('https://cataas.com/cat');
},
async newCatGif() {
this.kittyImage = await
this.getImage('https://cataas.com/cat/gif');
},
},
beforeMount
生命周期钩子中,我们需要将其设置为异步,并执行newCatImage
方法:async beforeMount() {
await this.newCatImage();
},
<template>
部分<template>
部分:MaterialCardBox
组件,激活media
和action
部分,并为media
和action
创建<template>
命名插槽:<MaterialCardBox
header="Cat as a Service"
sub-header="Random Cat Image"
show-media
show-actions >
<template
v-slot:media> </template>
<template v-slot:action> </template> </MaterialCardBox>
<template>
中名为media
的插槽中,我们需要添加一个<img>
元素,它将接收一个 URIBlob
,当kittyImage
变量中有任何数据时,它将显示出来,否则将显示一个加载图标:<img
v-if="kittyImage"
alt="Meow!"
:src="kittyImage"
style="width: 300px;" >
<p v-else style="text-align: center">
<i class="material-icons">
cached
</i>
</p>
<template>
中名为action
的插槽中,我们将创建两个按钮,一个用于获取猫的图片,另一个用于获取猫的 GIF,两者都将在@click
指令上有一个事件监听器,调用一个函数来获取相应的图片:<MaterialButton
background-color="#4ba3c7"
text-color="#fff"
@click="newCatImage" >
<i class="material-icons">
pets
</i> Cat Image
</MaterialButton> <MaterialButton
background-color="#005b9f"
text-color="#fff"
@click="newCatGif" >
<i class="material-icons">
pets
</i> Cat GIF
</MaterialButton>
<style>
部分<style>
部分中,我们需要设置body font-size
以便基于rem
和em
进行 CSS 样式计算:<style>
body {
font-size: 14px;
} </style>
在src
文件夹中的App.vue
文件中打开。
在components
属性中,异步导入RandomCat.vue
组件:
<script> export default { name: 'App',
components: {
RandomCat: () => import('./components/RandomCat'),
}, }; </script>
<template>
部分中,声明导入的组件:<template>
<div id="app">
<random-cat />
</div> </template>
> npm run serve
getHttp
包装器,组件能够获取 URL 并将其作为Blob
类型检索出来。有了这个响应,我们可以使用URL.createObjectUrl
导航方法,并将Blob
作为参数传递,以获取一个有效的图像 URL,该 URL 可以用作src
属性。developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
找到有关URL.createObjectUrl
的更多信息。developer.mozilla.org/en-US/docs/Web/API/Body/blob
找到有关Blob
响应类型的更多信息。@vue/cli
@vue/cli-service-global
> vue create visual-component
default
选项:? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
fetchApi
包装器的getHttp
函数。MirageJS
模拟服务器:MirageJS
服务器安装到您的软件包中。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:> npm install --save miragejs
MirageJS
的任何更改,因为目前还没有当前的 LTS 版本。MirageJS
数据库,用于存储临时数据。按照以下步骤创建它:在src/server
文件夹中创建一个名为db.js
的新文件,用于初始加载的数据。
我们需要为这个文件创建一个 JavaScript 对象作为默认导出,其中包含我们希望服务器具有的初始数据:
export default {
users: [
{ name: 'Heitor Ramon Ribeiro',
email: 'heitor@example.com',
age: 31,
country: 'Brazil',
active: true,
},
], };
MirageJS
服务器模拟的HTTP GET方法。按照以下步骤创建它:对于GET方法,我们需要在src/server
文件夹中创建一个名为get.js
的新文件。
对于这个配方,我们将创建一个通用的getFrom
函数,该函数接收一个键作为参数并返回一个函数。这个返回的函数返回一个直接指向本地数据库的指定键:
export const getFrom = key => ({ db }) => db[key]; export default {
getFrom, };
MirageJS
服务器模拟。按照以下步骤创建它:对于POST方法,我们需要在src/server
文件夹中创建一个名为post.js
的新文件。
对于这个配方,我们将创建一个通用的postFrom
函数,该函数接收一个键作为参数并返回一个函数。这个返回的函数将解析 HTTP 请求体的data
属性,并返回服务器模式的内部函数,将数据插入数据库。使用key
参数,模式知道我们正在处理哪个表:
export const postFrom = key => (schema, request) => {
const { data } = typeof request.requestBody === 'string'
? JSON.parse(request.requestBody)
: request.requestBody; return schema.db[key].insert(data); }; export default {
postFrom, };
MirageJS
服务器模拟的HTTP PATCH方法。按照以下步骤创建它:对于PATCH方法,我们需要在src/server
文件夹中创建一个名为patch.js
的新文件。
对于这个配方,我们将制作一个通用的patchFrom
函数,该函数接收一个键作为参数并返回一个函数。返回的函数将解析 HTTP 请求体的data
属性,并返回一个服务器模式的内部函数,该函数更新具有与数据一起传递的id
属性的特定对象。使用key
参数,模式知道我们正在处理哪个表:
export const patchFrom = key => (schema, request) => {
const { data } = typeof request.requestBody === 'string'
? JSON.parse(request.requestBody)
: request.requestBody; return schema.db[key].update(data.id, data); }; export default {
patchFrom, };
MirageJS
服务器模拟的HTTP DELETE方法。按照以下步骤创建它:对于DELETE方法,我们需要在src/server
文件夹中创建一个名为delete.js
的新文件。
对于这个配方,我们将制作一个通用的patchFrom
函数,该函数接收一个键作为参数并返回一个函数。返回的函数将解析 HTTP 请求体的data
属性,并返回一个服务器模式的内部函数,该函数删除具有通过路由REST参数传递给服务器的id
属性的特定对象。使用key
参数,模式知道我们正在处理哪个表:
export const deleteFrom = key => (schema, request) =>
schema.db[key].remove(request.params.id); export default {
deleteFrom, };
MirageJS
服务器和可用的路由。按照以下步骤创建服务器:在src/server
文件夹中创建一个名为server.js
的新文件。
接下来,我们需要导入Server
类,baseData
和路由方法:
import { Server } from 'miragejs'; import baseData from './db'; import { getFrom } from './get'; import { postFrom } from './post'; import { patchFrom } from './patch'; import { deleteFrom } from './delete';
server
,并将此变量设置为Server
类的新执行:window.server = new Server({});
Server
类的构造选项中,添加一个名为seeds
的新属性。此属性是一个接收服务器(srv
)作为参数并执行srv.db.loadData
函数传递baseDate
作为参数的函数:seeds(srv) {
srv.db.loadData({ ...baseData }); },
routes
的新属性中,它将创建模拟服务器路由。这个属性是一个函数,在函数体内,我们需要设置模拟服务器的namespace
和服务器响应的毫秒延迟。将有四个路由。对于创建路由,我们将创建一个名为/users
的新路由,监听POST方法。对于读取路由,我们将创建一个名为/users
的新路由,监听GET方法。对于更新路由,我们将创建一个名为/users/:id
的新路由,监听PATCH方法,最后,对于删除路由,我们将创建一个名为/users
的新路由,监听DELETE方法:routes() {
this.namespace = 'api'; this.timing = 750; this.get('/users', getFrom('users')); this.post('/users', postFrom('users')); this.patch('/users/:id', patchFrom('users')); this.delete('/users/:id', deleteFrom('users'));
},
MirageJS
服务器添加到 Vue 应用程序中。按照以下步骤使服务器对您的 Vue 应用程序可用:在src
文件夹中打开main.js
文件。
我们需要将服务器声明为第一个导入声明,这样它就可以在应用程序的初始加载时可用:
import './server/server'; import Vue from 'vue'; import App from './App.vue'; Vue.config.productionTip = false; new Vue({
render: h => h(App), }).$mount('#app');
<script>
部分<script>
部分。按照以下步骤创建它:在src
文件夹中打开App.vue
文件。
从我们在'将 Fetch API 包装为 HTTP 客户端的创建包装器'中制作的fetchHttp
包装器中导入getHttp
,postHttp
,patchHttp
和deleteHTTP
方法:
import {
getHttp,
postHttp,
patchHttp,
deleteHttp, } from './http/fetchApi';
data
属性中,我们需要创建三个新属性来使用,response
,userData
和userId
:data: () => ({
response: undefined,
userData: '',
userId: undefined,
}),
methods
属性中,我们需要创建四个新方法,getAllUsers
,createUser
,updateUser
和deleteUser
:methods: {
async getAllUsers() {
},
async createUser() {
},
async updateUser() {
},
async deleteUser() {
}, },
getAllUsers
方法中,我们将设置响应数据属性为api/users
路由的getHttp
函数的结果:async getAllUsers() {
this.response = await getHttp(`${window.location.href}api/users`); },
createUser
方法中,我们将接收一个data
参数,这将是一个对象,我们将把它传递给api/users
路由上的postHttp
,然后执行getAllUsers
方法:async createUser(data) {
await postHttp(`${window.location.href}api/users`, { data });
await this.getAllUsers(); },
updateUser
方法,我们将接收一个data
参数,这将是一个对象,我们将其传递给patchHttp
,在api/users/:id
路由上使用对象上的id
属性作为路由上的:id
。之后,我们将执行getAllUsers
方法:async updateUser(data) {
await patchHttp(`${window.location.href}api/users/${data.id}`,
{ data });
await this.getAllUsers(); },
deleteUser
方法中,我们接收用户id
作为参数,该参数是一个数字,然后我们将其传递给deleteHttp
,在api/users/:id
路由上使用 ID 作为:id
。之后,我们执行getAllUsers
方法:async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`, {}, 'text');
await this.getAllUsers(); },
在这部分,我们将创建单文件组件的<template>
部分。按照以下步骤创建它:
- 在模板的顶部,我们需要添加
response
属性,包裹在一个<pre>
HTML 元素中:
<h3>Response</h3> <pre>{{ response }}</pre>
- 对于用户的创建和更新,我们需要创建一个带有
v-model
指令绑定到userData
属性的textarea
HTML 输入:
<hr/> <h1> Create / Update User </h1> <label for="userData">
User JSON:
<textarea
id="userData"
v-model="userData"
rows="10"
cols="40"
style="display: block;"
></textarea> </label>
- 要发送这些数据,我们需要创建两个按钮,两者都在单击事件上绑定了事件侦听器,使用
@click
指令分别指向createUser
和updateUser
,并在执行时传递userData
:
<button
style="margin: 20px;"
@click="createUser(JSON.parse(userData))" >
Create User
</button> <button
style="margin: 20px;"
@click="updateUser(JSON.parse(userData))" >
Update User
</button>
- 要执行DELETE方法,我们需要创建一个类型为
number
的输入 HTML 元素,并将v-model
指令绑定到userId
属性:
<h1> Delete User </h1> <label for="userData">
User Id:
<input type="number" step="1" v-model="userId"> </label>
- 最后,要执行此操作,我们需要创建一个按钮,该按钮将在单击事件上绑定一个事件侦听器,使用
@click
指令,将其指向deleteUser
方法,并在执行时传递userId
属性:
<button
style="margin: 20px;"
@click="deleteUser(userId)" >
Delete User
</button>
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows),并执行以下命令:
> npm run serve
这是您的组件呈现并运行的方式:
它是如何工作的...
MirageJS
的工作原理类似于拦截应用程序上发生的每个 HTTP 请求的拦截器。服务器拦截浏览器上的所有XHR (XMLHttpRequest)执行,并检查路由,以查看它是否与服务器创建的任何一个路由匹配。如果匹配,服务器将根据相应的路由执行函数。
作为具有基本 CRUD 功能的简单 REST 服务器,服务器具有类似模式的数据库结构,有助于创建用于存储数据的虚拟数据库。
另请参阅
您可以在github.com/miragejs/miragejs
找到有关 MirageJS 的更多信息。
使用 axios 作为新的 HTTP 客户端
当您需要一个用于 HTTP 请求的库时,毫无疑问axios
是您应该选择的。这个库被超过 150 万个开源项目和无数个闭源项目使用,是 HTTP 库之王。
它构建成适用于大多数浏览器,并提供了最完整的选项集之一-您可以自定义请求中的一切。
在这个食谱中,我们将学习如何将我们的 Fetch API 包装器更改为axios
并开始围绕它工作。
准备就绪
这个食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用在'使用 MirageJS 创建您的虚拟 JSON API 服务器'食谱中制作的 Vue 项目,或者我们可以开始一个新的项目。
要开始一个新的项目,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 会询问一些问题,这些问题将有助于创建项目。您可以使用箭头键进行导航,Enter键继续,Spacebar键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
从 Fetch API 更改为 Axios
在接下来的步骤中,我们将为 HTTP 包装器中使用的 Fetch API 更改为axios
库。按照以下步骤正确更改它:
- 在您的包中安装
axios
。您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save axios
此食谱中使用的版本是 0.19.0。注意axios
的更改,因为该库尚无 LTS 版本。
-
打开
src/http
文件夹中的baseFetch.js
文件。 -
简化该方法,使其接收三个参数,
url
、method
和options
,并返回一个axios
方法,使用传递给实例构造函数的方法调用 HTTP 请求:
import axios from 'axios';
export default async (url, method, options = {}) => axios({
method: method.toUpperCase(),
url,
...options,
});
更改 GET 方法函数
在这部分中,我们正在更改HTTP GET方法。按照以下说明更改getHttp
函数:
-
打开
src/http
文件夹中的fetchApi.js
文件。 -
在
getHttp
函数中,我们将添加一个新的参数 param,并删除柯里化函数:
export const getHttp = async (
url,
params,
options, ) => baseHttp(url,
'get',
{
...options,
params,
});
更改 POST 方法函数
在这部分中,我们正在更改HTTP POST方法。按照以下说明更改postHttp
函数:
-
打开
http
文件夹中的fetchApi.js
文件。 -
在
postHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const postHttp = async (
url,
data,
options, ) => baseHttp(url,
'post',
{
data,
...options,
});
更改 PUT 方法函数
在这部分,我们正在更改HTTP PUT方法。按照以下说明更改putHttp
函数:
-
打开
http
文件夹内的fetchApi.js
文件。 -
在
putHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const putHttp = async (
url,
data,
options, ) => baseHttp(url,
'put',
{
data,
...options,
});
更改 PATCH 方法函数
在这部分,我们正在更改HTTP PATCH方法。按照以下说明更改patchHttp
函数:
-
打开
http
文件夹内的fetchApi.js
文件。 -
在
patchHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const patchHttp = async (
url,
data,
options, ) => baseHttp(url,
'patch',
{
data,
...options,
});
更改 UPDATE 方法函数
在这部分,我们正在更改HTTP UPDATE方法。按照以下说明更改updateHttp
函数:
-
打开
http
文件夹内的fetchApi.js
文件。 -
在
updateHttp
函数中,我们将添加一个新的参数 param,并删除柯里化函数:
export const updateHttp = async (
url,
data,
options, ) => baseHttp(url,
'update',
{
data,
...options,
});
更改 DELETE 方法函数
在这部分,我们正在更改HTTP DELETE方法。按照以下说明更改deleteHttp
函数:
-
打开
http
文件夹内的fetchApi.js
文件。 -
在
deleteHttp
函数中,我们将把body
参数改为data
,并删除柯里化函数:
export const deleteHttp = async (
url,
data,
options, ) => baseHttp(url,
'delete',
{
data,
...options,
});
更改组件
在这部分,我们将改变组件与新函数的工作方式。按照以下说明正确更改它:
-
打开
src
文件夹内的App.vue
文件。 -
在
getAllUsers
方法中,我们需要更改响应的定义方式,因为axios
给我们提供了一个完全不同的响应对象,而不是 Fetch API:
async getAllUsers() {
const { data } = await getHttp(`${window.location.href}api/users`);
this.response = data;
},
- 在
deleteUser
方法中,我们可以直接将 URL 作为参数传递:
async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`);
await this.getAllUsers();
},
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
它是如何工作的...
当我们为 Fetch API 创建包装器时,我们使用了一种将 API 抽象成另一个接口的技术,这使得从 Fetch API 更改到axios
库成为可能。通过这样做,我们能够改进方法并简化函数的调用和处理方式。例如,GET 方法现在可以接收一个名为params的新参数,这些参数是 URL 查询参数的对象,将自动注入到 URL 中。
我们还必须更改响应的解释方式,因为axios
比 Fetch API 具有更健壮和完整的响应对象,后者只返回获取的响应本身。
另请参阅
您可以在github.com/axios/axios
找到有关axios
的更多信息。
创建不同的 axios 实例
使用axios
时,您可以运行多个实例,而它们互不干扰。例如,您可以有一个指向版本 1 的用户 API 的实例,另一个指向版本 2 的支付 API,两者共享相同的命名空间。
在这里,我们将学习如何创建各种axios
实例,因此您可以在不受问题或干扰的情况下使用尽可能多的 API 命名空间。
准备工作
此食谱的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
操作步骤...
要启动我们的组件,我们可以使用在“使用 axios 作为新的 HTTP 客户端”食谱中使用的 Vue CLI 创建的 Vue 项目,或者我们可以启动一个新的项目。
要启动一个新的实例,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键进行导航,Enter键继续,Spacebar键选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
更改 HTTP 函数
创建多个axios
实例时,调用axios
库的过程会发生变化。因此,我们需要更改 HTTP 包装器实例化axios
库的方式。
在接下来的部分中,我们将改变 HTTP 包装器与创建新的axios
实例的工作方式,并使其可用于应用程序。
更改 HTTP Fetch 包装器
在接下来的步骤中,我们将创建一个新的自定义axios
实例,该实例将用于 HTTP 包装器。按照以下说明将新实例添加到应用程序中:
-
在
src/http
文件夹中打开baseFetch.js
文件。 -
我们需要创建一个名为
createAxios
的新工厂函数,以便每次执行时生成一个新的axios
实例:
export function createAxios(options = {}) {
return axios.create({
...options,
}); }
- 现在,我们需要创建
localApi
常量,其值将是createAxios
工厂的执行结果:
const localApi = createAxios();
- 对于
JSONPlaceHolder
,我们将创建一个名为jsonPlaceholderApi
的常量,该常量将被导出,其值将是createAxios
工厂的执行。我们还将传递一个对象作为参数,其中包含定义的baseURL
属性:
export const jsonPlaceholderApi = createAxios({
baseURL: 'https://jsonplaceholder.typicode.com/', });
- 在
export default
函数中,我们需要从axios
更改为localApi
:
export default async (url, method, options = {}) => localApi({
method: method.toUpperCase(),
url,
...options, });
更改 HTTP 方法函数
在这部分,我们将改变 HTTP 方法如何与新的axios
实例一起工作。按照说明正确执行:
-
在
src/http
文件夹中打开fetchApi.js
文件。 -
我们将从
baseFetch
导入jsonPlaceholderApi
函数作为额外导入的值:
import baseHttp, { jsonPlaceholderApi } from './baseFetch';
- 我们需要创建一个名为
getTodos
的新常量,该常量将被导出。此常量将是一个函数,将接收userId
作为参数,并返回axios
的 GET 函数,其中userId
参数将在名为params
的属性的配置对象中接收:
export const getTodos = async userId => jsonPlaceholderApi.get('todos',
{
params: {
userId,
},
});
更改 MirageJS 服务器
在这部分,我们将更改MirageJS
服务器如何与新创建的axios
实例一起工作。按照说明正确执行:
-
在
src/server
文件夹中打开server.js
文件。 -
在构造函数对象的
routes
属性上,我们需要添加一个passthrough
声明,这将指示 MirageJS 不会拦截对该 URL 的所有调用:
import { Server } from 'miragejs'; import baseData from './db'; import { getFrom } from './get'; import { postFrom } from './post'; import { patchFrom } from './patch'; import { deleteFrom } from './delete'; window.server = new Server({
seeds(srv) {
srv.db.loadData({ ...baseData });
}, routes() {
this.passthrough();
this.passthrough('https://jsonplaceholder.typicode.com/**'); this.namespace = 'api'; this.timing = 750; this.get('/users', getFrom('users')); this.post('/users', postFrom('users')); this.patch('/users/:id', patchFrom('users')); this.delete('/users/:id', deleteFrom('users'));
}, });
更改组件
在包装函数、MirageJS
服务器方法和 HTTP 方法更改后,我们需要将组件更改为已实现的新库。
在接下来的部分,我们将更改组件以匹配已实现的新库。
单文件组件<script>
部分
在这部分,我们将更改单文件组件的<script>
部分。按照以下步骤执行:
-
在
src
文件夹中打开App.vue
文件。 -
我们需要按以下方式导入新的
getTodos
函数:
import {
getHttp,
postHttp,
patchHttp,
deleteHttp,
getTodos, } from './http/fetchApi';
- 在
Vue
对象的data
属性中,我们需要创建一个名为userTodo
的新属性,其默认值为一个空数组:
data: () => ({
response: undefined,
userData: '',
userId: undefined,
userTodo: [], }),
- 在
methods
属性中,我们需要创建一个名为getUserTodo
的新方法,该方法接收userId
参数。此方法将获取用户的待办事项列表,并将响应属性分配给userTodo
属性:
async getUserTodo(userId) {
this.userTodo = await getTodos(userId); },
单文件组件<template>
部分
在这部分,我们将更改单文件组件的<template>
部分。按照以下步骤执行:
-
在
src
文件夹中打开App.vue
文件。 -
在模板底部,我们需要创建一个新的
input
HTML 元素,使用v-model
指令绑定到userId
属性:
<h1> Get User ToDos </h1> <label for="userData">
User Id:
<input type="number" step="1" v-model="userId"> </label>
- 要获取项目列表,我们需要创建一个按钮,该按钮绑定了点击事件的事件监听器,使用
@click
指令,目标是getUserTodo
,并在执行中传递userId
:
<button
style="margin: 20px;"
@click="getUserTodo(userId)" >
Fetch Data
</button>
要运行服务器并查看您的组件,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm run serve
这是您的组件呈现并运行的地方:
它是如何工作的...
当我们创建axios
的新实例时,会创建一个新对象,并定义新的配置、标头、拦截器和操纵器。这是因为axios
声明create
函数与new Class
相同。它是相同的接口但不同的对象。
利用这个可能性,我们能够创建两个连接驱动程序,一个用于本地 API,另一个用于JSONPlaceHolder
API,它有一个不同的baseURL
。
由于 MirageJS 服务器集成,所有 HTTP 请求都被 MirageJS 拦截,因此我们需要在路由构造函数中添加一个指令,指示 MirageJS 不会拦截的路由。
另请参阅
您可以在jsonplaceholder.typicode.com/
找到有关 JSONPlaceHolder API 的更多信息。
您可以在github.com/axios/axios#creating-an-instance
找到有关axios
实例的更多信息。
您可以在github.com/miragejs/miragejs
找到有关 MirageJS 的更多信息。
为 axios 创建请求和响应拦截器
在我们的应用程序中使用axios
作为主要的 HTTP 操作器,允许我们使用请求和响应拦截器。这些用于在将数据发送到服务器之前或在接收数据时操纵数据,然后将其发送回 JavaScript 代码之前对其进行操纵。
拦截器最常用的方式是在 JWT 令牌验证和刷新接收特定错误或 API 错误操纵的请求时使用。
在这个示例中,我们将学习如何创建一个请求拦截器来检查POST、PATCH和DELETE方法以及一个响应错误操纵器。
准备工作
此示例的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,我们可以使用我们在'创建不同的 axios 实例'食谱中制作的 Vue CLI 项目,也可以启动一个新的项目。
要开始一个新的,打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
创建拦截器
在接下来的步骤中,我们将创建一个axios
拦截器,它将作为中间件工作。按照说明正确执行:
- 安装
Sweet Alert
包。要做到这一点,您需要打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save sweetalert2
-
在
src/http
文件夹中创建一个名为interceptors.js
的新文件并打开它。 -
然后,我们导入 Sweet Alert 包:
import Swal from 'sweetalert2';
- 我们需要创建一个包含将被拦截的POST方法的数组的常量:
const postMethods = ['post', 'patch'];
- 我们需要创建一个名为
requestInterceptor
的函数并导出它。这个函数将接收一个参数config
,它是一个axios
配置对象。我们需要检查请求方法是否包含在我们之前创建的数组中,以及数据主体的data
属性是否有一个id
属性。如果任何检查未通过,我们将抛出一个Error
,否则,我们将返回config
:
export function requestInterceptor(config) {
if (
postMethods.includes(config.method.toLocaleLowerCase()) &&
Object.prototype.hasOwnProperty.call('id', config.data.data) &&
!config.data.data.id)
{
throw new Error('You need to pass an ID for this request');
} return config; }
- 对于响应拦截器,我们需要创建一个名为
responseInterceptor
的新函数,它返回响应,因为我们在这个拦截器中不会改变任何东西:
export function responseInterceptor(response) {
return response; }
- 为了捕获错误,我们需要创建一个
errorInterceptor
函数,它将被导出。这个函数接收一个error
作为参数,并显示一个sweetalert2
警报错误消息,并返回一个带有error
的Promise.reject
:
export function errorInterceptor(error) {
Swal.fire({
type: 'error',
title: 'Error!',
text: error.message,
}); return Promise.reject(error); }
将拦截器添加到 HTTP 方法函数中
在接下来的步骤中,我们将向 HTTP 方法函数添加axios
拦截器。按照以下步骤正确执行:
-
在
src/http
文件夹中打开baseFetch.js
文件。 -
我们需要导入刚刚创建的三个拦截器:
import {
errorInterceptor,
requestInterceptor,
responseInterceptor, } from './interceptors';
- 在创建
localApi
实例之后,我们声明了请求和响应拦截器的使用:
localApi.interceptors
.request.use(requestInterceptor, errorInterceptor); localApi.interceptors
.response.use(responseInterceptor, errorInterceptor);
- 在创建
jsonPlaceholderApi
实例之后,我们声明了请求和响应拦截器的使用:
jsonPlaceholderApi.interceptors
.request.use(requestInterceptor, errorInterceptor); jsonPlaceholderApi.interceptors
.response.use(responseInterceptor, errorInterceptor);
工作原理...
axios
执行的每个请求都会通过拦截器集中的任何一个。响应也是一样。如果在拦截器上抛出任何错误,它将自动传递给错误处理程序,因此请求根本不会被执行,或者响应将作为错误发送到 JavaScript 代码。
我们检查了每个POST,PATCH和DELETE方法所做的每个请求,以查看在请求体数据中是否有id
属性。如果没有,我们向用户抛出错误,告诉他们需要为请求传递一个 ID。
另请参阅
您可以在sweetalert2.github.io
找到有关 Sweet Alert 2 的更多信息。
您可以在github.com/axios/axios#interceptors
找到有关axios
请求拦截器的更多信息。
使用 Axios 和 Vuesax 创建 CRUD 界面
在处理数据时,我们总是需要做一些事情:CRUD 过程。无论您正在开发什么类型的应用程序,都需要 CRUD 界面以便在服务器上输入和操作任何数据,管理面板,应用程序的后端,甚至客户端。
在这里,我们将学习如何使用Vuesax
框架和axios
进行 HTTP 请求来创建一个简单的 CRUD 界面。
准备工作
此配方的先决条件如下:
- Node.js 12+
所需的 Node.js 全局对象如下:
-
@vue/cli
-
@vue/cli-service-global
如何做...
要启动我们的组件,请使用我们在“为 axios 创建请求和响应拦截器”配方中使用的 Vue CLI 的 Vue 项目,或者启动一个新项目。
要启动一个新项目,请打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> vue create http-project
CLI 将询问一些问题,这些问题将有助于创建项目。您可以使用箭头键导航,Enter键继续,Spacebar选择选项。选择default
选项:
? Please pick a preset: **(Use arrow keys)** ❯ default (babel, eslint)
Manually select features
将 Vuesax 添加到应用程序
在接下来的步骤中,我们将介绍如何将Vuesax
UI 库添加到您的 Vue 应用程序中。按照这些说明正确执行:
- 打开终端(macOS 或 Linux)或命令提示符/PowerShell(Windows)并执行以下命令:
> npm install --save vuesax material-icons
-
在
src
文件夹中创建一个名为style.css
的文件并打开它。 -
导入
vuesax
,material-icon
和Open Sans
字体样式表:
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,
800i&display=swap'); @import url('~vuesax/dist/vuesax.css'); @import url('~material-icons/iconfont/material-icons.css'); * {
font-family: 'Open Sans', sans-serif; }
-
打开
src
文件夹中的main.js
文件。 -
导入
style.css
文件和Vuesax
。之后,您需要通知 Vue 使用Vuesax
:
import './server/server'; import Vue from 'vue'; import App from './App.vue'; import Vuesax from 'vuesax'; import './style.css'; Vue.use(Vuesax); Vue.config.productionTip = false; new Vue({
render: h => h(App), }).$mount('#app');
创建组件路由
我们将在五个部分中继续这个过程:List
,Create
,Read
,Update
和Delete
。我们的应用将是一个动态组件应用程序,因此我们将创建五个组件,每个部分一个。这些组件将类似于我们的页面。
首先,我们需要将App.vue
更改为我们的主路由管理器,并创建一个用于更改组件的混合器。
单文件组件<script>
部分
在这部分,我们将创建单文件组件的<script>
部分。按照以下说明正确创建组件:
-
在
src
文件夹中打开App.vue
。 -
导入将在此处创建的每个组件:
import List from './components/list'; import Create from './components/create'; import View from './components/view'; import Update from './components/update';
- 在
data
属性中,创建两个新值:componentIs
默认值为'list'
,userId
默认值为0
:
data: () => ({
componentIs: 'list',
userId: 0, }),
- 我们需要向 Vue 对象添加一个名为
provide
的新属性。该属性将是一个函数,因此提供给组件的值可以是响应式的:
provide () {
const base = {}; Object.defineProperty(base, 'userId', {
enumerable: true,
get: () => Number(this.userId),
}); return base; },
- 在
computed
属性中,我们需要创建一个名为component
的新属性。这将是一个 switch case,根据componentIs
属性返回我们的组件:
computed: {
component() {
switch (this.componentIs) {
case 'list':
return List;
case 'create':
return Create;
case 'view':
return View;
case 'edit':
return Update;
default:
return undefined;
}
} },
- 最后,在方法中,我们需要创建一个
changeComponent
方法,将当前组件更新为新组件:
methods: {
changeComponent(payload) {
this.componentIs = payload.component;
this.userId = Number(payload.userId);
}, },
单文件组件<template>
部分
在这部分,我们将创建单文件组件的<template>
部分。按照以下说明正确创建组件:
- 在
div#app
HTML 元素中,我们需要添加一个vs-row
组件:
<div id="app">
<vs-row></vs-row> </div>
- 在
vs-row
组件中,我们需要添加一个vs-col
组件,具有以下属性:vs-type
定义为flex
,vs-justify
定义为left
,vs-align
定义为left
,vs-w
定义为12
:
<vs-col
vs-type="flex"
vs-justify="left"
vs-align="left"
vs-w="12"> </vs-col>
- 最后,在
vs-col
组件内部,我们将添加一个动态组件,该组件具有一个is
属性,指向计算属性component
,并将事件侦听器指向将执行changeComponent
方法的"change-component"
事件:
<component
:is="component"
@change-component="changeComponent" />
创建路由混合器
在这部分,我们将创建组件混合器,以便在其他组件中重复使用。按照以下说明正确创建组件:
-
在
src/mixin
文件夹中创建一个名为changeComponent.js
的新文件并打开它。 -
这个混合器将有一个名为
changeComponent
的方法,它将发出一个名为'change-component'
的事件,其中包含需要呈现的新组件的名称和userId
:
export default {
methods: {
changeComponent(component, userId = 0) {
this.$emit('change-component', { component, userId });
},
}
}
创建列表组件
列表组件将是索引组件。它将列出应用程序中的用户,并具有其他 CRUD 操作的所有链接。
单文件组件<script>
部分
在这部分,我们将创建单文件组件的<script>
部分。按照这些说明正确创建组件:
-
在
src/components
文件夹中创建一个名为list.vue
的新文件并打开它。 -
从
fetchApi
导入getHttp
和deleteHttp
,以及changeComponent
混合器:
import {
getHttp,
deleteHttp,
} from '../http/fetchApi';
import changeComponent from '../mixin/changeComponent';
- 在组件的
mixins
属性中,我们需要添加导入的changeComponent
混合器:
mixins: [changeComponent],
- 在组件的
data
属性中,我们添加了一个名为userList
的新属性,其默认为空数组:
data: () => ({
userList: [], }),
- 对于方法,我们创建了
getAllUsers
和deleteUsers
方法。在getAllUsers
方法中,我们获取用户列表,并将userList
的值设置为getHttp
函数执行的响应。deleteUser
方法将执行deleteHttp
函数,然后执行getAllUsers
方法:
methods: {
async getAllUsers() {
const { data } = await getHttp(`${window.location.href}api/users`);
this.userList = data;
},
async deleteUser(id) {
await deleteHttp(`${window.location.href}api/users/${id}`);
await this.getAllUsers();
}, }
- 最后,我们将
beforeMount
生命周期钩子异步化,调用getAllUsers
方法:
async beforeMount() {
await this.getAllUsers(); },
单文件组件部分
在这部分,我们将创建单文件组件的<template>
部分。按照这些说明正确创建组件:
- 创建一个带有
style
属性定义为margin: 20px
的vs-card
组件:
<vs-card style="margin: 20px;"
>
</vs-card>
- 在
vs-card
组件内部,为header
创建一个动态的<template>
,其中包含一个<h3>
标签和您的标题:
<template slot="header">
<h3>
Users
</h3> </template>
- 之后,创建一个
vs-row
组件,其中包含一个vs-col
组件,具有以下属性:vs-type
定义为flex
,vs-justify
定义为left
,vs-align
定义为left
,vs-w
定义为12
:
<vs-row>
<vs-col
vs-type="flex"
vs-justify="left"
vs-align="left"
vs-w="12">
</vs-col>
</vs-row>
- 在
vs-col
组件内部,我们需要创建一个vs-table
组件。该组件将具有指向userList
变量的data
属性,并将search
,stripe
和pagination
属性定义为 true。max-items
属性将被定义为10
,style
属性将具有width: 100%; padding: 20px;
的值:
<vs-table
:data="userList"
search
stripe pagination max-items="10"
style="width: 100%; padding: 20px;" ></vs-table>
- 对于表头,我们需要创建一个名为
thead
的动态 <template>
,并为每一列创建一个带有 sort-key
属性定义为相应对象键属性和显示为您想要的名称的 vs-th
组件:
<template slot="thead">
<vs-th sort-key="id">
#
</vs-th>
<vs-th sort-key="name">
Name
</vs-th>
<vs-th sort-key="email">
Email
</vs-th>
<vs-th sort-key="country">
Country
</vs-th>
<vs-th sort-key="phone">
Phone
</vs-th>
<vs-th sort-key="Birthday">
Birthday
</vs-th>
<vs-th>
Actions
</vs-th> </template>
- 对于表格主体,我们需要创建一个动态的
<template>
,其中定义了一个 slot-scope
属性作为 data
属性。在这个 <template>
中,我们需要创建一个 vs-tr
组件,它将迭代数据属性,并为表格头部设置的每一列创建一个 vs-td
组件。每个 vs-td
组件都有一个设置为相应列数据对象属性的数据属性,并且内容将是相同的数据渲染。最后一列是操作列,将有三个按钮,一个用于读取,另一个用于更新,最后一个用于删除。读取按钮将在 "click"
事件上有一个指向 changeComponent
的事件监听器,更新按钮也是如此。删除按钮的 "click"
事件监听器将指向 deleteUser
方法:
<template slot-scope="{data}">
<vs-tr :key="index" v-for="(tr, index) in data">
<vs-td :data="data[index].id">
{{data[index].id}}
</vs-td>
<vs-td :data="data[index].name">
{{data[index].name}}
</vs-td>
<vs-td :data="data[index].email">
<a :href="`mailto:${data[index].email}`">
{{data[index].email}}
</a>
</vs-td>
<vs-td :data="data[index].country">
{{data[index].country}}
</vs-td>
<vs-td :data="data[index].phone">
{{data[index].phone}}
</vs-td>
<vs-td :data="data[index].birthday">
{{data[index].birthday}}
</vs-td>
<vs-td :data="data[index].id">
<vs-button
color="primary"
type="filled"
icon="remove_red_eye"
size="small"
@click="changeComponent('view', data[index].id)"
/>
<vs-button
color="success"
type="filled"
icon="edit"
size="small"
@click="changeComponent('edit', data[index].id)"
/>
<vs-button
color="danger"
type="filled"
icon="delete"
size="small"
@click="deleteUser(data[index].id)"
/>
</vs-td>
</vs-tr> </template>
- 最后,在卡片页脚中,我们需要创建一个名为
footer
的动态 <template>
。在这个 <template>
中,我们将添加一个带有 vs-justify
属性定义为 flex-start
的 vs-row
组件,并插入一个带有 color
属性定义为 primary
、type
属性定义为 filled
、icon
属性定义为 fiber_new
和 size
属性定义为 small
的 vs-button
。@click
事件监听器将以参数 'create'
和 0
目标 changeComponent
方法:
<template slot="footer">
<vs-row vs-justify="flex-start">
<vs-button
color="primary"
type="filled"
icon="fiber_new"
size="small"
@click="changeComponent('create', 0)"
>
Create User
</vs-button>
</vs-row> </template>
单文件组件
<template>
部分。按照这些说明正确创建组件:style
属性定义为margin: 20px
的vs-card
组件:<vs-card style="margin: 20px;"
>
</vs-card>
vs-card
组件内部,为header
创建一个动态的<template>
,其中包含一个<h3>
标签和您的标题:<template slot="header">
<h3>
Users
</h3> </template>
vs-row
组件,其中包含一个vs-col
组件,具有以下属性:vs-type
定义为flex
,vs-justify
定义为left
,vs-align
定义为left
,vs-w
定义为12
:<vs-row>
<vs-col
vs-type="flex"
vs-justify="left"
vs-align="left"
vs-w="12">
</vs-col>
</vs-row>
vs-col
组件内部,我们需要创建一个vs-table
组件。该组件将具有指向userList
变量的data
属性,并将search
,stripe
和pagination
属性定义为 true。max-items
属性将被定义为10
,style
属性将具有width: 100%; padding: 20px;
的值:<vs-table
:data="userList"
search
stripe pagination max-items="10"
style="width: 100%; padding: 20px;" ></vs-table>
thead
的动态 <template>
,并为每一列创建一个带有 sort-key
属性定义为相应对象键属性和显示为您想要的名称的 vs-th
组件:<template slot="thead">
<vs-th sort-key="id">
#
</vs-th>
<vs-th sort-key="name">
Name
</vs-th>
<vs-th sort-key="email">
Email
</vs-th>
<vs-th sort-key="country">
Country
</vs-th>
<vs-th sort-key="phone">
Phone
</vs-th>
<vs-th sort-key="Birthday">
Birthday
</vs-th>
<vs-th>
Actions
</vs-th> </template>
<template>
,其中定义了一个 slot-scope
属性作为 data
属性。在这个 <template>
中,我们需要创建一个 vs-tr
组件,它将迭代数据属性,并为表格头部设置的每一列创建一个 vs-td
组件。每个 vs-td
组件都有一个设置为相应列数据对象属性的数据属性,并且内容将是相同的数据渲染。最后一列是操作列,将有三个按钮,一个用于读取,另一个用于更新,最后一个用于删除。读取按钮将在 "click"
事件上有一个指向 changeComponent
的事件监听器,更新按钮也是如此。删除按钮的 "click"
事件监听器将指向 deleteUser
方法:<template slot-scope="{data}">
<vs-tr :key="index" v-for="(tr, index) in data">
<vs-td :data="data[index].id">
{{data[index].id}}
</vs-td>
<vs-td :data="data[index].name">
{{data[index].name}}
</vs-td>
<vs-td :data="data[index].email">
<a :href="`mailto:${data[index].email}`">
{{data[index].email}}
</a>
</vs-td>
<vs-td :data="data[index].country">
{{data[index].country}}
</vs-td>
<vs-td :data="data[index].phone">
{{data[index].phone}}
</vs-td>
<vs-td :data="data[index].birthday">
{{data[index].birthday}}
</vs-td>
<vs-td :data="data[index].id">
<vs-button
color="primary"
type="filled"
icon="remove_red_eye"
size="small"
@click="changeComponent('view', data[index].id)"
/>
<vs-button
color="success"
type="filled"
icon="edit"
size="small"
@click="changeComponent('edit', data[index].id)"
/>
<vs-button
color="danger"
type="filled"
icon="delete"
size="small"
@click="deleteUser(data[index].id)"
/>
</vs-td>
</vs-tr> </template>
footer
的动态 <template>
。在这个 <template>
中,我们将添加一个带有 vs-justify
属性定义为 flex-start
的 vs-row
组件,并插入一个带有 color
属性定义为 primary
、type
属性定义为 filled
、icon
属性定义为 fiber_new
和 size
属性定义为 small
的 vs-button
。@click
事件监听器将以参数 'create'
和 0
目标 changeComponent
方法:<template slot="footer">
<vs-row vs-justify="flex-start">
<vs-button
color="primary"
type="filled"
icon="fiber_new"
size="small"
@click="changeComponent('create', 0)"
>
Create User
</vs-button>
</vs-row> </template>