65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular 进行网页表单验证

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023年1月3日

MIT

20分钟阅读

viewsIcon

12075

downloadIcon

111

使用 Angular 框架进行表单字段验证的几种方法

引言

各位 CodeProject 的读者们,大家好。这是我 2022 年的最后一篇教程。这是很棒的一年。我非常感谢大家阅读我的教程。对于这篇教程,我希望它简单而实用。在过去几年里,我学到的最有用的技能之一就是如何用 AngularJS 进行表单验证。Angular 2 及以上版本在语法和用法上是一种完全不同的东西。但我的研究表明,在表单验证方面,适用于 AngularJS 的方法同样也适用于基于 Angular 的应用程序。在本教程中,我将讨论几种使用 Angular 框架进行表单字段验证的技术。正如你将看到的,这些技术不仅易于实现,而且在 Web 应用程序设计中也极其强大。

什么是表单验证?对我而言,它是在表单数据提交到后端服务器之前验证表单上字段的任务。对于 Angular 和 AngularJS 应用程序,我们已经超越了传统的表单提交方式(Web 1.0 和 1.5 时代的产物)。大多数客户端验证可以直接在网页上完成,无需与后端服务器进行往返通信。客户端验证是通过 JavaScript 或任何基于 JavaScript 的框架完成的。对于 AngularJS,验证和在页面上显示错误是内置的。但它用起来不是最容易的。Angular 框架使这一点变得更加复杂。其原因是为了给每个开发人员提供为他们的应用程序设计机制的方法。但并非每个功能都是必不可少的。我相信 AngularJS 能做到的,Angular 应用程序也能做到。本教程将讨论这种方法。我以前的做法就是我现在想做的。我相信这是解决问题的最佳方式,除非它非常笨拙以至于我必须学习一种新的方式。

整体架构

本教程的示例应用程序是一个纯 Node.js 应用程序,没有 Java 或 .NET 后端。该应用程序是一个单页应用程序。在应用程序内部,只有一个索引页面——一个有很多字段的表单,以及两个按钮,一个是 保存(Save),一个是 清除(Clear)。字段验证全部在页面本身上进行。这可能是我唯一一篇没有包含 Java 或 .NET 后端应用程序的教程。它们(Java 或 .NET 应用程序代码)是不需要的。这个应用程序所需要的只是页面端的验证。并且该应用程序可以在一个基于 Node.js 的 Web 服务器中运行。

该应用程序开发使用了 Angular 框架(版本 14.1.0)。我还引入了 Bootstrap(版本 5.2.0)和 JQuery(3.6.0)。其他框架则不需要。有两种方法可以添加 Bootstrap 和 JQuery。简单的方法是使用 Angular CLI 命令“ng add”。我不知道这个命令是如何工作的,因为我没有用它来做。第二种是比较困难的方法,找到应用程序项目的配置文件,并通过修改这些配置文件来添加额外的框架。我必须修改的两个文件是 package.jsonangular.json。如果你对这是如何完成的感兴趣,可以查看这两个文件。修改 package.json 是为了包含 Bootstrap 和 JQuery 的依赖项。修改 angular.json 则有点难理解。它实际上会将这些依赖项包含到应用程序中,这样当浏览器加载应用程序时,它也会加载这些依赖项。没有这一步,这些框架将不会被加载,应用程序也无法按预期工作。

我将在这个示例应用程序中演示三种不同类型的验证:

  • 检查必填字段并显示相关的错误信息。
  • 检查字段值的有效性并显示相关的错误信息。
  • 动态转换字段值,并阻止输入无效字符。

这些可以通过内置的表单验证机制和字段事件处理来完成。就像我说的,我想用最熟悉、最简单的方法来执行最强大的输入验证处理。闲话少说,让我们开始实际的教程吧。

Angular 表单验证操作指南

我在本教程中采用的方法,是我认为最简单的表单验证方法。它类似于使用 AngularJS 进行表单验证的方式。AngularJS 的做法是,表单名称和字段名称可以在 AngularJS 代码中用作对象。我学到的方法是,我可以将表单上的所有字段设置为“dirty”(脏)状态,这将立即暴露所有必填但未输入值的字段。每个字段都会有一个最初不可见的子标签,但当字段出现错误时,该标签就会显示。对于实时值验证,我可以使用“blur”(字段失去焦点)事件处理来验证字段,如果无效,相关的错误标签可以立即显示。同样的事件也可以用于字段显示值的转换。最后,对于实时字段值验证,我使用值改变事件处理来完成。同样的原则也可以用于 Angular 应用程序。在接下来的几个小节中,我将描述每种机制是如何工作的。

