Vue.js 结合 TypeScript,面向 Angular (2+) 开发者





5.00/5 (4投票s)
单页应用 (SPA) 结合 Vue.js
下载 Web API 下载 JavaScript 版本 下载 TypeScript 版本
引言
“三巨头”时代。如果您是职业篮球迷,您可能会将“三巨头”一词与勒布朗·詹姆斯、德怀恩·韦德和克里斯·波什三名篮球运动员联系起来,他们于 2010 年至 2014 年效力于 NBA 的迈阿密热火队。热火队在 2010 年的重磅自由球员市场引起了巨大的权力转移,开启了“三巨头”时代,随之而来的是一段混乱但伟大辉煌的历史。如今可以肯定地说,那支球队是 NBA 历史上最有趣的球队。在生活的其他领域,“三巨头”通常是指任何特定分组或主题中最突出的三个实体。以男子职业网球目前的状况为例。目前网球仍由网球界的“三巨头”——罗杰·费德勒、拉斐尔·纳达尔和诺瓦克·德约科维奇统治。自斯坦·瓦林卡赢得 2016 年美国网球公开赛以来,“网球三巨头”已经赢得了全部八项大满贯赛事。
在 Web 开发领域,我们在单页应用 (SPA) 方面现在也有了“三巨头”。单页应用正变得越来越受欢迎。Facebook、YouTube、Twitter、GitHub、Google 和许多其他服务都使用 SPA 技术构建。在本世纪的这些年里,我们看到了人们对三个主要 SPA 框架的兴趣日益增长:Angular、React 和 Vue.js,以及对 Ember.JS、Aurelia、Meteor.Js 等其他框架的一些关注。
单页应用 (SPA) 的演进
在 Web 应用开发早期,JavaScript 和 jQuery 为前端 Web 开发带来了显著的进步。它们提供了简单有效的开箱即用功能,例如客户端验证、模态窗口、警告消息、动画,甚至是基于 Ajax 的部分页面更新。然后进入单页应用框架和库的世界。单页应用是基于 Web 的应用程序,它们加载单个 HTML 页面,并在用户与应用程序交互时动态更新页面。SPA 使用 AJAX 和 HTML5 创建流畅响应式的 Web 应用前端,而无需不断重新加载页面。然而,这意味着大部分工作发生在客户端,即 JavaScript 中。术语“单页应用”的起源尚不清楚,尽管该概念至少早在 2003 年就被讨论过,但通常被描述为使用 Web 应用程序中的 JavaScript 来显示用户界面 (UI)、运行应用程序逻辑并与 Web 服务器通信的自包含网站。
三大框架概述
欢迎来到现在的 JavaScript 世界。Angular、React.js 或 Vue.js 等框架之所以流行,是因为它们解决了 jQuery 未能解决的一个根本性问题:跟踪页面状态、DOM 并与之交互,而无需我们自己控制一切,也无需执行完整的页面刷新。以下是“三大”SPA 框架和库的总结:
- **Angular**,由 Google 开发,于 2010 年首次发布,是其中最古老的。2016 年随着 Angular 2 的发布(并且从原始名称 AngularJS 中删除了“JS”)发生了一个重大转变,Angular 框架完全用 TypeScript 重写,尽管 AngularJS(版本 1)仍会收到更新。Angular 2+ 简称为 Angular。
- **React**,由 Facebook 开发,于 2013 年首次发布。Facebook 在其产品(Facebook、Instagram 和 WhatsApp)中广泛使用 React。
- **Vue**,也称为 Vue.js,是该群体中最年轻的成员。它由前 Google 员工 Evan You 于 2014 年开发。近几年来,Vue.js 的受欢迎程度显著提高,尽管它没有大型公司的支持。对于 Vue.Js 的第 3 版,Vue 的贡献者和支持者正在使用 TypeScript 重写该框架。
面向 Angular 开发者的 Vue.js
如果您和我一样,自早期采用以来就一直在开发单页应用程序,那么您可能已经使用 Angular 1 编写了一到两个或更多的 SPA,并在 2016 年发布时转向了使用 TypeScript 的 Angular 2。我的 codeproject.com 的几篇文章都包含了 Angular 1 或使用 TypeScript 的最新版本 Angular 2。作为软件开发人员和架构师,我们需要精通多种技术,但由于时间有限,我们必须仔细选择如何分配我们的时间。当然,您不希望花费五年时间学习和使用 Microsoft Silverlight 等技术,结果却发现 Web 应用程序开发社区完全放弃了它,或者从未接受过它。为此,我决定调整方向,转向 Vue.js。来自 Angular SPA 世界的我花费了大量时间,本文将引导您体验我学习 Vue.js 的过程,最初使用纯粹的原生 JavaScript,但最终转向使用 TypeScript 开发 Vue.js。
学习 Vue.js
如果您是一名 Angular 背景的开发者,刚刚开始接触 Vue.js,那么上手可能会既令人兴奋又不知所措。虽然每个人的学习过程都大不相同,但我发现以下路径对我帮助最大:
- 访问 Vue.js 网站,概览 Vue。
- 下载并安装 Vue.js 及其配套的 CLI 工具。
- 使用 Vue.js CLI 构建和创建默认的 Vue.js Hello World 应用程序。
- 先学习原生 JavaScript 的 Vue.js,然后再涉足 TypeScript 的世界。
- 付费在线课程,可以从 plurasight.com 或 Udemy.com 购买——Maximillian Schwarzmuller 在 Udemy 上的完整 Vue.js 课程非常出色,物有所值。
- 学习 Vue.js 的主要组件和方面,包括 Vue Router 和 Vuex。
- 学习一个 Vue.js UI 框架,例如用于 Material Design 的 Vuetify。
- 熟悉一些 Vue.js 开发工具。
- 开发一个小型 Vue.js 应用程序。
Vue.js 页面的结构
每个 Vue.js 单页应用程序的核心都是 Vue 文件。Vue 文件以 `.vue` 扩展名结尾,包含三个标签:一个用于 `template` 标签内的 HTML 模板,一个用于 `script` 标签内控制页面的 JavaScript,最后是用于将 CSS 应用于页面的 `style` 标签。CSS 样式还可以通过 `style` 标签上的 `scoped` 属性限制为仅应用于当前页面。Vue 页面还包含数据属性、方法、计算属性、监视器、生命周期事件和其他属性。`计算属性`和`监视器`是 Vue.js 中两个最基本概念。对于 Angular 开发者来说,Vue 的计算属性和监视器与 Angular 的 Observables 非常相似,它们提供了一种观察和响应 Vue 实例数据变化的方式。默认情况下,Vue 页面将页面所有的 HTML、JavaScript 和 CSS 都包含在单个文件中。与 Angular 一样,Vue.js 使用基于 HTML 的模板语法,允许您声明性地将渲染的 DOM 绑定到底层的 Vue 实例数据。
//
// orders.vue
//
<template>
<v-layout>
<!---xs12 sm6 offset-sm3-->
<v-flex>
<v-card>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">Orders</h3>
</div>
</v-card-title>
<v-data-table
class="elevation-1" :headers="headers" :hide-actions="true" :loading="loading"
:items="orderInquiryViewModel.orders">
<v-progress-linear v-slot:progress color="blue" indeterminate></v-progress-linear>
<template v-slot:items="props">
<td style="min-width: 50px; max-width: 50px; width:50px;">
{{ props.item.orderNumber }}
</td>
<td style="min-width:50px; max-width: 50px; width:50px;">
{{ props.item.orderDate | moment("MM/DD/YYYY") }}
</td>
</template>
<template v-slot:no-data>
<v-alert :value="displayNoDataMessage" color="error" icon="warning">
There are no orders
</v-alert>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
</template>
<style scoped>
a {
color: white;
}
</style>
<script>
import { OrderInquiryViewModel } from "../viewmodels/order-inquiry.viewmodel";
import { HttpService } from "../services/http-service";
export default {
components: {
HttpService
},
data() {
return {
httpService: null,
alertMessage: null,
orderInquiryViewModel: null,
headers: [
{
text: "Order Number",
align: "left",
sortable: false,
value: "OrderNumber"
},
{ text: "Order Date", value: "OrderDate", sortable: false },
{ text: "Customer Name", value: "CustomerName", sortable: false },
{ text: "Product #", value: "ProductNumber", sortable: false },
{ text: "Description", value: "Description", sortable: false },
{
text: "Order Quantity",
align: "right",
value: "OrderQuantity",
sortable: false
},
{
text: "Unit Price",
align: "right",
value: "UnitPrice",
sortable: false
}
],
loading: true,
displayNoDataMessage: false
};
},
methods: {
initializeSearch() {
this.orderInquiryViewModel.orders = [];
},
displayOrders(response) {
this.orderInquiryViewModel.orders = response.entity;
this.loading = false;
if (this.orderInquiryViewModel.orders.length == 0) {
this.displayNoDataMessage = true;
}
},
displayServerError: function(response) {
this.loading = false;
store.dispatch("alert/error", response.returnMessage[0]);
},
executeSearch() {
this.httpService.getOrders().then(function(response) {
if (response.returnStatus == true) {
this.displayOrders(response);
} else {
this.displayServerError(response);
}
});
}
},
created() {
this.orderInquiryViewModel = new OrderInquiryViewModel();
this.httpService = new HttpService(this.$http);
this.displayNoDataMessage = false;
},
mounted() {
this.initializeSearch();
this.executeSearch();
}
};
</script>
构建示例应用程序
本文的示例应用程序是一个小型购物车应用程序,也是我之前文章《使用 .NET Core 2 对 MongoDB 进行单元测试》的后续。该文章使用了最新版本的 Angular 2 和 TypeScript 作为前端。在本文中,我将介绍前端应用程序的开发,但这次使用最新版本的 Vue.js 并将 TypeScript 纳入其中。本文的示例应用程序的后端将使用我上面提到的先前文章中的 MongoDB 数据库的修改版 .NET Core 2 后端项目。
TypeScript 的优势
我开始使用原生 JavaScript 学习 Vue.js,并首先使用原生 JavaScript 开发了本文示例应用程序的整个前端。我认为这是学习 Vue.js 框架的最佳方式,因为大多数在线课程和关于 Vue.js 的文章都基于原生 JavaScript。但作为一名 Angular 2 开发者,我开始想念 TypeScript。TypeScript 是由 Microsoft 开发和维护的开源编程语言。它是 JavaScript 的严格语法超集,为该语言增加了静态类型。我喜欢 TypeScript,但它对大多数开发者来说是一种习惯。像技术中的所有事物一样,JavaScript 开发深深植根于大多数 JavaScript 开发者偏好的标准和传统。毕竟,TypeScript 代码只是编译成最新版本的 JavaScript。
我喜欢使用 TypeScript 进行开发的一些方面包括:
- **变量声明**——在 TypeScript 中,变量必须在使用前声明。这是我最喜欢的 TypeScript 功能。原生 JavaScript 开发可能会因为拼写错误而导致 bug,并且在测试过程中没有被发现。TypeScript 编译器将在我们尝试为不存在的变量赋值或由于变量未声明或拼写错误而找不到变量时生成错误。
- **语法糖**——TypeScript 附带了改进 JavaScript 外观和感觉的语法,包括类、接口和箭头函数。它感觉就像对 JavaScript 语言的自然扩展,也是许多 TypeScript 功能被纳入 ECMA 规范的原因之一。
- **强类型/静态类型**——静态类型语言可以最大限度地减少您犯的错误数量,并改进代码分析,从而使您所有的工具(如 IDE)都能提供提示、帮助和适当的重构。TypeScript 编译器还将在我们尝试为非同一类型的变量赋值时生成错误。
- **未来已来**——如前所述,许多 TypeScript 功能已成为 ECMA 规范的一部分。并且由于编译(转译),您无需等待浏览器支持。此外,由于 Angular 和下一版本 Vue.js(版本 3)都完全使用 TypeScript 进行内部开发,因此可以肯定地说,TypeScript 将在可预见的未来继续存在,甚至可能成为 JavaScript 开发标准。
总而言之,编写无 bug 的软件很难。我很高兴我们现在拥有了所有新工具,包括 CLI 工具、linter 以及当然还有 TypeScript,这些工具可以帮助我们生产出更少 bug 的软件。
入门 - Vue.js CLI 3.0
与 Angular 一样,Vue.js 提供了一个完整的系统,用于快速 Vue.js 开发。Vue CLI 3.0 版本安装最新版本的 Vue.js 框架,并提供完整的项目创建和脚手架。Vue CLI 旨在成为整个 Vue.js 生态系统的标准工具基线,包括安装插件、与 webpack 集成以在开发过程中实现热模块替换以及生产部署功能。
安装 Vue CLI 后,您可以按照以下方式创建新的 Vue 项目:
vue create hello-world
系统将提示您选择一个预设。您可以选择默认预设,也可以选择“手动选择功能”来选择所需的功能。对于示例应用程序,我选择了以下功能:Babel、TypeScript、Router、Vuex、Linting 和 Formatting。此外,您还可以使用 `vue ui` 命令创建和管理项目,这将打开一个 CLI UI,使项目创建和设置过程更加直观。
创建后,您可以使用以下 CLI 命令编译、构建并启动 webpack 开发服务器,然后在 localhost:8080 上访问应用程序:
npm run serve
Vuetify 和 Material Design
在开发应用程序之前,决定如何实现用户界面非常重要。就像选择任何其他库或框架一样,选择 UI 框架或库时应进行仔细研究。这些 UI 框架和库通常具有专有的语法和/或表示法。一旦您的应用程序变得庞大,您将与该框架绑定——比您选择的底层 JavaScript 框架更甚。最新的趋势之一是 Material Design。Material 是一个可适应的指南、组件和工具系统,支持用户界面设计的最佳实践。Material 在开源代码的支持下,简化了设计师和开发人员之间的协作,并帮助团队快速构建精美的产品。对于示例应用程序,我选择了 Vuetify 作为应用程序 Material Design UI 框架。Vuetify 符合 Material Design 规范。这意味着 Vue.js 和 Material 的核心功能将默认可用,并且可以由两个社区进行改进。Vuetify 还通过其 vue-cli-3 插件支持 Vue.js 工具的未来。
您也可以使用 Vue CLI UI,或通过命令行安装 Vuetify,如下所示:
Vue add vuetify
准备就绪
作为一名 Angular 开发者,您会注意到使用 Vue.js 准备就绪要容易得多。对于示例应用程序,一切都从下面的 `main.ts` TypeScript 文件开始。您只需将 Vuetify 注册到 Vue.js,然后创建一个 Vue 实例并将其附加/挂载到 HTML 元素。您无需配置任何模块或其他 Angular 所需的复杂配置。
// main.ts
import Vue from "vue";
import AppComponent from "./app.component.vue";
import router from "./router";
import "./plugins/vuetify";
import "./registerServiceWorker";
import Vuetify from "vuetify";
import colors from "vuetify/es5/util/colors";
Vue.use(Vuetify, {
theme: {
primary: colors.blue.darken1, // #E53935
secondary: colors.red.lighten4, // #FFCDD2
accent: colors.indigo.base // #3F51B5
}
});
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(AppComponent)
}).$mount("#app");
主要的 Vue.js TypeScript 页面
下面的示例应用程序的主要 Vue 页面是用 TypeScript 编写的。此页面是主页面,作为示例应用程序的 `body`,包含顶部的菜单栏和所有其他 Vue 页面将在此渲染的内容区域。主要 Vue 页面还将包含显示用户信息和警报的功能。
// app.component.ts
template src="./app.component.html"></template>
<style>
#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>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { UserInformationViewModel } from "./viewmodels/user-information.viewmodel";
import AlertComponent from "./alert.component.vue";
import store from "./store";
@Component({
components: { AlertComponent }
})
export default class AppComponent extends Vue {
private title: string;
constructor() {
super();
this.title = "VueJs With Typescript";
}
public logout() {
store.dispatch("logoutUser");
}
get userInformation() {
return store.state.userInformation;
}
get alert() {
return store.state.alert;
}
get displayProcessing() {
return store.state.processing;
}
}
</script>
主应用程序 HTML 页面包含一个 `router-view` 标签,所有内容页面将在路由更改时注入到该标签中。主应用程序 HTML 页面还包含一个进度条,在发出 HTTP 请求时将在内容页面顶部显示。
<!-- app.component.html -->
<v-app style="height: 100vh;">
<v-toolbar app style="background-color: green; color: white">
<v-toolbar-title class="headline text-uppercase">
<span>Code Project</span>
<span class="font-weight-light"> {{title}}</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<div v-if="userInformation.isAuthenicated==true">
{{userInformation.firstName}} {{userInformation.lastName}}
</div>
<v-spacer></v-spacer>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
</div>
<div id="nav" v-if="userInformation.isAuthenicated==true">
<router-link to="/productsearch">Product Search</router-link> |
<router-link to="/orders">Orders</router-link> |
<router-link to="/shoppingcart">Shopping Cart</router-link>
</div>
<div id="nav" style="margin-left:15px;" v-if="userInformation.isAuthenicated==false">
<router-link to="/register">Register</router-link> |
<router-link to="/login">Login</router-link>
</div>
<div id="nav" style="margin-left:15px;" v-if="userInformation.isAuthenicated==true">
<router-link v-on:click.native="logout" to="/login">Logout</router-link>
</div>
</v-toolbar>
<v-content>
<v-dialog content-class="progressbar" v-model="displayProcessing" persistent transition="false">
<v-container fluid fill-height fullscreen transition="false">
<v-layout justify-center align-center>
<v-progress-linear height="10" indeterminate color="primary"></v-progress-linear>
</v-layout>
</v-container>
</v-dialog>
<router-view />
<v-layout style="position:absolute; top:0; width:100%">
<AlertComponent
:messageId="alert.messageId" :messageType="alert.type" :displayMessage="alert.message">
</AlertComponent>
</v-layout>
</v-content>
</v-app>
装饰器和类组件
如果您来自 Angular (2+) 背景,您可能熟悉编写组件类并使用属性和装饰器来描述组件复杂部分的模式。类组件相对于标准 Vue.js 组件的最大优势在于它们可以实现更简洁、更标准的代码。在示例应用程序的 TypeScript 代码中,我安装并导入了 `vue-class-component` 和 `vue-property-decorator` npm 包。
类组件只是一个扩展 Vue 对象的 TypeScript 类。在下面的 AlertComponent 中,Vue 对象通过 `@Component` 装饰器进行扩展,这样我们就在 Vue.js 中拥有了一个类组件。AlertComponent 还使用了 `@Prop` 装饰器来装饰组件的输入属性。
import { Component, Watch, Prop, Vue } from "vue-property-decorator";
@Component
export default class AlertComponent extends Vue {
public timeoutId: number;
public originalMessageId: Date;
public alertModel: any;
public dismissed: Boolean;
@Prop() displayMessage: any;
@Prop() messageType: any;
@Prop() messageId: any;
constructor() {}
}
</script>
vue-property-decorator npm 包包含以下七个装饰器:`@Prop`、`@PropSync`、`@Provide`、`@Model`、`@Watch`、`@Inject`、`@Provide` 和 `@Emit`。`@Component` 装饰器由 vue-class-component npm 包提供。您还可以使用 `vuex-class` npm 包中的装饰器扩展 Vuex store。vuex-class 包附带以下用于 Vuex Store 的装饰器:`@State`、`@Getter`、`@Action` 和 `@Mutation`。
原始原生 JavaScript 的主 Vue.js 页面
下面是主 Vue 页面的原始原生 JavaScript。此页面具有三个计算属性,通过计算对象块结构实现:一个用于状态信息,一个用于显示警报消息,一个用于显示进度条。将示例应用程序重写为 TypeScript 意味着将计算属性块更改为 `get` 语句。在查看了示例应用程序的最终产品后,原生 JavaScript 版本的 Vue 页面似乎具有专有的外观和感觉。使用 TypeScript 似乎为 Vue 页面带来了更干净、更标准、更简洁的自然外观和感觉;包括使用类构造函数构建类和组件。使用 TypeScript,一切看起来都像一个常规类。突然之间,Vue.js 开始看起来很像 Angular (2+)。
<template>
<v-app style="height: 100vh;">
<v-toolbar app style="background-color: green; color: white">
<v-toolbar-title class="headline text-uppercase">
<span>Code Project</span>
<span class="font-weight-light"> MATERIAL DESIGN</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<div v-if="userInformation.isAuthenicated==true">
{{userInformation.firstName}}
{{userInformation.lastName}}
</div>
<v-spacer></v-spacer>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
</div>
<div id="nav" v-if="userInformation.isAuthenicated==true">
<router-link to="/productsearch">Product Search</router-link> |
<router-link to="/orders">Orders</router-link> |
<router-link to="/shoppingcart">Shopping Cart</router-link>
</div>
<div id="nav" style="margin-left:15px;" v-if="userInformation.isAuthenicated==false">
<router-link to="/register">Register</router-link> |
<router-link to="/login">Login</router-link>
</div>
<div id="nav" style="margin-left:15px;" v-if="userInformation.isAuthenicated==true">
<router-link v-on:click.native="logout" to="/login">Logout</router-link>
</div>
</v-toolbar>
<v-content>
<v-dialog content-class="progressbar"
v-model="displayProcessing" persistent transition="false">
<v-container fluid fill-height fullscreen transition="false">
<v-layout justify-center align-center>
<v-progress-linear height="10" indeterminate color="primary"></v-progress-linear>
</v-layout>
</v-container>
</v-dialog>
<router-view/>
<v-layout style="position:absolute; top:0; width:100%">
<AlertComponent :messageId="alert.messageId" :messageType="alert.type"
:displayMessage="alert.message">
</AlertComponent>
</v-layout>
</v-content>
</v-app>
</template>
<style>
.progressbar {
opacity: 0.7;
position: absolute;
top: 25px;
height: 25px;
overflow: hidden;
}
</style>
<script>
import { UserInformationViewModel } from "./viewmodels/user-information.viewmodel";
import AlertComponent from "./components/Alert.component";
import store from "./store";
export default {
name: "App",
components: {
AlertComponent
},
data() {
return {};
},
methods: {
logout() {
store.dispatch("logoutUser");
}
},
created() {},
computed: {
userInformation() {
return store.state.userInformation;
},
alert() {
return store.state.alert;
},
displayProcessing() {
return store.state.processing;
}
}
};
</script>
单独的 HTML 模板文件
Vue.js 的一个亮点是所有的 HTML 模板都与 JavaScript 代码包含在同一个文件中。这遵循了 React 框架的实现。在开发示例应用程序的原生 JavaScript 版本时,我注意到我不得不在 `.vue` 文件中在 HTML 模板和 JavaScript 代码之间上下滚动,从而丢失了文件中的位置。当然,我也可以在 Visual Studio Code 中安装一些工具,提供跳转和锚定功能,以便轻松地在源文件中来回移动。也许我只是习惯了使用单独的 HTML 文件开发 Angular 应用程序,并使用 Visual Studio Code 中的上下文切换工具和命令在模板文件和代码文件之间快速切换。在开发示例应用程序的 TypeScript 版本时,我决定将 HTML 模板移动到每个 Vue 页面的单独 HTML 文件中。使用 `template` 标签的 `src` 属性提供了此功能。作为一名 Angular 开发者,我开发 Vue 页面的体验变得与 Angular 类似。
<template src="./app.component.html"></template>
Vuex
大型应用程序可能会因多个组件之间分散的状态以及它们之间的交互而变得复杂。为了解决这个问题,Vue 提供了 Vuex。Vuex 受到 React 的 Redux 实现的启发,它帮助 React 开发者集中应用程序的状态和逻辑,从而实现强大的状态持久化功能。Vuex 是 Vue.js 应用程序的状态管理模式+库。它作为应用程序中所有组件的集中式存储,并有规则确保状态只能以可预测的方式进行变异。在 Angular 应用程序中,状态管理通常在一个全局的单例类对象中进行管理,该对象可以通过依赖注入在所有组件之间共享。通过定义和分离状态管理中涉及的概念,并强制执行维护视图和状态之间独立性的规则,Vuex 使 Vue 代码更具结构性和可维护性。Vuex npm 包可以通过 Vue CLI UI 或从命令行安装,如下所示:
vue install vuex --save
在下面的 `main.ts` 文件中,我添加了一个导入语句来引用 Vuex 包,并将其注册到 Vue。
// main.ts
import Vue from ‘vue’;
import VueX from ‘vuex’;
Vue.use(Vuex);
Vuex Store
为了管理示例应用程序中的状态,我创建了一个 `store.ts` 文件,并添加了代码来创建 store 实例,使用 `Vuex.Store` 函数来跟踪应用程序状态。Vuex store 通过 `mutations` 和 `actions` 进行管理。更改 Vuex store 中状态的唯一方法是提交 mutation。Vuex mutations 非常类似于事件:每个 mutation 都有一个字符串类型和一个处理器。处理器函数是我们执行实际状态修改的地方。
//
// store.ts
//
import Vue from "vue";
import Vuex from "vuex";
import alert from "./alert.module";
import { UserInformationViewModel } from "./viewmodels/user-information.viewmodel";
let userInformationModel: UserInformationViewModel = new UserInformationViewModel();
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
alert
},
state: {
userInformation: userInformationModel,
processing: false
},
mutations: {
updateUser(state: any, user: UserInformationViewModel): void {
state.userInformation = user;
},
startProcessing(state: any): void {
state.processing = true;
},
stopProcessing(state: any): void {
setTimeout(function(): void {
state.processing = false;
}, 100);
},
logoutUser(state: any): void {
state.userInformation = new UserInformationViewModel();
localStorage.removeItem("VueToken");
}
},
actions: {
updateUser(context: any, user: UserInformationViewModel): void {
context.commit("updateUser", user);
},
logoutUser(context: any): void {
context.commit("logoutUser");
},
startProcessing(context: any): void {
context.commit("startProcessing");
},
stopProcessing(context: any): void {
context.commit("stopProcessing");
}
}
});
要调用 mutation 处理器,您需要调用 store 的 `commit` 语句。例如,下面一行会更新 store 中的用户信息。
context.commit(“updateUser”, user);
Actions 类似于 mutations,区别在于 actions 提交 mutations 而不是变异状态。希望更新 store 的 Vue 页面可以通过执行 `store.dispatch` 方法来触发 store actions。要从 Vue 页面更新用户的状态信息,将在示例应用程序的各个 Vue 页面中执行以下语句。
store.dispatch("updateUser", userInformation);
用户信息视图模型
为了增强单页应用程序的开发过程,我喜欢使用 ES6 类,并使用强类型的 TypeScript 属性,并通过构造函数初始化类的每个属性的值。ES6 类和 TypeScript 的结合使得单页应用程序的代码库更加紧凑,并且不容易出现在测试期间可能未被捕获的生产错误。
下面的 `UserInformationViewModel` 将用于在 Vuex store 中存储用户的登录信息。
export class UserInformationViewModel {
constructor() {
this.id = "";
this.firstName = "";
this.lastName = "";
this.addressLine1 = "";
this.addressLine2 = "";
this.city = "";
this.state = "";
this.zipCode = "";
this.emailAddress = "";
this.phoneNumber = "";
this.lastLogin = new Date();
this.isAuthenicated = false;
}
public id: string;
public firstName: string;
public lastName: string;
public addressLine1: string;
public addressLine2: string;
public city: string;
public state: string;
public zipCode: string;
public emailAddress: string;
public phoneNumber: string;
public lastLogin : Date;
public isAuthenicated: boolean;
}
管理 Store 大小和代码库
随着应用程序规模的增大,Vuex store 中的信息很可能会增加,管理 store 的代码也可能变得庞大且难以管理。为了解决这个问题,您可以创建单独的命名空间模块并将其添加到 store 代码库中。示例应用程序的附加功能之一是从一个中心位置生成各种 toaster 和警报消息。Vuex store 似乎是此功能的一个合乎逻辑的位置。示例应用程序可以显示几种不同类型的消息:信息消息、错误消息、警告消息和成功消息,每种消息都使用我们在大多数应用程序中看到的标准颜色(红、蓝、黄和橙)。
为了使 store 保持可管理并帮助保持代码库的整洁,我创建了一个 alerts 模块,如下所示:
//
// alert.module.ts
//
import store from "./store";
export const alert: any = {
namespaced: true,
state: {
type: null,
message: null,
messageId: null
},
mutations: {
error(state: any, message: any): void {
state.type = "error";
state.message = message;
state.messageId = new Date();
},
success(state: any, message: any): void {
state.type = "success";
state.message = message;
state.messageId = new Date();
},
warning(state: any, message: any): void {
state.type = "warning";
state.message = message;
state.messageId = new Date();
},
info(state: any, message: any): void {
state.type = "info";
state.message = message;
state.messageId = new Date();
}
},
actions: {
success(state: any, message: any): void {
store.commit("alert/success", message);
},
error(state: any, message: any): void {
store.commit("alert/error", message);
},
warning(state: any, message: any): void {
store.commit("alert/warning", message);
},
info(state: any, message: any): void {
store.commit("alert/info", message);
}
}
};
export default alert;
要将 alerts 模块添加到 Vuex store,只需在 `store.ts` 文件中导入该模块并将其作为模块引用即可。
//
// store.ts
//
import Vue from "vue";
import Vuex from "vuex";
import alert from "./alert.module";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
alert
}
});
由于 alerts 模块是 `namespaced`(命名空间化的),因此从任何 Vue 页面触发警报消息都是通过分派一个以 alerts 命名空间为前缀的 action,并指定您要显示的消息类型来完成的。
store.dispatch("alert/error", response.returnMessage[0]);
共享 Vue.js 组件
与 Angular 应用程序一样,Vue.js 也支持可在整个应用程序中使用的可重用共享组件。在示例应用程序中,将在页面顶部显示警报消息,并具有 toaster 功能。下面的代码片段引用了一个 `AlertComponent`,并已添加到主 Vue 页面 HTML 模板中。
<v-layout style="position:absolute; top:0; width:100%">
<AlertComponent
:displayMessage="alert.message" :messageId="alert.messageId" :messageType="alert.type">
</AlertComponent>
</v-layout>
AlertComponent 的 HTML 模板包含以下 HTML。Vuetify 提供了显示 toaster 消息和消息框的功能。
<div class="alert-style">
<v-alert v-if="messageType !== null"
dismissible :type="messageType" :value="displayAlertMessage"
transition="scale-transition"
@click="close" :value="displayAlertMessage" >
{{displayMessage}}
</v-alert>
</div>
在使用各种 UI 库时,有时必须克服的一个问题是它们并不总是提供您想要的确切功能。Vuetify 的警报没有自动在用户按下关闭按钮后一定秒数后自动消失的消息框或 toaster 消息的功能。为了提供此功能,我必须通过实现一个超时功能来扩展消息框,该功能在五秒钟后将计算属性 `displayAlertMessage` 设置为 false。
<template src="./alert.component.html"></template>
<style scoped>
.alert-style {
width: 100%;
position: absolute;
right: 0;
z-index: 1000;
}
</style>
<script lang="ts">
import { Component, Watch, Prop, Vue } from "vue-property-decorator";
@Component
export default class AlertComponent extends Vue {
public timeoutId: number;
public originalMessageId: Date;
public alertModel: any;
public dismissed: Boolean;
@Prop() displayMessage: any;
@Prop() messageType: any;
@Prop() messageId: any;
constructor() {
super();
this.timeoutId = -1;
this.originalMessageId = new Date();
this.alertModel = null;
this.dismissed = false;
}
public close() {
clearTimeout(this.timeoutId);
this.dismissed = true;
this.originalMessageId = new Date();
}
public created() {
this.dismissed = false;
}
get displayAlertMessage() {
if (this.messageId != this.originalMessageId && this.dismissed == false) {
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => {
this.originalMessageId = this.messageId;
}, 5000);
this.dismissed = false;
return true;
} else {
this.dismissed = false;
return false;
}
}
}
</script>
AlertComponent 有三个输入属性,包括要显示的消息类型和要显示的消息。在使用 TypeScript 时,这些属性需要用 `@Prop()` 注解进行注解,如上所示。这个 AlertComponent 的原始原生 JavaScript 版本包含一个用于输入属性的 props 块。您再次可以看到,此组件的 TypeScript 版本具有更少的专有外观和感觉。
<script>
export default {
name: "AlertComponent",
data() {
return {
timeoutId: -1,
originalMessageId: null,
alertModel: null,
dismissed: false
}
},
props: {
displayMessage: String,
messageType: String,
messageId: Date
},
methods: {
close() {
clearTimeout(this.timeoutId);
this.dismissed = true;
this.originalMessageId = null;
}
},
created() {
this.dismissed = false;
},
computed: {
displayAlertMessage: function() {
if (this.messageId != this.originalMessageId && this.dismissed == false) {
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(()=>{
this.originalMessageId = this.messageId;
}, 5000);
this.dismissed = false;
return true;
}
else {
this.dismissed = false;
return false;
}
}
}
}
</script
Vue.js 路由
Vue Router 是 Vue.js 的官方路由器。它与 Vue.js 核心功能深度集成,使使用 Vue.js 构建单页应用程序变得轻而易举。它实际上与 Angular 中的路由非常相似。您只需将路由路径与 Vue 组件关联起来。在下面的 `router.ts` 文件中,`vue-router` 被导入并注册到 Vue 实例。一些额外的关键注意事项包括:
- **Meta 字段**——在 Vue 路由中,您可以在定义路由时包含一个 meta 字段。当在 Vue 应用程序中发生路由时,meta 字段会包含在路由信息中。在下面的代码片段中,我创建了一个 `requiresAuthorization` meta 字段。此字段将在以后用于保护路由免受未经授权的访问,类似于 Angular 中的路由守卫实现。
- **Webpack Chunk 名称**——使用打包器构建应用程序时,JavaScript 包可能会变得相当大,从而影响页面加载时间。如果我们能将每个路由的组件拆分成一个单独的块,并在访问路由时才加载它们,那将更有效。使用 `webpack` 块名称,您可以将组件分组到块中。在下面的代码片段中,将创建两个块文件,一个用于产品,一个用于订单。
- **History 模式**——Vue Router 的默认模式是 hash 模式,它使用 URL hash 来模拟完整的 URL,这样在 URL 更改时页面就不会重新加载。要从 URL 中删除 hash(井号),我们可以使用路由器的 history 模式,该模式利用 `history.pushstate` API。
//
// router.ts
//
import Vue from "vue";
import Router from "vue-router";
import HomeComponent from "./views/home.component.vue";
import AboutComponent from "./views/about.component.vue";
import RegisterComponent from "./views/register.component.vue";
import LoginComponent from "./views/login.component.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: HomeComponent
},
{
path: "/login",
name: "login",
component: LoginComponent
},
{
path: "/register",
name: "register",
component: RegisterComponent
},
{
path: "/checkout",
name: "checkout",
component: () =>
import(
/* webpackChunkName: "orders" */ "./views/checkout.component.vue"
),
meta: {
requiresAuthorization: true
}
},
{
path: "/orders",
name: "orders",
component: () =>
import(/* webpackChunkName: "orders" */ "./views/orders.component.vue"),
meta: {
requiresAuthorization: true
}
},
{
path: "/productdetail/:id",
name: "productdetail",
component: () =>
import(
/* webpackChunkName: "products" */ "./views/productdetail.component.vue"
),
meta: {
requiresAuthorization: true
}
},
{
path: "/productsearch",
name: "productsearch",
component: () =>
import(
/* webpackChunkName: "products" */ "./views/productsearch.component.vue"
),
meta: {
requiresAuthorization: true
}
},
{
path: "/shoppingcart",
name: "shoppingcart",
component: () =>
import(
/* webpackChunkName: "orders" */ "./views/shoppingcart.component.vue"
),
meta: {
requiresAuthorization: true
}
},
{
path: "/about",
name: "about",
component: AboutComponent
}
]
})
Vue.js 路由导航守卫
顾名思义,Vue 路由提供的导航守卫主要用于通过重定向路由请求或取消路由请求来保护导航。有几种方法可以挂接到路由导航过程:全局、按路由或在组件中。保护 Vue.js 应用程序的某些部分的替代方法是使用带有 meta 字段的全局导航守卫。通过这种方法,我们可以如前所述在 Vue 路由器中注册守卫。在下面的 `main.ts` 文件中,添加了一个 `beforeEach` 钩子,它将在每次路由转换时执行。该钩子检查路由定义中定义的布尔 meta 字段 `requiresAuthorizated`,当 requiredAuthorization 字段设置为 true 时,路由钩子会检查应用程序 store 以确定用户是否已进行身份验证。如果身份验证检查失败,路由器将应用程序重定向到登录页面;否则,路由器将继续执行到请求的路由。
//
// router guard implementation - main.ts
//
router.beforeEach(
(to: any, from: any, next: any): void => {
let requiresAuthorization: Boolean = to.matched.some(
(x: { meta: { requiresAuthorization: Boolean } }) =>
x.meta.requiresAuthorization
);
if (requiresAuthorization) {
if (store.state.userInformation.isAuthenicated === true) {
next();
} else {
next("/login");
}
} else {
next();
}
}
);
HTTP RESTful Web API 支持
构建现代 Web 应用程序时,您很可能需要从某些远程资源(无论是您自己构建的还是别人构建的)获取数据。发送 HTTP 请求是从面向客户端的应用程序向 RESTful Web API 后端发送数据的一种更流行的方式。Vue.js 本身不提供 HTTP 功能,但有几个库和插件可供使用,例如流行的 `Axios` HTTP 客户端和 `vue-resource` 插件,或者浏览器内置的 `fetch` API。我最初为示例应用程序的原生 JavaScript 版本实现了 vue-resource 插件,但 vue-resource 插件被认为是已弃用的,并且 Vue 界中的许多人已将其从官方推荐状态中撤销。对于示例应用程序的 TypeScript 版本,我实现了 Axios HTTP 客户端。Axios 目前是最流行的 HTTP 客户端库之一,它提供了与 vue-resource 非常相似的 API,覆盖了几乎所有功能。此外,它是通用的,支持取消,并且具有 TypeScript 定义。Axios 简单且非常轻量级,这使其成为任何 Vue.js 项目的绝佳解决方案。
要将 axios 包含在您的项目中,请执行以下操作:
npm install axios --save
实现 HTTP 单例服务
本文的示例应用程序向 .NET Core Web API 后端发出 RESTful Web API 调用。为了帮助封装和集中 Axios 库的 HTTP 功能,使其能够在应用程序中重用,而不会在应用程序中充斥大量重复的 HTTP 代码,我创建了一个实现单例模式的 HTTP 服务类。单例服务是跨组件共享的服务实例。实现单例模式意味着您在应用程序中只创建一个类的实例。`http.service.ts` 文件中的最后一行创建了 `HttpService` 类的一个实例并将其导出,以便任何需要使用它的组件都可以简单地导入它。
//
// http.service.ts
//
import { ResponseModel } from "./viewmodels/response.model";
import axios from "axios";
import store from "./store";
export class HttpService {
private urlRoot: string;
constructor() {
this.urlRoot = "https://:44340/api/secureonlinestore";
}
login(requestData: any): any {
return this.httpPost("/login", requestData);
}
register(requestData: any): any {
return this.httpPost("/register", requestData);
}
getProducts(requestData: any): any {
return this.httpPost("/productinquiry", requestData);
}
createOrder(requestData: any): any {
return this.httpPost("/createOrder", requestData);
}
getOrders(): any {
return this.httpGet("/GetOrders");
}
getProductDetail(productId: string): any {
return this.httpGet("/getproductdetail/" + productId);
}
validateEmailAddress(emailAddress: string): any {
return this.httpGet("/ValidateEmailAddress/" + emailAddress);
}
httpGet(urlPath: string): any {
store.dispatch("startProcessing");
let url: string = this.urlRoot + urlPath;
return axios
.get(url)
.then(response => {
store.dispatch("stopProcessing");
return response.data;
})
.catch((error: any) => {
let response: ResponseModel = new ResponseModel();
response.returnStatus = false;
if (error.response) {
if (error.response.status === "401") {
response.returnMessage.push("Unauthorized");
} else {
response.returnMessage.push(error.response.data.returnMessage[0]);
}
} else {
response.returnMessage.push(error);
}
store.dispatch("stopProcessing");
return response;
});
}
httpPost(urlPath: string, requestData: any): any {
store.dispatch("startProcessing");
let url: string = this.urlRoot + urlPath;
return axios
.post(url, requestData)
.then(response => {
store.dispatch("stopProcessing");
return response.data;
})
.catch((error: any) => {
let response: ResponseModel = new ResponseModel();
response.returnStatus = false;
if (error.response) {
if (error.response.status === "401") {
response.returnMessage.push("Unauthorized");
} else {
response.returnMessage.push(error.response.data.returnMessage[0]);
}
} else {
response.returnMessage.push(error);
}
store.dispatch("stopProcessing");
return response;
});
}
}
export default new HttpService();
Axios HTTP Interceptors
Axios 提供了一些受 Angular $http 库启发的特性。Axios 允许我们添加称为 `interceptors` 的函数。这些拦截器函数可以在发出请求时附加到请求,或者在收到响应时附加到响应。对于示例应用程序,用于 Web API 请求的 Axios 拦截器将作为一种集中方式将自定义标头发送到服务器。由于服务器 Web API 大部分端点都需要授权令牌,因此 Axios 请求拦截器将从客户端的 `local storage` 中提取存储的 `JSON Web Token (JWT)`,并在请求发送到服务器之前将其作为“Bearer”令牌添加到请求标头中。对于响应拦截器,任何来自服务器的响应,如果其 HTTP 状态码为 401(未授权),将被拦截并重定向用户到登录页面。
import axios from "axios";
//
// http interceptors - main.ts
//
axios.interceptors.request.use(
config => {
const token: any = localStorage.getItem("VueToken");
if (token != null && token !== undefined) {
config.headers.Authorization = "Bearer " + token;
}
config.headers.Accept = "application/json";
return config;
},
error => {
return Promise.reject(error);
}
);
const UNAUTHORIZED: number = 401;
axios.interceptors.response.use(response => response,
error => {
const status: any = error.response;
if (status === UNAUTHORIZED) {
router.push({ name: "login" });
}
return Promise.reject(error);
}
);
Login 组件
为了看到一切正常运行,下面的登录组件执行以下功能:
- 导入 Http 服务单例服务
- 导入 Vuex Store
- 导入并初始化用户和用户信息视图模型
- 为 Vuetify 表单设置必需的验证规则
- 当用户按下登录按钮时,将验证表单
- 执行 Http 服务将用户的凭据发布到服务器
- 成功登录后,服务器生成的授权令牌(JWT)将存储在用户的本地存储中
- 分派 Vuex store 以更新 Vuex store 中的用户信息
- 成功登录后,Vue 路由器会将用户重定向到产品搜索页面
- 如果用户的凭据身份验证失败,将显示一个 toaster 错误消息
//
// login.component.ts
//
<template src="./login.component.html"></template
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { UserViewModel } from "../viewmodels/user.viewmodel";
import { UserInformationViewModel } from "../viewmodels/user-information.viewmodel";
import httpService from "../http.service";
import store from "../store";
@Component
export default class LoginComponent extends Vue {
public emailAddressErrors: Array<string>;
public validForm: Boolean;
public userViewModel: UserViewModel;
public requiredRules: Array<any>;
constructor() {
super();
this.emailAddressErrors = new Array<string>();
this.validForm = false;
this.userViewModel = new UserViewModel();
this.userViewModel.emailAddress = "";
this.userViewModel.password = "";
this.requiredRules = [(v: any) => !!v || "Field is required"];
}
public login() {
let thisComponent: any = this;
let form: any = this.$refs.form;
if (form.validate == false) {
return false;
}
httpService.login(this.userViewModel).then(function(response: any) {
if (response.returnStatus === true) {
let token = response.entity.token;
localStorage.setItem("VueToken", token);
let userInformation = new UserInformationViewModel();
userInformation.firstName = response.entity.firstName;
userInformation.lastName = response.entity.lastName;
userInformation.emailAddress = response.entity.emailAddress;
userInformation.id = response.entity.id;
userInformation.phoneNumber = response.entity.phoneNumber;
userInformation.addressLine1 = response.entity.addressLine1;
userInformation.addressLine2 = response.entity.addressLine2;
userInformation.city = response.entity.city;
userInformation.state = response.entity.state;
userInformation.zipCode = response.entity.zipCode;
userInformation.isAuthenicated = true;
store.dispatch("updateUser", userInformation);
thisComponent.loginSuccessful();
} else {
store.dispatch("alert/error", response.returnMessage[0]);
}
});
}
public loginSuccessful() {
this.$router.push({ name: "productsearch" });
}
}
</script>
Vuetify 表单验证
在表单验证方面,Vuetify 具有大量的集成和内置功能。您也可以选择使用第三方验证插件,例如 `Vee-validate` 或 `Vuelidate`。对于示例应用程序,我使用了内置的验证功能。
下面是注册页面的 HTML 模板,其中包含一个 Vuetify 表单和对必填字段、电子邮件地址格式、密码匹配确认以及后端数据库中重复电子邮件地址的验证。
<v-layout>
<v-flex>
<v-card>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">Register</h3>
</div>
</v-card-title>
<v-form ref="form" v-model="validForm">
<v-container fluid>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field @blur="validateEmailAddress"
v-model="userViewModel.emailAddress"
required :rules="emailRules" :error-messages="emailAddressErrors"
label="Email Address">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.password" required :rules="requiredRules"
label="Password"></v-text-field>
</v-flex>
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.passwordConfirmation"
required :error-messages="passwordConfirmationErrors"
label="Password Confirmation" :rules="passwordConfirmationRules">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.firstName"
required :rules="requiredRules"
label="First Name">
</v-text-field>
</v-flex>
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.lastName"
required :rules="requiredRules"
label="Last Name">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.addressLine1"
required :rules="requiredRules"
label="Address Line 1"></v-text-field>
</v-flex>
<v-flex xs12 lg3 sm6 md6>
<v-text-field
v-model="userViewModel.addressLine2"
label="Address Line 2">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.city"
required :rules="requiredRules"
label="City">
</v-text-field>
</v-flex>
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.state"
required :rules="requiredRules"
label="State">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.zipCode"
required :rules="requiredRules"
label="Zip Code">
</v-text-field>
</v-flex>
</v-layout>
<v-layout v-bind="binding">
<v-flex xs12 lg3 sm6 md6>
<v-text-field v-model="userViewModel.phoneNumber"
required :rules="requiredRules"
label="Phone Number">
</v-text-field>
</v-flex>
</v-layout>
</v-container>
<v-card-actions>
<v-btn flat color="primary" :disabled="!validForm" @click="register">Register</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-flex>
</v-layout>
在电子邮件地址表单字段中,添加了一个 `@blur` 事件处理程序到该字段,以验证电子邮件地址是否唯一且当前未被使用。电子邮件验证规则被分配给 `:rules` 属性,任何错误都将通过绑定到 `:error-messages` 表单字段属性来显示。
<v-text-field @blur="validateEmailAddress"
v-model="userViewModel.emailAddress"
required :rules="emailRules" :error-messages="emailAddressErrors"
label="Email Address">
</v-text-field>
设置 Vuetify 的验证规则就像设置一个验证数组一样简单。下面的电子邮件验证规则数组包含一个必填字段规则和一个确保电子邮件地址遵循有效电子邮件地址格式的规则。
this.emailRules = [
(v: any) => !!v || "E-mail address is required",
(v: any) => /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) || "E-mail address is invalid"
];
在 `on blur` 事件上,将触发 `validateEmailAddress` 方法,该方法将重置电子邮件地址验证数组,并发出 Web API 请求到服务器,以验证电子邮件地址是否唯一且尚不存在。
public validateEmailAddress(event: any) {
let vm = this;
let emailAddress = event.target.value;
if (emailAddress == null || emailAddress == undefined || emailAddress.length == 0) {
return false;
}
vm.emailAddressErrors = [];
httpService.validateEmailAddress(emailAddress).then(function(response: any) {
if (response.returnStatus == false) {
vm.emailAddressErrors.push(response.returnMessage[0]);
}
});
}
另一种附加的验证类型是密码确认匹配。由于密码验证匹配需要在表单中的两个字段中匹配,因此密码匹配验证需要设置为一个 `computed property`(计算属性),以便它能够响应表单中值的变化。使用 TypeScript 的 `get` 属性将 `passwordConfirmationRules` 属性设置为计算属性。此页面的原始原生 JavaScript 版本为此实现了一个计算块。
get passwordConfirmationRules() {
let rules = [];
let rule1 = (v: any) => !!v || "Password is required";
rules.push(rule1);
let rule2 = (v: any) =>
(!!v && v) === this.userViewModel.password || `Password confirmation must match`;
rules.push(rule2);
return rules;
}
当用户按下注册按钮时,将执行 `register` 方法。在将表单信息提交到服务器之前,始终要做的第一件事是重新验证表单上的所有内容是否仍然有效,方法是执行 `form.validate` 方法。用户可能会重新编辑表单上的某个字段,该字段可能不会触发表单上的所有必需验证。在 `register` 方法中,需要将对表单实例的引用重新分配给另一个类型为 `any` 的对象。这是因为 Vue.js 没有 TypeScript 的 `validate` 方法的定义,在代码中引用它会产生 TypeScript 编译错误。
public register() {
let vm: any = this;
let form: any = this.$refs.form;
if (form.validate() === false) {
return false;
}
httpService.register(this.userViewModel).then(function(response: any) {
if (response.returnStatus == true) {
vm.$router.push({ name: "login" });
} else {
store.dispatch("alert/error", response.returnMessage[0]);
}
});
}
Vue.js 过滤器
下面是订单页面的 Vuetify Material Design 模板。在模板中您将看到的两个项目是订单日期和单价正在使用过滤器进行格式化。Vue.Js 中的过滤器类似于 Angular 中的过滤器。过滤器是 Vue 组件提供的一种功能,允许您对模板数据中的任何部分应用格式化和转换。过滤器不会改变组件数据或任何东西,只会影响渲染的输出。过滤器可以有两种范围:全局供所有组件使用,或局部作用于定义它的组件。
事实证明,Vue.Js 并没有提供任何开箱即用的过滤器,但有一些有用的社区包可能对您的项目有所帮助。对于示例应用程序的 Vue.js 项目的原生 JavaScript 版本,我下载并安装了以下两个流行的 npm 包:
- vue2-filters——这个 npm 包提供了超过十种常见的过滤器,如 capitalize、uppercase、lowercase、currency 等。
- vue-moment——基于流行的 Moment.js 过滤器包,用于格式化时间值。
<v-layout>
<!---xs12 sm6 offset-sm3-->
<v-flex>
<v-card>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">Orders</h3>
</div>
</v-card-title>
<v-data-table :headers="headers" :hide-actions="true" :items="orderInquiryViewModel.orders"
class="elevation-1">
<v-progress-linear v-slot:progress color="blue" indeterminate></v-progress-linear>
<template v-slot:items="props">
<td style="min-width: 50px; max-width: 50px; width:50px;">
{{ props.item.orderNumber }}
</td>
<td style="min-width:50px; max-width: 50px; width:50px;">
{{ props.item.orderDate | moment("MM/DD/YYYY") }}
</td>
<td style="min-width:200px; max-width: 200px; width:200px;">
{{ props.item.customerName }}
</td>
<td style="min-width:150px; max-width: 150px; width:150px;">
{{ props.item.productNumber }}
</td>
<td style="min-width:200px; max-width: 200px; width:200px;">
{{ props.item.description }}
</td>
<td style="min-width: 50px; max-width: 50px; width:50px;" class="text-xs-right">
{{ props.item.orderQuantity }}
</td>
<td style="min-width: 50px; max-width: 50px; width:50px;" class="text-xs-right">
{{ props.item.unitPrice | currency }}
</td>
</template>
<template v-slot:no-data>
<v-alert :value="displayNoDataMessage" color="error" icon="warning">
There are no orders
</v-alert>
</template>
</v-data-table>
</v-card>
</v-flex>
</v-layout>
安装了 `vue2-filters` 和 `vue-moment` 的 npm 包后,您只需在 `main.js` 启动代码中导入这些包并将其注册到 Vue 实例。
//
// maint.ts
//
import Vue from 'vue';
import Vue2Filters from "vue2-filters";
import VueMoment from "vue-moment";
Vue.use(Vue2Filters);
Vue.use(VueMoment);
TypeScript 兼容性和类型定义文件
因此,在示例应用程序的原生 JavaScript Vue.js 版本中使用 vue 过滤器和 moment.js 包和功能一切顺利。然后我开始处理此项目的 TypeScript 版本,并发现并非所有 Vue.Js 的内容都对 TypeScript 有良好的支持。TypeScript 对 Vue.js 生态系统来说仍然很新。为了让 TypeScript 执行类型检查,类型需要在某个地方定义。这就是 `type definition files`(类型定义文件)发挥作用的地方。它们允许您为本质上不是静态类型的 JavaScript 代码提供类型信息。此类文件的文件扩展名为“`.d.ts`”,其中 `d` 代表 `definition`(定义)。类型定义文件使得能够享受类型检查、自动完成和成员文档的好处。尽管类型定义文件非常有用,但创建它们需要大量时间。幸运的是,大多数 npm 包和库都带有 TypeScript 的类型定义。不幸的是,vue2-filters 和 vue-moment npm 包都不带类型定义文件。幸运的是,您有办法解决没有 TypeScript 定义文件的包。一种方法是创建自己的定义文件,或者找到别人可能已创建的。对于示例应用程序,我决定以不同的方式解决 TypeScript 类型定义问题。要使用 vue-moment 包,我只需使用 `require` 语句将其注册,而不是像这样导入它:
Vue.use(require("vue-moment"));
require 语句将动态加载 vue-moment 模块,从而绕过 TypeScript 类型检查器。
自定义全局过滤器
Vue.js 允许您定义过滤器,这些过滤器可用于应用常见的文本格式化。作为对 vue2-filters 包的解决方案,我决定为示例应用程序需要的货币格式化功能创建一个自定义全局过滤器。查看 vue2-filters 的 npm 包,我能够提取提供货币格式化的 JavaScript 函数,并将其作为自定义全局过滤器添加到示例应用程序中。下面的 `filters.ts` 文件实现了全局自定义货币过滤器。
import Vue from "vue";
//
// currency filter - filters.ts
//
Vue.filter("currency", (value: any, symbol: string, decimals: number, options: any) => {
var thousandsSeparator: any,
var symbolOnLeft: any,
var spaceBetweenAmountAndSymbol: any;
var digitsRE: any = /(\d{3})(?=\d)/g;
options = options || {};
value = parseFloat(value);
if (!isFinite(value) || (!value && value !== 0)) {
return "";
}
symbol = symbol != null ? symbol : "$";
decimals = decimals != null ? decimals : 2;
thousandsSeparator =
options.thousandsSeparator != null ? options.thousandsSeparator : ",";
symbolOnLeft = options.symbolOnLeft != null ? options.symbolOnLeft : true;
spaceBetweenAmountAndSymbol =
options.spaceBetweenAmountAndSymbol != null
? options.spaceBetweenAmountAndSymbol
: false;
var stringified: any = Math.abs(value).toFixed(decimals);
stringified = options.decimalSeparator
? stringified.replace(".", options.decimalSeparator)
: stringified;
var _int: any = decimals
? stringified.slice(0, -1 - decimals)
: stringified;
var i: any = _int.length % 3;
var head: any =
i > 0
? _int.slice(0, i) + (_int.length > 3 ? thousandsSeparator : "")
: "";
var _float: any = decimals ? stringified.slice(-1 - decimals) : "";
symbol = spaceBetweenAmountAndSymbol
? symbolOnLeft
? symbol + " "
: " " + symbol
: symbol;
symbol = symbolOnLeft
? symbol +
head +
_int.slice(i).replace(digitsRE, "$1" + thousandsSeparator) +
_float
: head +
_int.slice(i).replace(digitsRE, "$1" + thousandsSeparator) +
_float +
symbol;
var sign: any = value < 0 ? "-" : "";
return sign + symbol;
}
);
Vue.js Mixins
在示例应用程序中,我想要做的一件事是,每当用户启动应用程序、刷新浏览器窗口(F5)或切换到不同路由时,始终更新用户的当前状态。为此,我需要一段全局代码来执行以更新应用程序的 Vuex store。这时就轮到 Vue.js Mixins 了。Mixins 是一种灵活的方式,用于为 Vue 组件分发可重用功能。Mixin 对象可以包含任何组件选项。当组件使用 mixin 时,mixin 中的所有选项都将被“混合”到组件自身的选项中。当 mixin 和组件本身包含重叠的选项时,它们将使用适当的策略进行“合并”。在下面的代码中,每当触发 `created` 生命周期事件时,都会创建一个全局 mixin。当触发 created 事件时,mixin 将检查本地存储中的用户 JSON Web Token (JWT),并确定用户的令牌是否已过期。如果令牌已过期,Vuex store 将通过 `dispatch` 语句进行更新,并且用户的状态将被设置为未认证状态;这实际上是将用户从应用程序中注销。由于 Vue.js 中的一切都被视为组件,包括共享组件和所有其他视觉组件,因此 created 事件将在每个页面上触发数次。我只想在 vue 组件页面被路由到并创建时检查用户的令牌;因此,我在 mixin 中添加了一个检查,以查看事件是否在 vue 页面中触发。这是一个很好的例子,说明如何创建全局 mixin。当然,所需的功能也可以在路由更改时使用 `foreach` 事件钩子来实现。
//
// mixin for user authenication on page routing
//
Vue.mixin({
created(): void {
let vm: any = this;
if (vm._self.$vnode.data.routerView !== undefined) {
let user: UserInformationViewModel = new UserInformationViewModel();
let token: any = localStorage.getItem("VueToken");
if (token === null || token === undefined) {
user.emailAddress = "";
user.firstName = "";
user.lastName = "";
user.id = "";
user.isAuthenicated = false;
store.dispatch("updateUser", user);
} else {
let jwt: any = JSON.parse(atob(token.split(".")[1]));
let currentTime: number = Date.now() / 1000;
if (jwt.exp < currentTime) {
user.emailAddress = "";
user.firstName = "";
user.lastName = "";
user.id = "";
user.isAuthenicated = false;
store.dispatch("updateUser", user);
}
}
}
}
});
当用户刷新浏览器(F5)或在浏览器中重新打开应用程序时,可以在 `main.ts` 文件中解析用户的令牌并更新应用程序 store,如下所示:
//
// user authenication on application start-up/refresh - main.ts
//
let user: UserInformationViewModel = new UserInformationViewModel();
user.emailAddress = "";
user.firstName = "";
user.lastName = "";
user.id = "";
user.isAuthenicated = false;
let token: any = localStorage.getItem("VueToken");
if (token == null || token === undefined) {
user.isAuthenicated = false;
} else {
let jwt: any = JSON.parse(atob(token.split(".")[1]));
user.emailAddress = jwt.emailAddress;
user.firstName = jwt.given_name;
user.lastName = jwt.nameid;
user.id = jwt.primarysid;
user.isAuthenicated = true;
let currentTime: number = Date.now() / 1000;
if (jwt.exp < currentTime) {
user.isAuthenicated = false;
}
}
store.dispatch("updateUser", user);
尽管 JSON Web 令牌是 `base64 编码` 的,但任何人仍然可以解码它们。重要的是不要在这些令牌中存储任何可能危及应用程序安全的关键或私有信息。
生产部署
单页应用程序通常由数十甚至数百个组件组成,这些组件可以分成几个 JavaScript 包文件。使用打包器构建应用程序时,JavaScript 包可能会变得相当大,从而影响页面加载时间。如果我们能将几个相关的组件拆分成一个单独的包,并在访问与组件相关的路由时才加载它们(`lazy-load`),那将更有效。结合 Vue 的 `async component loading`(异步组件加载)和 webpack 的 `code splitting feature`(代码分割功能),可以轻松地基于路由进行组件的懒加载。
在示例应用程序中,路由配置了 `webpackChunckName`,webpack 打包器将使用它来创建单独的包。
// router.ts
{
path: "/shoppingcart",
name: "shoppingcart",
component: () =>
import(
/* webpackChunkName: "orders" */ "./views/shoppingcart.component.vue"
),
meta: { requiresAuthorization: true }
}
要为生产部署创建打包的 JavaScript 文件,并对 JS/CSS/HTML 进行最小化处理,您可以简单地执行 Vue.js CLI 的构建命令,该命令将编译应用程序并调用 `webpack bundler`。
npm run build
webpack 构建的输出默认将进入项目内的 dist 文件夹。下面的列表是构建示例应用程序并为 Vue.js 路由器配置中定义的 `products` 和 `orders` 创建预定义包的结果。
File Size Gzipped dist\js\chunk-vendors.132c74f3.js 829.55 KiB 207.68 KiB dist\js\orders~products.7efe385e.js 68.99 KiB 17.35 KiB dist\js\app.0eed3d02.js 29.69 KiB 8.35 KiB dist\js\products.cf0343bb.js 16.21 KiB 4.37 KiB dist\js\orders.77b1fed7.js 14.09 KiB 3.46 KiB dist\precache-manifest.8c24b13f01c7e.js 1.16 KiB 0.40 KiB dist\service-worker.js 0.95 KiB 0.54 KiB dist\css\chunk-vendors.587a5dd2.css 137.14 KiB 18.01 KiB dist\css\orders~products.c472f02d.css 29.67 KiB 4.74 KiB dist\css\products.0aa943ff.css 2.75 KiB 0.64 KiB dist\css\app.2db963e6.css 0.45 KiB 0.28 KiB dist\css\orders.dd9178be.css 0.36 KiB 0.21 KiB
Vue.js 扩展包
凭借其对 TypeScript 和 Chrome Debugger 等开发工具的内置支持,Visual Studio Code 已成为 Microsoft 社区中 JavaScript 和 TypeScript 项目的事实上的代码编辑器。为了让事情变得更好,有许多 Visual Studio Code 的扩展可以帮助 Vue.js 开发获得出色的体验。如果您正在 Visual Studio Code 中开发 Vue.js 应用程序,您将需要从 Visual Studio Marketplace 下载 Vue VS Code Extension Pack。此扩展包包含一套用于在 Visual Studio Code 中处理 Vue 应用程序的扩展。该扩展包包含 `Vetur`,并对 .vue 文件提供完全支持。Vetur 包括语法高亮、代码片段、linting、错误检查、格式化、自动完成和调试。在扩展包中,您还可以找到一个名为 `Prettier` 的代码格式化程序,它可以自动格式化您的代码。扩展包中大约有二十个扩展功能,它们将增强您的 Vue.js 开发体验。
Vue DevTools 浏览器扩展
在复杂的 JavaScript 代码中使用 `console.log` 语句仍然是调试浏览器中 JavaScript 代码的一种流行且有用的方法。为了增强浏览器中应用程序的调试,Vue 有一个专门的插件可以帮助您更有效地调试和开发应用程序。Vue DevTools 是 Chrome 和 Firefox 的一个扩展,用于调试 Vue.js 应用程序。安装后,可以通过浏览器的开发者工具(F12)面板,然后进入 Vue 选项卡来访问 Vue DevTools。
Vue DevTools Chrome 和 Firefox 扩展的一些功能包括:
- **实时编辑组件数据**——Vue DevTools 的一个非常方便的功能是实时编辑组件数据。这使您可以快速测试组件的不同变体。
- **使用时间旅行调试 Vuex**——Vue DevTools 与 Vuex 状态管理库无缝集成,并使您可以轻松地在 Vuex 状态对象的先前版本之间循环。这允许您进行所谓的 `time-travel debugging`(时间旅行调试)。时间旅行调试是一个通过应用程序数据回溯时间的过程,以了解执行过程中发生了什么。
- **跟踪应用程序的自定义事件**——如果您在应用程序中使用事件,您可以在 Vue DevTools 的事件选项卡中跟踪它们。
摘要
开发人员经常将 Vue.js 描述为 Angular 和 React 的结合体,其相似性都融入到框架中。Vue.js 和 React 都使用类似的状态管理库,例如 Vuex 和 Redux。React 和 Vue.js 都是单向的。这意味着数据只从父组件流向子组件。与 Angular 相比,Vue.js 是一个更灵活、不那么固执的解决方案。这允许您按照自己的方式构建应用程序,而不是被迫以 Angular 的方式做事。与 Angular 的模块配置和声明要求相比,Vue.js 的上手过程要简单得多。当然,如果您正在开发一个大型的关键任务应用程序,并且有一个大型开发团队,那么像 Angular 这样有大型公司支持、拥有庞大生态系统的、具有指导性的完整框架可能仍然是最佳选择。在将示例 Vue.js 应用程序用 TypeScript 重写后,Vue.js 代码库开始看起来非常像 Angular 2 应用程序。一旦应用程序运行起来,Vue.js 和 Angular 之间的界限就会开始变得模糊,尤其是在使用 TypeScript 时。就像宗教一样,技术选择通常基于您从小接触到的东西。最终,这完全取决于您自己的偏好。好消息是:我看不出 Vue.js 或 Angular 会成为下一个 Silverlight,一个从未真正被接受的被抛弃的技术。为了完成“三大”SPA 框架的鼎盛时期,我的下一篇 codeproject.com 文章将深入探讨 React。敬请关注。
先决条件和运行示例应用程序
示例应用程序的后端是一个使用 MongoDB 的 .NET Core Web API 应用程序。在 Vue.js、.NET Core 和 MongoDB 之间,有许多需要安装和配置的组件才能使示例应用程序正常运行。示例应用程序包含两个 Visual Studio Professional(2017 或 2019)项目和两个 Visual Studio Code 项目——一个用于原生 JavaScript Vue.js 版本的 Visual Studio Code 项目,以及一个用于 TypeScript 版本的 Visual Studio Code 项目。为了尽可能轻松地在本地开发环境中运行示例应用程序,我在下面概述了运行它所需的先决条件和安装步骤。
软件安装先决条件
- MongoDB 4.0 或更高版本
- Visual Studio 2017 或 2019 Professional 或 Community Edition
- Visual Studio Code
- .NET Core 2.2 - SDK 版本 2.2.106
- NodeJS 10.13.0 或更高版本
- Vue.js CLI 3
要运行示例应用程序,需要执行以下步骤:
- **安装 MongoDB 服务器作为服务**——从其下载页面下载 MongoDB Community Server 版本 4.0。从 4.0 版本开始,您可以在安装过程中将 MongoDB 安装并配置为 Windows 服务,并在安装成功后启动 MongoDB 服务。MongoDB 使用安装目录 bin 中的配置文件进行配置。
- **可选地将 MongoDB 独立服务器转换为副本集**——在购物车应用程序中下订单需要 MongoDB 事务支持。如果您想看到此功能正常运行;则需要支持 MongoDB 事务。要支持事务,请按照我之前文章《使用 .NET Core 2 对 MongoDB 进行单元测试》中提到的说明,将 MongoDB 安装转换为副本集。
- **下载示例应用程序源代码**——示例应用程序的源代码可以从本文顶部的下载源代码链接下载。有三个下载链接,一个用于使用 TypeScript 的 Vue.js 前端,另一个用于示例应用程序的原生 JavaScript 版本。第三个链接将下载 MongoDB 和 .NET Core 后端 Web API 应用程序。
- **.NET Core 2.2**——当您下载并安装 Visual Studio 2017 或 2019(Professional 或 Community Edition)时,.NET Core 2.2 应该会自动安装。如果您已有 Visual Studio,可以通过转到“工具”菜单并选择“获取工具和功能”来验证您的安装,这将启动 Visual Studio Installer。从安装程序选项中,您可以验证 .NET Core 2.2 已安装。如果您需要下载 Visual Studio 的 .NET Core SDK;请从 .NET Core SDK 下载页面选择 Windows 64 位操作系统使用的 SDK 版本 2.2.106。
- **Vue.js CLI 3**——Vue.js 前端应用程序通过 Vue.js CLI 版本 3 构建和提供服务。您可以通过运行 Vue.js CLI 命令来验证您的 Vue.js CLI 安装: vue --version。要安装最新版本的 Vue CLI,请执行:npm `install -g @vue/cli`。Vue CLI 需要 Node.js 版本 8.9 或更高版本(建议 8.11.0+)。安装 Vue CLI 也会安装最新版本的 Vue.js。
- **构建和运行示例应用程序 .NET Core Web API 项目**——为了验证所有组件是否已正确安装,请编译示例应用程序的 Web API 项目 `CodeProject.Mongo.WebAPI`。在打开和构建这些项目时,请务必等待一到两分钟,因为 Visual Studio 在打开项目时需要恢复编译这些项目所需的包。
- **SSL**——Web API 项目已配置为使用 SSL。为避免 SSL 问题,您需要尝试通过选择 `IISExpress` 配置文件并选择运行按钮来运行项目,ASP.NET Core 将创建一个 SSL 证书。根据您的开发计算机,Visual Studio 可能会询问您是否要信任 ASP.NET Core 生成的自签名证书。选择“是”信任该证书。由于 Visual Studio 就是 Visual Studio,您可能需要运行项目一到两次,或者退出并重新加载 Visual Studio 以确认项目一切正常。从 Visual Studio 运行项目时,浏览器应启动并在浏览器中显示来自 Values 控制器的输出。
- **构建和运行 Seed 程序**——为了用示例应用程序的测试数据填充 `Products` 集合,请构建并运行 .NET Core 控制台应用程序 `CodeProject.Mongo.Import`。此程序还将创建支持示例应用程序功能所需的索引。
- **构建和提供 Vue.js 前端应用程序**——Vue.js 前端应用程序的两个版本都依赖于安装在项目 `node_modules` 文件夹中的 node 模块。通过在应用程序的根文件夹的 DOS 命令窗口中执行 `npm install`,可以创建所有 node 模块。安装完包后,您可以在 DOS 命令窗口中使用 Vue.js CLI 构建 Vue.js 应用程序项目,方法是执行:`npm run serve`。一旦 Vue.js 前端应用程序构建完成,webpack 将在后台启动一个 Node.js Express Web 服务器。
- **故障排除与修复 Node.js 和 npm 问题**——似乎 Node.js 和 Node 包管理器 (npm) 时不时地会出现奇怪的问题。这些问题有时可以通过简单地运行 `npm cache clean` 或使用 `-verbose` 选项运行 npm install 来查看更多详细信息来解决。作为最后的手段,您可以卸载 Node.js 并删除 `%APPDATA%` 文件夹中的 `npm` 和 `npm-cache`,然后重新安装最新版本的长期支持 (LTS) Node.js。
- **启动示例应用程序**——要运行示例应用程序,请在浏览器中输入 `https://:8080`。