在表单提交时以编程方式强制进行表单验证

对于 Angular,要使表单成为一个对象,或使表单的字段成为可供 JavaScript 访问的对象,有几种方法可以做到。最熟悉的方法(与 AngularJS 相比)是在表单本身上声明表单对象和所有字段为对象。我是这样处理表单的:

<form novalidate #formSignUp="ngForm">
...
...
...
</form>

属性 novalidate 是为了设置表单不通过 HTML5 内置功能显式验证表单字段。这是必要的,因为没有它,浏览器将进行验证并显示内置的错误提示。如果我想自定义错误显示,就必须移除浏览器的默认行为。接下来,我声明一个名为 #formSignUp 的变量对象,并将表单对象赋给这个变量。

问题是,为什么这是必要的?表单字段只有在它们是“dirty”(脏)状态且字段无效/有错误时,才会显示错误信息和输入框周围的红色矩形。当表单没有输入任何数据,用户点击“注册(Sign up)”按钮时,表单仍处于“pristine”(原始)状态,所以不会显示任何错误。这是一个 bug,修复的方法是在按钮点击事件处理程序中编写代码,强制将表单设为“dirty”,并使所有字段也变为“dirty”。这将触发表单对字段的内置验证,例如必填字段验证、最大长度验证和电子邮件地址值验证等。这些验证还会在单个字段和表单本身上设置错误/无效状态。我把这样的机制放在一个通用的服务类中:

import { Injectable } from '@angular/core';

@Injectable({
   providedIn: 'root',
})
export class FormsService {
   public makeFormFieldsClean(formToCheck: any):void {
      if (formToCheck != null) {
         Object.keys(formToCheck.controls).forEach(key =< {
            formToCheck.controls[key].markAsUntouched({});
            formToCheck.controls[key].markAsPristine();
         });
      }
   }
   
   public makeFormFieldsDirty(formToCheck: any):void {
      if (formToCheck != null) {
         Object.keys(formToCheck.controls).forEach(key =< {
            formToCheck.controls[key].markAsDirty();
         });
      }
   }
}

在这个服务类中,我定义了两个方法,一个叫做 makeFormFieldsClean()。我稍后会讲到这个方法。我想指出的是 makeFormFieldsDirty() 方法。这个方法会强制表单对象中的所有字段变为“dirty”状态。它只是一个 `for` 循环,遍历表单的所有子控件,并调用控件的 markAsDirty() 方法。现在,回到这个服务类中的第一个方法。基本上,这个方法将字段的状态恢复为未触摸/原始(untouched/pristine)状态。这将隐藏所有的错误。当用户点击“清除(Clear)”按钮时,将调用此方法。在所有值被清除后,此方法将强制表单中的每个字段变为原始/未触摸状态,所有错误都将消失。

让我们举一个简单的例子。在示例应用程序中,我们有一个名为“姓名(Name)”的字段。它用于存放用户注册时的真实姓名,并且是一个必填字段。我设置这个字段的方式如下:

<div class="row mt-2">
   <div class="col-xs-12 col-sm-6">
      <label for="signup_UserRealName">Name</label>
      <input class="form-control"
             type="text"
             id="signup_UserRealName"
             name="userRealName"
             [(ngModel)]="userRealName"
             [ngClass]="{'invalid-input': (usrRealName.dirty || 
             usrRealName.touched) && usrRealName.invalid}"
             required
             maxlength="80"
             #usrRealName="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrRealName.dirty || usrRealName.touched) && 
            usrRealName.errors?.['required']">Required</span>
   </div>
</div>

我用粗体字体突出了这个字段中重要的部分。关键字 required 将此字段设置为必填字段。如果此字段没有输入值,当字段被设置为 dirty 时,浏览器将自动对此字段进行验证。这一部分 #usrRealName="ngModel" 是声明一个包含该字段对象的局部变量。这样做有两个必要的原因。第一,看声明的最后一部分,那里使用了 ngIf,它需要字段对象名称来检查错误状态,以便隐藏/显示错误。第二,这个变量可以传递给 Angular 方法,并用于以编程方式操作输入字段。上面 HTML 标记的最后一部分是带有“Required”错误的 span 元素。这个元素只有在输入字段被触摸过(touched)或变脏(dirty)并且具有“required”错误状态时才会显示。你可能会问,为什么我不能只在它有必填字段错误但值未输入时就显示它?原因是我们不那样做,因为这样一来,错误会在表单一显示时就出现,并且当用户点击“清除(Clear)”按钮时它也不会被移除。最好是包含对 dirtytouched 状态的检查,这样当字段未被修改甚至未被触摸时,错误就不会显示出来。

正如我之前提到的,如果用户根本没有触摸该字段,错误就永远不会显示。错误必须被强制显示出来。这是我为“注册(Sign up)”按钮事件处理编写的代码:

import { FormsService } from '../common/forms.service';
...
...
public onClickSaveChange(formToValidate: any):void {
   this._formsService.makeFormFieldsDirty(formToValidate);

   if (formToValidate.valid === true) {
      alert("User sign up is successfully.");
   }
}

这个方法非常简单。它强制表单的所有字段变为“dirty”,这反过来会自动对字段进行 HTML5 验证。这样一来,如果上述字段没有输入值,红色的“required”标签就会显示出来。在代码片段的顶部,我导入了表单服务类的类型,正如我之前展示的,是 makeFormFieldsDirty() 方法会强制所有字段进行验证。如果你忘记了这段代码是如何工作的,请回顾一下。

要清除错误,使表单没有任何错误显示,这个功能是在“清除(Clear)”按钮的点击事件处理方法中完成的。这是该方法:

public onClickClear(formToValidate:any):void {
   this.clearAllViewData();
   this._formsService.makeFormFieldsClean(formToValidate);
}

在我清除了表单字段的所有值之后,我使用了表单服务对象并调用了 makeFormFieldsClean() 方法,将所有字段设置为干净和原始(clean and pristine)状态。这应该会移除所有的错误显示。makeFormFieldsClean() 方法的代码在前面已经列出,如果你忘了它是如何工作的,请回顾一下。

这是整个验证设置能正常工作的最核心部分,也是最困难的部分。现在我们已经克服了它,向这个示例应用程序添加更多功能就会变得更容易。接下来,我将展示如何为用户交互添加动态验证。

用户交互的动态字段验证

在这一小节中,我将把验证机制提升一个档次。这个表单是为用户注册准备的。有一个“用户名”字段。我想模拟这样一种情况:用户输入一个用户名,该用户名值必须与后端数据存储库进行核对,看该用户名是否已被占用。如果用户名已被占用,则应显示错误信息。

我想展示的另一个验证是值验证。基本上,用户名只能包含特定的字符。如果用户名包含不允许字符集之外的字符,则会显示无效用户名的错误。除了这两个验证之外,该字段还是一个必填字段。

这是用户名输入字段的定义:

<div class="row">
   <div class="col-xs-12 col-sm-8 col-md-8">
      <label for="signup_UserName" class="form-label">User Name</label>
      <input class="form-control"
               type="text"
               id="signup_UserName"
               name="userName"
               [(ngModel)]="userName"
               [ngClass]="{'invalid-input': 
               (usrName.dirty || usrName.touched) && usrName.invalid}"
               (blur)="checkUserName(usrName)"
               required
               maxlength="60"
               #usrName="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && 
            usrName.errors?.['required']">Required</span>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && 
            usrName.errors?.['invalid_username']">Invalid user name</span>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && 
            usrName.errors?.['duplicated_name']">Try a different user name</span>
   </div>
</div>

我高亮了这段代码片段的重要部分。最重要的部分是 blur 事件的处理。当字段失去焦点时,即另一个字段获得焦点时,blur 事件就会发生。这是一个强大的机制,可以用于许多功能,比如执行验证、值转换或许多其他不同的操作。在这种情况下,事件处理将执行两个操作:

  • 检查用户名是否已被占用。如果已被占用,则设置选择其他用户名的错误。
  • 检查用户名是否包含任何不允许的字符。如果存在不允许的字符,则设置无效用户名的错误。

这是我为 blur 事件设置事件处理的代码行:

<input 
...
...
(blur)="checkUserName(usrName)"
...
...
/>

这个方法调用接受一个参数,其值为 userName。这是包含用户名输入框对象的局部变量。你接下来会看到这个值是如何被使用的。下面的代码片段展示了用于用户名验证的两个方法:

public checkUserNameFieldValid(userNameField:any):void {
   if (this._userName != null && 
   this._userName.trim() !== "" && !this.checkUserNameValidity(this._userName)) {
      userNameField?.control?.setErrors({"invalid_username": true});
   }
}

public checkUserName(userNameField: any):void {
   this.checkUserNameFieldValid(userNameField);

   if (this._userName && this._userName.trim() !== "") {
      if (this._existingUserNames && this._existingUserNames.length > 0) {
         let i = 0;
         for (; i < this._existingUserNames.length; i++) {
            if (this._existingUserNames[i] &&
               this._existingUserNames[i].trim() !== "" &&
               this._existingUserNames[i].toLowerCase() === 
                                          this._userName.toLowerCase()) {
               userNameField?.control?.setErrors({"duplicated_name": true});
               break;
            }
         }
      }
   }
}

第一个方法 (checkUserNameFieldValid()) 检查用户名是否包含任何不允许的字符。如果检查发现有不允许的字符,高亮部分就是我如何为字段设置特定错误的方式:

...
userNameField?.control?.setErrors({"invalid_username": true});
...
}

我使用问号“?”的原因是,它可以首先确保对象变量不为 null,然后才能引用其后续属性或调用对象的方法。这是避免 null 引用异常的一个好方法。

为了检查用户名是否已被占用,我创建了一个数组,并放入了三个用户名:“testuser1”、“testuser2”和“testuser3”。输入的用户名将与这个列表进行比较,如果有任何匹配(不区分大小写),那么它就会在字段上设置错误:

...
   if (this._userName && this._userName.trim() !== "") {
      if (this._existingUserNames && this._existingUserNames.length > 0) {
         let i = 0;
         for (; i < this._existingUserNames.length; i++) {
            if (this._existingUserNames[i] &&
               this._existingUserNames[i].trim() !== "" &&
               this._existingUserNames[i].toLowerCase() === 
                                          this._userName.toLowerCase()) {
               userNameField?.control?.setErrors({"duplicated_name": true});
               break;
            }
         }
      }
   }
...

当你测试这个输入字段时,你所需要做的就是输入一些字符,然后用鼠标光标或“Tab”键从这个字段切换到下一个字段,验证就会执行。当你输入“testuser1”时,你会看到错误显示:请尝试其他用户名。

如果你输入的值是“test##$user!1”,验证将显示:用户名无效。

我用同样的方法处理了另外两个字段。密码字段会有一个密码值相等性验证。blur 事件处理附加在第二个密码字段上,如果两个密码不匹配,就会显示错误。另外,对于电子邮件地址字段,其值会根据一个正则表达式进行验证。如果模式匹配失败,也会显示错误。请注意,这个正则表达式模式很简单。它并不涵盖所有类型的有效电子邮件地址,因此该验证机制不能用于生产环境。

如果你输入一个有效值,比如 “shebanger_69”,将不会显示任何错误。

这还不错,可能没有最初设置验证的工作那么难。让我再把这个机制提升一个档次——使用自动值转换来移除无效字符并转换值的显示格式。

输入字段的值转换与清理

这里有一个新的场景,假设你有一个输入字段,你只想限制输入为数字字符。一种方法是将输入类型设置为 number。这种方法在功能上是有限的。如果该字段旨在显示美国地区的电话号码,那么该值可以被格式化为“(XXX) XXX-XXXX”。很明显,这种场景下使用 number 类型的输入是行不通的。所以我必须依赖两种不同的事件处理机制来:

  1. 阻止输入无效字符
  2. 转换用于显示和数据存储的值

其中一种机制是 blur 事件处理。另一种机制是值变化事件处理。值变化事件处理器用于进行值验证。对于电话号码,我只关心数字字符。所有其他字符都将被移除。每次输入字段中的值发生变化时,值变化事件都会被调用。这使得处理成为一个开销很大的操作。我选择使用这个事件处理来剥离无效字符的原因是,它会产生字符被输入后立即被移除的效果,这样用户就知道它是不被允许的,你可以用电话号码输入字段试试看这个效果。

blur 事件处理用于转换,即将电话号码转换为美国电话号码格式。如果我在值变化事件处理中这样做,那将是一个相当昂贵的操作。所以我只在焦点离开输入字段时才执行它。除了转换,我还从电话号码中剥离了所有非数字字符,然后将该值赋给另一个属性。这个值将被保存到数据库中(如果实现了保存到后端数据库的功能)。这是我对这两个事件处理方法的实现:

...

// for value changed event
public userPhoneNumFieldValueChanged():void {
   this._userPhoneNum = this._userPhoneNum.replace(/\D/g, '');
}

// for blur event
public userPhoneNumFieldBlurred():void {
   if (this._userPhoneNum != null && this._userPhoneNum.trim() !== "") {
      let temp_phNum:string = this._userPhoneNum.replace(/\D/g, '');
      temp_phNum = temp_phNum.substring(0, 10);
      if (temp_phNum.length >= 7) {
         this._userPhoneNum = "(" + temp_phNum.substring(0, 3) + ") " + 
         temp_phNum.substring(3, 6) + "-" + temp_phNum.substring(6);
      } else if (temp_phNum.length > 3 && temp_phNum.length < 7) {
         this._userPhoneNum = "(" + temp_phNum.substring(0, 3) + ") " + 
                                    temp_phNum.substring(3);
      } else if (temp_phNum.length >= 1 && temp_phNum.length <= 3) {
         if (temp_phNum.length < 3) {
            this._userPhoneNum = "(" + temp_phNum.substring(0);
         } else {
            this._userPhoneNum = "(" + temp_phNum.substring(0) + ")";
         }
      }
   }
   this._userPhoneNumVal = this._userPhoneNum.replace(/\D/g, '');
}
...

你可以看到这两个方法之间的区别,上面那个使用正则表达式来剔除任何不需要的字符。下面那个是一个复杂的方法,它将整个字符串切成三部分,然后重新排列成一个像“(XXX) XXX-XXXX”这样的新值。在值转换之后,我会再次进行剔除,并将纯数字的值赋给其他变量。

电话号码字段的定义如下:

<div class="row mt-2">
   <div class="col-xs-12 col-sm-6 col-md-5">
      <label for="signup_UserPhoneNum">Phone #</label>
      <input class="form-control"
               type="text"
               id="signup_UserPhoneNum"
               name="userPhoneNum"
               [(ngModel)]="userPhoneNum"
               [ngClass]="{'invalid-input': (usrPhoneNum.dirty || 
               usrPhoneNum.touched) && usrPhoneNum.invalid}"
               (change)="userPhoneNumFieldValueChanged()"
               (blur)="userPhoneNumFieldBlurred()"
               required
               maxlength="15"
               #usrPhoneNum="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrPhoneNum.dirty || usrPhoneNum.touched) && 
            usrPhoneNum.errors?.['required']">Required</span>
   </div>
</div>

如你所见,它是一个必填字段,并且只有一个错误标签。之所以不需要另一个错误标签,是因为 blur 和值变化事件处理应该能处理任何无效的输入字符。唯一可能发生的错误是用户根本没有输入任何值。

无论如何,同样的机制也应用于邮政编码字段。该字段的值全是数字。当用户输入值时,它将被格式化为美国格式“XXXXX-XXXX”。

就是这样!通过对表单字段事件处理的调整和变通,可以为表单输入构建任何类型的验证。我在本节中描述的这些应该能给你一个好的开始。在下一节中,我将讨论如何构建和运行这个示例应用程序。

如何运行和测试此示例应用程序

有趣的时间到了。你已经知道了这个示例应用程序中使用的所有技巧。是时候运行它,看看所有技巧的实际效果了。在你下载了 zip 格式的源代码后,将它解压到一个文件夹中。在运行任何东西之前,只有一个文件“karma.conf.sj”,请将其重命名为“karma.conf.js”。

接下来,检查你的系统,确保你安装了最新的 Node.js。我安装的是 v18.2.1。这个示例应用程序可能无法在 v10 上运行。所以如果可以的话,请安装最新版本。然后是 npm、Angular 和 Angular-CLI。

你必须安装所有必需的包。这是你需要运行的命令:

npm install 

安装包需要一些时间来下载。之后,你可能需要审计并修复任何有高危问题的包。运行这个命令:

npm audit fix 

在我使用的包版本中,有两个高危漏洞。我使用的这个设置是为了给另一个项目集成 ui-bootstrap 组件,所以目前我对这个问题还能接受。我相信以后可以修复它。

接下来,我们可以直接用 Angular-CLI 来运行这个应用。这是命令:

ng serve 

当它成功启动后,你会看到控制台显示如下输出:

要运行该应用程序,请使用浏览器并导航到以下端点:

https://:4200/ 

你将看到以下页面显示出来:

运行示例应用程序

你可以尝试几种不同的场景。第一种是,你可以滚动到底部并点击“注册(Sign up)”按钮,所有的输入字段都会亮起错误提示。输入字段会有一个红色的边框,并且下方会有一个红色的标签显示错误“必填(Required)”。这是一张截图:

当你点击“清除(Clear)”按钮时,所有的错误都会消失。接下来,试试用户名输入框,如果你输入“testuser2”并转到下一个输入框,你会看到错误提示“请尝试其他用户名”。

如果你将值更改为“testuser$#%^&1”,错误将变为“用户名无效”。一旦你更改为像“shebanger_69”这样的值,用户名就会是正确的。

对于用户电子邮件输入字段,这是一个有效值:“shebanger69@testmail.com”。而一个无效值会是:“shebanger_69@testmail.com”。同样,你可以通过切换到不同的输入字段来测试输入,你会看到错误显示。尽管两个邮箱都是有效的,但因为我在这里使用的规则会显示一个有效而另一个无效。正如我所说,电子邮件验证是不正确的,不能用于生产环境。

对于用户密码,请尝试为两个输入框输入不同的密码,你将看到错误:“密码不匹配”。

你最后想尝试的场景是邮政编码和电话号码。以电话号码为例,你可以尝试以下几个值:

  • 如果你输入1或2个数字字符,值的格式将是:“(X” 或 “(XX”。
  • 如果你输入3个数字字符,值的格式将是:“(XXX)”。
  • 如果你输入4到6个数字字符,值的格式将是:“(XXX) X”、“(XXX) XX”或“(XXX) XXX”。
  • 如果你输入7到10个数字字符,值的格式将是:“(XXX) XXX-X”、“(XXX) XXX-XX”、“(XXX) XXX-XXX”或“(XXX) XXX-XXXX”。

邮政编码也是一样。邮政编码的格式可以是5位数字,或者5位数字后跟一个破折号,再接着最多4位的扩展码。

摘要

教程到此结束。与我今年其他的作品相比,这篇内容不多。然而,在编写这篇教程和设计示例应用程序时,我获得了很多乐趣。正如我所预料的,基本的验证机制与 AngularJS 几乎相同,简单且可重用。

在我的示例应用程序中,我讨论了以编程方式触发表单验证和重置表单。它还包含了更高级的验证示例,例如验证输入值并在其无效时设置错误。最后,我加入了两个值转换为验证和显示目的的例子。这些都是一些非常简单的机制,你可以应用并用自己发明的复杂输入验证来扩展。只要你了解基础知识,更复杂的验证也可以通过多一些努力来实现。尽管前端验证功能强大,但它是不够的。想象一下,一个恶意用户使用 Postman 或其他工具来提交包含无效输入数据的 HTTP 请求,而不是通过前端网页,你的客户端验证对此类滥用是无用的。因此,你必须将相同的验证复制到后端服务器端实现中,这一点至关重要。你会惊讶地发现,有如此多的应用程序只做前端验证,而后端根本没有验证,为攻击者留下了巨大的安全漏洞。

感谢您阅读我的教程文章。这是 2022 年的最后一篇。对于 2023 年,我将推出更多关于 Angular 的教程,例如与 ui-bootstrap 及其他第三方组件的集成。我的目标是让自己成为一名 Angular 应用程序开发专家,并与这个社区分享更多中级和高级的设计方法。

圣诞快乐,新年快乐!

历史

  • 2022年12月25日 - 初稿
© . All rights reserved.