0%

Spring boot是什么?

可以将Spring boot理解为spring项目的脚手架,它会默认配置我们引入的第三方依赖模块(jar),我们无需再用繁琐的xml配置,也可以快速搭建spring项目。Spring boot还内嵌了web容器(默认tomcat),服务器上不再需要单独安装tomcat,只需运行jar包即可。非常适合开发现在流行的微服务,拆分独立业务。

Spring boot优点

  1. 降低开发成本。大大简化了项目搭建时xml配置。
  2. 降低运维成本。内嵌了tomcat,不再需要运维再搭建tomcat.
下面我们step by step搭建Spring boot+mybatis+freemarker项目,大家可以参考下前面不用spring boot的文章,对比下我们节约了多少时间。基于Intelij Idea 2017.1。
  1. 创建项目。

    File-New-Project 选择Spring Initializr
    屏幕快照 2017-04-09 上午11.03.49

    Next,输入项目信息:

    屏幕快照 2017-04-09 上午11.06.39

    Next,选择需要的依赖:Web,Freemarker,Mybatis,Mysql

    Next,可以改也可以不改项目位置,Finish,创建出一个新项目.

  2. 配置项目
    虽然Spring boot可以帮我们处理大部分配置,但仍有一些是需要我们自己处理的,比如,数据库,mybatis的mapper位置.

    创建成功后,项目的结构是这样的

    所有配置都在SpringBootDemoApplication和application.properties里,没有各种复杂的xml,Freemarker的模板放在resources/templates.
    打开application.properties,配置服务器端口(默认8080)和数据库

    #服务器配置
    server.port=8000
    
    #数据库
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8
    spring.datasource.username=root
    spring.datasource.password=
    

    打开SpringBootDemoApplication配置SqlSessionFactory:

    package com.pocketdigi;
    
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    
    import javax.sql.DataSource;
    
    @SpringBootApplication
    @MapperScan("com.pocketdigi.dao.mapper")
    public class SpringBootDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootDemoApplication.class, args);
        }
    
        @Bean
        @Autowired
        public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    
            final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.pocketdigi.dao.po");
            PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
            sqlSessionFactoryBean.setMapperLocations(pathMatchingResourcePatternResolver.getResources("classpath:mybatis_mapper/*.xml"));
            return sqlSessionFactoryBean.getObject();
    
        }
    }
    

    类上加上@MapperScan注解,增加创建 SqlSessionFactory的方法

    此时其实还没有com.pocketdigi.dao.mappercom.pocketdigi.dao.po包,这两个包是分别用来放mybatis generator生成的mapper接口文件和po,example类的,mybatis_mapper在resources目录下,放mybatis generator生成的mapper xml文件。请在生成后放到对应目录,怎么生成就不介绍了。

  3. 开始编写业务逻辑
    创建service,controller包,开始写业务逻辑吧
    完成时,整体项目结构大概是这样的:

    BeanConverter是一个将po转成model的工具类

  4. 运行
    正常情况下,intellij idea已经自动帮我们创建好了运行配置,只要点击工具栏上的运行按钮就可以了。

  5. 打包
    mvn package,在target目录生成spring-boot-demo-0.0.1-SNAPSHOT.jar,运行java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar即可启动服务

当项目变得复杂庞大以后,如果所有页面都在一个模块里,就会出现首页加载慢的问题,因为首页就已经把整个项目加载进来了。所以,很有必要根据业务将不同的功能分模块,以便Angular2按需加载,提升用户体验。

下面的例子是将首页放到home模块里,访问/home时加载home模块内容,仅供学习懒加载,其实首页访问路径应该是/

先看项目文件结构:

Angular 懒加懒

home模块放到src/app/home目录下,里面的home目录是home组件。
home模块有单独的定义和路由(home.module.ts,home-routing.module.ts)

创建home模块和home组件:

cd src/app/
mkdir home
cd home
ng g module home
ng g component home

创建home模块的路由配置模块

创建 home-routing.module.ts:
import {Routes, RouterModule} from "@angular/router";
import {HomeComponent} from "./home/home.component";
import {NgModule} from "@angular/core";

const routes: Routes=[
  {
    path:'',
    component:HomeComponent
  }
]

@NgModule({
  imports:[RouterModule.forChild(routes)],
  exports:[RouterModule],
  providers:[]

})
export class HomeRoutingModule{}

模块下的页面都可以单独在该模块自己的的路由配置模块上配置,而不用在app-routing.module.ts里配置,注意RouterModule.forChild(routes)

home.module.ts导入路由模块:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeComponent } from './home/home.component';
import {HomeRoutingModule} from "./home-routing.module";

@NgModule({
  imports: [
    CommonModule,
    HomeRoutingModule
  ],
  declarations: [HomeComponent]
})
export class HomeModule { }
在app-routing.module.ts配置路由:
import {NgModule} from "@angular/core";
import {Routes, RouterModule} from "@angular/router";
import {UserListComponent} from "./user/user-list/user-list.component";
import {UserDetailComponent} from "./user/user-detail/user-detail.component";
import {RxjsComponent} from "./rxjs/rxjs.component";
import {UserEditComponent} from "./user/user-edit/user-edit.component";
import {environment} from "../environments/environment";

const routes: Routes = [
  {
    path:'home',
    loadChildren:'app/home/home.module#HomeModule'
  }

];

@NgModule({
  imports: [RouterModule.forRoot(routes,{ useHash: environment.useHash })],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

配置home路径,使用loadChildren加载home模块

完成后打开chrome的开发者工具,切到Network,看看不同的页面是不是加载了不同的文件。

整合Angular2的问题在于,修改前端源码后,需要即时生效,总不能每次修改都ng build吧,效率太低。

ng build支持-w参数,加上后可以一直运行,检测文件变化后重新build,我们就利用这个特性来配置idea。当然,如果你不嫌麻烦,可以在每天写代码前先在终端里执行ng build -w

先看配置好后的项目结构:
项目结构

angular开发目录在src/main目录下,其实这个目录放哪都行,因为这里不是最终执行的代码。为了保护源代码,不建议放到webapp目录下。

dist目录才是angular编译生成的最终代码,访问路径 http://localhost:8080/dist/

配置步骤

  1. 先配置SpringMVC环境,或者可以直接下载SpringMVCModuleDemo ,在Intellij Idea中打开。

  2. 打开终端,切到web/src/main目录下
    执行ng new angular --routing --skip-install
    routing参数是自动创建路由模块,skip-install是不自动执行npm install下载依赖模块,因为用淘宝cnpm更快。

  3. 切到刚创建的angular目录,执行cnpm install下载依赖

  4. 编辑 angular目录下的package.json,添加两个script,develop和release:

    {
      "name": "angular",
      "version": "0.0.0",
      "license": "MIT",
      "angular-cli": {},
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "test": "ng test",
        "lint": "ng lint",
        "e2e": "ng e2e",
        "develop":"ng build -op ../webapp/dist -bh /dist/ -w",
        "release":"ng build -op ../webapp/dist -bh /dist/ -prod -aot --env=prod"
      },
      ...
      }
    

    develop用于开发过程中,自动监控文件变化,一旦有文件修改,自动rebuild,覆盖webapp/dist目录下生成的文件。

    release用于正式发布,build时启用aot,可大幅缩小代码体积。同时使用environments/environment.prod.ts里的配置,切换到正式环境

  5. 添加运行配置
    Run/Debug Configurations(Run-Edit Configurations)窗口,添加一个npm配置:

    npm

    release只有发布时才使用一次,加不加都无所谓

  6. 修改路由策略
    Angular路由默认使用了PathLocationStrategy,使得路径看起来像是访问真正存在服务器上的文件,但是这个模式需要web容器将所有文件都转发到index.html上,我目前没找到jetty的相关配置。所以,在开发阶段,暂时使用HashLocationStrategy这种兼容模式,路径用#隔开。
    配置app-routing.module.ts:

    import {NgModule} from "@angular/core";
    import {Routes, RouterModule} from "@angular/router";
    import {UserListComponent} from "./user/user-list/user-list.component";
    import {UserDetailComponent} from "./user/user-detail/user-detail.component";
    
    const routes: Routes = [
      {
        path: 'user/list',
        component:UserListComponent,
        children: []
      },
      {
        path:'user/:id',
        component:UserDetailComponent,
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes,{ useHash: true })],
      exports: [RouterModule],
      providers: []
    })
    export class AppRoutingModule { }
    

    重点在RouterModule.forRoot{ useHash: true }

  7. git忽略dist目录
    因为dist里所有文件都是Angular生成的,我们不需要维护到git上,在
    .gitignore里将dist目录加上,添加一行:

    /web/src/main/webapp/dist/
    

    如果之前已经跟踪之个目录,执行下面的命令取消:

    git rm --cached -r web/src/main/angular/
    
  8. 使用maven插件执行angular build
    在web模块的pom.xml里,使用exec-maven-plugin插件构建angular代码

     <build>
            <finalName>web</finalName>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>1.5.0</version>
                    <executions>
                        <execution>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>exec</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <executable>cnpm</executable>
                        <workingDirectory>${basedir}/src/main/angular
                        </workingDirectory>
                        <arguments>
                            <argument>run</argument>
                            <argument>release</argument>
                        </arguments>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    

如何使用

开发过程中:

打开项目后,在工具栏上选中AngularBuilder启动配置,点运行图标。启动后AngularCLI会一直在后台监控文件变化。再切到WebServer,启动Jetty即可。

因为build需要时间,改完angular代码后可能需要几秒钟后刷新页面才能看到效果,在底部的Run窗口可以看到日志输出。

发布:
mvn package

一条命令即可,maven会在打包过程中执行cnpm run release命令,构建angular代码

配置好的脚手架项目:

https://github.com/pocketdigi/SpringMVCWithAngular2

因为Angular是单页面应用,不能用传统的文件目录跳转,需要使用路由器。

路由配置

配置路由有两种方式:

  1. 在创建项目或初始化项目时,使用--routing参数,例如:

    ng new angular --routing
    项目创建时会自动生成app-routing.module.ts:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    
    const routes: Routes = [
      {
        path: '',
        children: []
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
      providers: []
    })
    export class AppRoutingModule { }
    

    并且app.module.ts自动加上 AppRoutingModule

    入口组件模板 app.component.html自动加上<router-outlet></router-outlet>标签

  2. 手工处理上面的步骤

    当URL匹配时,Angular会把相应的组件,插到router-outlet位置。

看下面的例子:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {ArticleListComponent} from "./article-list/article-list.component";
import {ArticleDetailComponent} from "./article-detail/article-detail.component";
import {ArticleComponent} from "./article/article.component";

const routes: Routes = [
  {
    path: 'article',
    component:ArticleComponent,
    children: [
      {
        path:'list',
        component:ArticleListComponent,
        children:[]
      },
      {
        path:':id',
        component:ArticleDetailComponent
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

分别配置了/article,/article/list,/article/1234 三个路由,:id会匹配article的id。component指定匹配时需要显示的组件。

需要注意的是children字段,它的组件是在parent里渲染,而不是在根组件里渲染。也就是说,article组件必须有router-outlet标签,/article/list/article/1234是显示在article组件中的,而不是app组件中

页面跳转

模板中跳转:

<a routerLink="/article">文章</a>
<a routerLink="/article/list" routerLinkActive="active">文章列表</a>

routerLinkActive会根据当前路由状态应用相应的css类,上面的例子中,只有当前页面是/article/list时,才会应用active

代码中跳转:
在构造方法中注入Router

constructor(private router:Router){}

跳转

this.router.navigate(['article','list']);

navigate方法的参数是一个数组,包含路径或路径中的参数,上面的例子会跳到/article/list

Angular的指令分为三种:

  1. 属性指令

    属性指令指的是以属性形式使用的指令,如NgClass,NgStyle,FormsModule里的NgModel,NgModelGroup等。

  2. 结构指令

    结构指令用于修改DOM结构,如NgIf,当条件为true时,该元素会被添加到DOM中。

  3. 组件

    这个不必说了,我们用得最多的便是组件。与其他指令不同,它描述的是一个视图,是用户可以直接看到的东西。

自定义属性指令

添加一个color属性,支持传入颜色名参数,设置标签内文本的颜色。

  1. 创建directive:

    ng g directive color

  2. 编写代码

    color.directive.ts :

    import {Directive, ElementRef, Input, AfterViewInit} from '@angular/core';
    
    @Directive({
      selector: '[color]'
    })
    export class ColorDirective implements AfterViewInit {
      _defaultColor='black';
      ngAfterViewInit(): void {
    
      }
      //参数 setter
      @Input('color') set color(colorName:string) {
        this.setFontColor(colorName);
      };
      constructor(private el:ElementRef) {
        this.setFontColor(this._defaultColor);
      }
    
      setFontColor(color:string) {
          this.el.nativeElement.style.color=color;
      }
    }
    
  3. 应用属性

    <h1 color="green">
      { {title}}
    </h1>
    

    此时,该h1标签内容的颜色为绿色

自定义结构指令

结构指令需要在构造方法注入TemplateRef和ViewContainerRef这两个服务,TemplateRef用于访问组件模板,ViewContainerRef用于往DOM插入或移除模板,此处不作演示。

管道(Pipe)可以根据开发者的意愿将数据格式化,还可以多个管道串联。

纯管道(Pure Pipe)与非纯管道(Impure Pipe)

管道分纯管道(Pure Pipe)和非纯管道(Impure Pipe)。默认情况下,管道都是纯的,在自定义管道声明时把pure标志置为false,就是非纯管道。如:

@Pipe({
  name: 'sexReform',
  pure:false
})

纯管道和非纯管道的区别:

  • 纯管道:

    Angular只有检查到输入值发生纯变更时,才会执行纯管道。纯变更指的是,原始类型值(String,Number,Boolean,Symbol)的改变,或者对象引用的改变(对象值改变不是纯变更,不会执行).

  • 非纯管道

    Angular会在每个组件的变更检测周期执行非纯管道。所以,如果使用非纯管道,我们就得注意性能问题了。

管道使用语法

{ {expression | pipe : arg}}
如果是链式串联:
{ {expression | pipe1 : arg | pipe2 | pipe3 }}

常用内置管道

管道

类型

功能

DatePipe

纯管道

日期格式化

JsonPipe

非纯管道

使用JSON.stringify()将对象转成json字符串

UpperCasePipe

纯管道

将文本中的字母全部转在大写

LowerCasePipe

纯管道

将文本中的字母全部转成小写

DecimalPipe

纯管道

数值格式化

CurrencyPipe

纯管道

货币格式化

PercentPipe

纯管道

百分比格式化

SlicePipe

非纯管道

数组或字符串取切割

  • DatePipe

    语法:{ {expression | date:format}}
    expression支持日期对象、日期字符串、毫秒级时间戳。format是指定的格式,常用标志符:

    • yy使用4位数字表示年份(2017),yy使用两位数字表示(17)
    • MM 1位或两位数字(2或10、11、12),MM 两位数字表示,前面补0(02)
    • dd 一位或两位数字(9) dd两位数字,前面补0(09)
    • E 星期 EEE 三位字母缩写的星期 EEEE 星期全称
    • j 12小时制时间 j (9 AM) jj (09 AM)
    • h 12小时制小时 h(9) hh (09)
    • H 24小时制小时 H(9) HH (09)
    • mm (5) mm (05)
    • ss (1) ss (01)
    • z 时区 z China Standard Time
  • DecimalPipe
    语法:{ {expression | number[: digiInfo] }}
    digiInfo格式:
    {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
    即:整数位保留最小位数.小数位保留最小位数-小数位最大保留位置
    默认值: 1.0-3

  • CurrencyPipe
    语法:{ {expression | currency[: currencyCode[: symbolDisplay[: digiInfo]]] }}
    digiInfo格式与DecimalPipe相同,不再解释。
    currencyCod是指货币代码,其值为ISO 4217标准,人民币CNY,美元USD,欧元 EUR.
    symbolDisplay 是一个布尔值,true时显示货币符号($¥) false显示货币码

  • PercentPipe
    语法:{ {expression | percent[: digiInfo] }}
    digiInfo格式与DecimalPipe相同,不再解释。

  • SlicePipe
    语法:{ {expression | slice: start [: end] }}
    expression 可以是一个字符串或数组。字符串时,该管道调用String.prototype.slice()方法截取子串。如果是数组,调用Array.prototype.slice()方法取数组子元素。

自定义管道

除了使用内置的管道,还可以通过自定义管道实现更复杂的功能。
创建管道:
ng g pipe sexReform
angular-cli会帮我们创建SexReformPipe管道,这个管道的功能是根据malefemale返回中文的
代码:

import {Pipe, PipeTransform} from '@angular/core';

@Pipe({
  name: 'sexReform',
  //非纯管道
  pure:false
})
export class SexReformPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    let chineseSex;
    switch (value) {
      case 'male':
        chineseSex = '男';
        break;
      case 'female':
        chineseSex = '女';
        break;
      default:
        chineseSex = '未知性别';
        break;

    }
    return chineseSex;
  }

}

重点在于实现PipeTransform接口的transform方法,定义为非纯管道仅用于演示,非纯管道对性能影响较大,尽量避免。

演示代码

组件:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-pipe',
  templateUrl: './pipe.component.html',
  styleUrls: ['./pipe.component.css']
})
export class PipeComponent implements OnInit {
  date=new Date();
  money=5.9372;
  object={title:'ffff',subTitle:'subtitlefff'};
  str='abcdABCD';
  percent=0.97989;
  constructor() { }

  ngOnInit() {
  }

}

模板:

  <p>
  { {date| date:'y-MM-dd HH:mm:ss'}} <br />
  { {object| json }} <br />
  { {str| uppercase }} <br />
  { {str| lowercase }} <br />
  { {money| number:'2.4-10' }} <br />
  { {money| number:'5.1-2' }} <br />
  { {money| currency:'CNY':false:'1.1-2' }} <br />
  { {percent| percent:'1.1-2' }} <br />
  { {str| slice:1:3 }} <br />
  { {'female'| sexReform }} <br />
</p>

前面一文介绍了模板驱动表单的验证,但它的功能比较简单,有时无法满足我们的需求。响应式表单与模板驱动表单不同的是,响应式表单在组件类中创建表单控制器模型,可在组件中随意控制校验规则。

响应式表单使用ReactiveFormsModule,而非普通的FormModule,需要在app.module.ts里导入

import { ReactiveFormsModule} from '@angular/forms';

@NgModule({
  declarations: [
    ...
  ],
  exports:[AppComponent],
  imports: [
    BrowserModule,
    ...
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

ReactiveFormsModule包含FormControlDirective、FormGroupDirective、FormControlName、FormArrayName和InternalFormsSharedModule模块里的指令。

  • FormControlDirective 描述表单的一个字段
  • FormGroupDirective 描述表单分组
  • FormControlName 描述表单字段名
  • FormArrayName 描述同类型的一组数据的名称,与表单分组无关

下面的例子与前文模板驱动表单类似,不过换了一种实现。自定义校验器也一样,代码不再贴。

模板:

<form [formGroup]="registerForm">
  <div>
    <label for="userName">用户名:</label>
    <!--给input设置一个本地变量,可以读取errors显示错误信息-->
    <input type="text" id="userName" name="userName"  formControlName="userName" required>
    <span *ngIf="formErrors['userName']" class="error">
    { { formErrors['userName'] }}
    </span>

  </div>
  <fieldset aria-required="true" formGroupName="passwordGroup" required>
    <label for="password1">密码:</label>
    <input type="password" id="password1" name="password1" formControlName="password1" required (keyup)="password1ValChanged()"  >
    <span *ngIf="formErrors['passwordGroup.password1']" class="error">
    { { formErrors['passwordGroup.password1'] }}
    </span>
    <label for="password2">重复密码:</label>
    <input type="password" id="password2" name="password2" formControlName="password2" required>
    <span *ngIf="formErrors['passwordGroup.password2']" class="error">
    { { formErrors['passwordGroup.password2'] }}
  </span>
  </fieldset>

  <div>
    <label for="email">邮箱:</label>
    <input type="text" id="email" name="email" formControlName="email" required>
    <!-- 可以通过表单的onValueChanged事件,读到当前的错误信息,写到指定字段里 -->
    <div *ngIf="formErrors.email" class="error">
      { { formErrors.email }}
    </div>
  </div>
  <div>
    <label>性别:</label>
    <input type="radio" name="sex"  value="male" formControlName="sex"> 男
    <input type="radio" name="sex"  value="female" formControlName="sex" > 女
  </div>
  <fieldset formGroupName="nameGroup">
    <label>姓:</label>
    <input type="text" name="firstName" formControlName="firstName" required checked="checked"><br />
    <label>名:</label>
    <input type="text" name="lastName" formControlName="lastName">
  </fieldset>

  <button type="button" class="btn btn-default"
          [disabled]="!registerForm.valid" (click)="doSubmit(registerForm.value)">注册</button>
</form>

{ {registerForm.value|json}}

组件:

import {Component, OnInit, AfterViewInit} from "@angular/core";
import {FormGroup, FormControl, FormBuilder, Validators} from "@angular/forms";
import {repeatPassword} from "../repeat-password.directive";

@Component({
  selector: 'app-reactive-form',
  templateUrl: './reactive-form.component.html',
  styleUrls: ['./reactive-form.component.css']
})
export class ReactiveFormComponent implements OnInit,AfterViewInit {
  registerForm: FormGroup;

  ngAfterViewInit(): void {

  }

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.registerForm = this.fb.group({
      //每一个input都是一个FormControl,key是formControlName,value是构建FormControl的参数,
      // 第一个参数是input的默认值,第二个参数是校验器数组
      'userName': ['', [Validators.required,Validators.minLength(4)]],
      //分组的FormControl,也需要分组构建,key是formGroupName
      'passwordGroup':this.fb.group({
        'password1': ['', [Validators.required,Validators.minLength(4)]],
      }),
      'nameGroup':this.fb.group({
        'firstName': ['', [Validators.required]],
        'lastName': ['', [Validators.required]],
      }),
      'email':['',[Validators.required,Validators.pattern("[\\w]+?@[\\w]+?\\.[a-z]+?")]],
      //默认选中男性
      'sex':['male',[Validators.required]],


    });
    let passwordFormGroup=this.registerForm.controls['passwordGroup'] as FormGroup;
    let password1Control=passwordFormGroup.controls['password1'] as FormControl;

    passwordFormGroup.addControl('password2',new FormControl('',[Validators.required,repeatPassword(password1Control)]));

    this.registerForm.valueChanges.subscribe(data => this.onValueChanged(data));

  }
  //存储错误信息
  formErrors = {
    'email': '',
    'userName': '',
    'passwordGroup.password1':'',
    'passwordGroup.password2':'',
    'sex':''
  };
  //错误对应的提示
  validationMessages = {
    'email': {
      'required': '邮箱必须填写.',
      'pattern': '邮箱格式不对',
    },
    'userName': {
      'required': '用户名必填.',
      'minlength': '用户名太短',
    },
    'passwordGroup.password1':{
      'required': '请输入密码',
      'minlength': '密码太短',
    },
    'passwordGroup.password2':{
      'required': '请重复输入密码',
      'minlength': '密码太短',
      'passwordNEQ':'两次输入密码不同',
      'password1InValid':''
    },
    'sex':{
      'required':'性别必填'
    }

  };

  /**
   * 第一个密码改变时,清空第二个密码框
   */
  password1ValChanged() {
    (this.registerForm.controls['passwordGroup'] as FormGroup).controls['password2'].reset();
  }

  /**
   * 表单值改变时,重新校验
   * @param data
   */
  onValueChanged(data) {

    for (const field in this.formErrors) {
      this.formErrors[field] = '';
      //取到表单字段
      const control = this.registerForm.get(field);
      //表单字段已修改或无效
      if (control && control.dirty && !control.valid) {
        //取出对应字段可能的错误信息
        const messages = this.validationMessages[field];
        //从errors里取出错误类型,再拼上该错误对应的信息
        for (const key in control.errors) {
          this.formErrors[field] += messages[key] + '';
        }
      }

    }

  }

  doSubmit(obj: any) {
    //表单提交
    console.log(JSON.stringify(obj));
  }



}

模板驱动表单,指的是通过html5标准校验的表单,优势在于使用起来简单,但要动态修改验证器、操纵控制器模型不是很方便。

Angular2对表单处理做了一系列封装(模板驱动表单以及响应式表单):

  1. 数据绑定

    这个自然不用说,使用ngModel可以双向绑定到组件里的对象字段。

  2. 控件状态检测

    Angular会自动根据控件状态加上相应的class,如果我们需要编辑input标签在不同状态下的样式,只需要在css里写相应的类就可以了。

    状态

    true时的css类

    false时的css类

    控件是否被访问过

    ng-touched

    ng-untouched

    控件值是否已经变化

    ng-dirty

    ng-pristine

    控件值是否有效

    ng-valid

    ng-invalid

  3. 表单校验

    模板驱动表单支持html5标准属性校验:

    • required:必填
    • minlength:最小长度
    • maxlength:最大长度
    • pattern:正则表达式校验

    另外支持自定义Validator.

    响应式表单内置了上面四种Validator,也可以自己扩展。

模板驱动表单相关指令封装在FormsModule模块中,app.module.ts里需要先导入:

import {FormsModule} from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    ...
  ],
  exports:[AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

FormsModule模块包含NgModule、NgModuleGroup、NgForm和InternalFormsSharedModule模块中包含的指令。

  • NgForm 标记一个表单
  • NgModelGroup 字段分组
  • NgModel 字段

InternalFormsSharedModule是Angular内部模块,FormsModule和ReactiveFormsModule都引用了它,所以可以不用显式引入,直接使用。

下面的例子演示了一个模板驱动表单,包括表单校验、字段分组、控件状态、数据绑定,以及自定义校验器。自定义校验器的功能是校验第二个密码是否与第一个密码相同。

自定义校验器:
repeat-password.directive.ts:

import {Directive, Input, OnChanges, SimpleChanges} from '@angular/core';
import {NG_VALIDATORS, FormControl, Validator, AbstractControl, ValidatorFn, NgModel} from "@angular/forms";
/**
 * 自定义指令,用于检验input标签的值是否跟指定input的值标签相同
 */
@Directive({
  selector: '[repeatPassword]',
  providers: [{provide: NG_VALIDATORS, useExisting: RepeatPasswordDirective, multi: true}]
})
export class RepeatPasswordDirective implements Validator,OnChanges{
  /**
   * 校验方法
   * @param c
   * @returns { {[p: string]: any}}
   */
  validate(c: AbstractControl): {[p: string]: any} {
    return verifyPassword(c,this.repeatPassword.control);
  }

  ngOnChanges(changes: SimpleChanges): void {
      this.repeatPassword=changes['repeatPassword'].currentValue;
  }

  /**
   * 通过属性传入另一个input标签的model
   * 名称与选择器一致,就不需要在使用的时候加额外的属性传入
   */
  @Input() repeatPassword:NgModel;
  constructor() { }


}
/**
 * 导出校验方法,供响应式表单使用
 * @param password1Controller
 * @returns {(currentControl:AbstractControl)=>{[p: string]: any}}
 */
export function repeatPassword(password1Controller:FormControl):ValidatorFn {
  return (currentControl: AbstractControl): {[key: string]: any} => {
    return verifyPassword(currentControl,password1Controller);
  };
}

function verifyPassword(currentControl: AbstractControl,password1Controller:FormControl):{[key: string]: any} {
    if(!password1Controller.valid) {
      console.log("密码1无效");
      return {password1InValid:{'errorMsg':''}}
    }
    if((!currentControl.untouched||currentControl.dirty)&&password1Controller.value!=currentControl.value) {
      return {passwordNEQ:{'errorMsg':'两次密码输入不一致!'}}
    }
}

创建指令后别忘了在app.module.ts里引入 :

@NgModule({
  declarations: [
    AppComponent,
    ...,
    RepeatPasswordDirective
  ],
  exports:[AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

当然,如果使用ng g directive repeatPassword 命令创建指令,会自动添加。

模板:

  <!--(ngSubmit)绑定的表单提交事件,ajax不需要-->
<form #registerForm="ngForm" (ngSubmit)="doSubmit(registerForm.value)" >
  <div>
    <label for="userName">用户名:</label>
    <!--给input设置一个本地变量,可以读取errors显示错误信息-->
    <input type="text" id="userName" name="userName" [(ngModel)]="formData.userName" #userName="ngModel" required minlength="4">
    <div *ngIf="userName.errors && (userName.dirty || userName.touched)" class="error">
      <span [hidden]="!userName.errors.required">用户名必须输入</span>
      <span [hidden]="!userName.errors.minlength">用户名至少4位</span>
    </div>
  </div>
  <!--ngModelGroup指令可以给表单字段分组,值password是registerForm.value里该组的字段名,#passwordGroup是该组的本地变量名-->
  <fieldset ngModelGroup="passwordGroup" #passwordGroup="ngModelGroup" aria-required="true">
    <label for="password1">密码:</label>
    <input type="password" id="password1" name="password1" [(ngModel)]="formData.password1" #password1="ngModel"  required minlength="8">
    <label for="password2">重复密码:</label>
    <!--使用自定义的校验器,加入repeatPassword指令,传入第一个密码输入框的ngModel,即用#password1="ngModel"声明的password1-->
    <input type="password" id="password2" name="password2" [(ngModel)]="formData.password2" [repeatPassword]="password1">
    <span *ngIf="formErrors['passwordGroup.password2']" class="error">
      { { formErrors['passwordGroup.password2'] }} </span>
  </fieldset>

  <div>
    <label for="email">邮箱:</label>
    <input type="text" id="email" name="email" [(ngModel)]="formData.email" required pattern="[\w]+?@[\w]+?\.[a-z]+?">
    <!-- 可以通过表单的onValueChanged事件,读到当前的错误信息,写到指定字段里 -->
    <div *ngIf="formErrors.email" class="error">
      { { formErrors.email }}
    </div>
  </div>
  <div>
    <label>性别:</label>
    <input type="radio" name="sex" [(ngModel)]="formData.sex" value="male" checked="checked"> 男
    <input type="radio" name="sex" [(ngModel)]="formData.sex" value="female" > 女
  </div>
  <fieldset ngModelGroup="nameGroup" #nameGroup="ngModelGroup">
    <label>姓:</label>
    <input type="text" name="firstName" [(ngModel)]="formData.firstName" required><br />
    <label>名:</label>
    <input type="text" name="lastName" [(ngModel)]="formData.lastName">
  </fieldset>

  <button type="button" class="btn btn-default"
          [disabled]="!registerForm.valid" (click)="doSubmit(registerForm.value)">注册</button>
</form>

{ {registerForm.value|json}}

组件:

import {Component, OnInit, ViewChild, AfterViewInit} from "@angular/core";
import {NgForm} from "@angular/forms";

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit,AfterViewInit {
  ngAfterViewInit(): void {
    //订阅表单值改变事件
    this.registerForm.valueChanges.subscribe(data => this.onValueChanged(data));
  }
  //找到表单
  @ViewChild('registerForm') registerForm: NgForm;


  constructor() {
  }

  formData = {} as any;
  ngOnInit() {
    //默认性别为male
    this.formData.sex = "male";

  }
  doSubmit(obj: any) {
    //表单提交
    console.log(JSON.stringify(obj));
  }

  onValueChanged(data) {

    for (const field in this.formErrors) {
      this.formErrors[field] = '';
      //取到表单字段
      const control = this.registerForm.form.get(field);
      //表单字段已修改或无效
      if (control && control.dirty && !control.valid) {
        //取出对应字段可能的错误信息
        const messages = this.validationMessages[field];
        //从errors里取出错误类型,再拼上该错误对应的信息
        for (const key in control.errors) {
          this.formErrors[field] += messages[key] + '';
        }
      }

    }

  }


  //存储错误信息
  formErrors = {
    'email': '',
    'userName': '',
    'passwordGroup.password1':'',
    'passwordGroup.password2':'',
    'sex':''
  };
  //错误对应的提示
  validationMessages = {
    'email': {
      'required': '邮箱必须填写.',
      'pattern': '邮箱格式不对',
    },
    'userName': {
      'required': '用户名必填.',
      'minlength': '用户名太短',
    },
    'passwordGroup.password1':{
      'required': '请输入密码',
      'minlength': '密码太短',
    },
    'passwordGroup.password2':{
      'required': '请重复输入密码',
      'minlength': '密码太短',
      'passwordNEQ':'两次输入密码不同',
      'password1InValid':''
    },
    'sex':{
      'required':'性别必填'
    }

  };
}

Angular2的模板语法大体上与html一致,功能上增加了数据绑定以及一些控制dom的小功能。
数据绑定主要有以下几种:
1. 插值绑定
2. property绑定
3. attribute绑定
4. class绑定
5. style绑定
6. 事件绑定
7. 局部变量绑定
8. ngModel
9. ngSwitch
10. ngFor
11. ngForTrackBy

其他功能:
1. 管道操作符
2. 安全导航操作符
3. others…

数据绑定方向:

数据方向

语法

绑定类型

从数据源到视图

{ {expression}}
[target] = “expression”
bind-target = “expression”

插值表达式
Property
Attribute
Class
Style

从视图到数据源

(event) = “method(event)”
on-event = “method”
$event可以不传

事件

双向

[(target)] = “expression”
bindon-target = “expression”

双向

Property 和 Attribute区别

Property和Attribute中文都叫属性,但这两者是有区别的。
Property由Dom定义,Attribute由HTML定义。
* 少量 HTML attribute 和 property 之间有着 1:1 的映射,如id
* 有些 HTML attribute 没有对应的 property,如colspan
* 有些 DOM property 没有对应的 attribute,如textContent
* 大量 HTML attribute看起来映射到了property…… 但却不像我们想的那样!

Dom property由attribute初始化,一旦初始化完成,attribute就没有用了。attribute的值不能改变,但property却可以。

就算名字相同,HTML attribute 和 DOM property 也不是同一样东西。

模板绑定是通过 property 和事件来工作的,而不是 attribute。

NgModel

NgModel适用表单的双向绑定,其原理是封装了value property的单向绑定和input事件。
ngModel还有展开形式,用于手工处理用户输入的数据:

<input
  [ngModel]="currentHero.firstName"
  (ngModelChange)="setUpperCaseFirstName($event)">

demo:
TemplateComponent:

import {Component, OnInit, ChangeDetectionStrategy, AfterViewInit} from '@angular/core';

@Component({
  selector: 'app-template',
  templateUrl: './template.component.html',
  styleUrls: ['./template.component.css'],
  changeDetection:ChangeDetectionStrategy.Default
})
export class TemplateComponent implements OnInit,AfterViewInit {
  ngAfterViewInit(): void {
    //更新时间
    setInterval(()=>{
      this.currentDate=new Date();
    },1000);
  }
  title="template title";
  inputType="text";
  clickCount=0;
  currentDate=new Date();
  obj:any;
  inputText="";
  toeChoice="Moe";
  array=[];

  constructor() { }

  ngOnInit() {
    // this.obj={};
    // this.obj.title="adffs";
    this.array.push({title:'aaaa',id:1});
    this.array.push({title:'bbbb',id:2});
    this.array.push({title:'cccc',id:3});
    this.array.push({title:'dddd',id:4});
    this.array.push({title:'eeee',id:5});

  }

  isBlue() {
    return false;
  }
  clickEvent(event) {
    event.currentTarget.value="点击 "+(++this.clickCount);
  }

  ngStyle() {
    let styles = {
      // CSS property names
      'font-style':  'italic',
      'font-weight': 'bold',
      'font-size':  '2rem',
    };
    return styles;
  }

  trackByObj(index:number,obj:any) {
    return obj.id;
  }
}

模板:

<p>
  <label>插值绑定:</label><span>{ {title}}</span>
</p>

<p>
  <label [title]="title">property绑定</label>
</p>

<p>
  <label>attribute绑定:
    <tr>
      <td [attr.colspan]="1 + 1">Three-Four</td>
    </tr>
  </label>
</p>

<p>
  <label [class.blue]="isBlue()">CSS class绑定,当isBlue()返回true时,加载css类blue</label>
</p>
<p>
  <label [ngClass]="{blue: true}">NgClass</label>
</p>

<p>
  <label [style.color]="isBlue()?'blue':'red'">CSS style绑定</label>
</p>
<p>
  <label [ngStyle]="ngStyle()">NgStyle</label>
</p>
<p>
  <label>事件绑定:<input type="button" (click)="clickEvent($event)" value="点击事件绑定"/><span>event可以不传</span> </label>
</p>

<p>
  <label>局部变量:<input type="text" #input1 (input)="null"/><span>{ { input1.value }}</span>
    此处必须增加一个事件,否则因为value改变没有触发事件,不会实时更新,但类似这样的需求,ngModel更适合</label>
</p>

<p>
  <label>管道操作符:{ {currentDate | date: "yyyy-MM-dd HH:mm:ss"}}</label>
</p>
<p>
  <label>安全导航操作符:{ {obj?.title}}</label>
</p>

<p>
  <label>NgModel:<input type="text" [(ngModel)]="inputText"/><span>{ { inputText }}</span></label>
</p>
<p>
  <label>NgIf:<span *ngIf="inputText">Hello, { {inputText}}</span></label>
</p>
<p>
  <label>NgSwitch:
    <span [ngSwitch]="toeChoice">
      <span *ngSwitchCase="'Eenie'">Eenie</span>
      <span *ngSwitchCase="'Meanie'">Meanie</span>
      <span *ngSwitchCase="'Miney'">Miney</span>
      <span *ngSwitchCase="'Moe'">Moe</span>
      <span *ngSwitchDefault>other</span>
    </span>
  </label>
</p>

<p>
  <label>NgFor:
    <div *ngFor="let obj of array;let i=index">{ {i + 1}} - { {obj.title}}</div>
  </label>

</p>

<p>
  <label>NgForTrackBy:
    <div *ngFor="let obj of array;trackBy:trackByObj">{ {obj.title}}</div>
  </label>

</p>

css:

p {
  font-size: 1.3rem;
}
.red {
  color: red;
}
.blue {
  color:blue;
}

Angular可以通过在一定的时间内将组件的css样式过渡切换成另一种样式来实现动画。下面的例子是将一个盒子从黄色背景切成蓝色,并且修改margin-left来移动位置。
效果如下:
angular 动画

AnimationComponent:

import {Component, OnInit, trigger, state, style, transition, animate} from '@angular/core';

@Component({
  selector: 'app-animation',
  templateUrl: './animation.component.html',
  styleUrls: ['./animation.component.css'],
  animations:[
    //在position状态改变时,触发动画
    trigger('position',[
      //position为left时的样式
      state('left',style({
        'margin-left': 0,
        'background-color':'yellow'
      })),
      //position为right时的样式
      state('right',style({
        'margin-left': 200,
        'background-color':'blue'
      })),
      //状态切换时的动画设置
      transition('left => right',animate('1000ms ease-in')),
      transition('right => left',animate('1000ms ease-out'))
    ])
  ]
})
export class AnimationComponent implements OnInit {

  constructor() { }
  currentPosition='left';
  ngOnInit() {
  }

  /**
   * 按钮事件,切换状态
   */
  togglePosition() {
    if(this.currentPosition=='left') {
      this.currentPosition='right';
    }else{
      this.currentPosition='left';
    }
  }
}

模板:

  <div id="brick" [@position]="currentPosition"></div>
  <button (click)="togglePosition()">切换位置</button>

css:

#brick {
  width: 20rem;
  height: 10rem;
  background-color: aqua;
  margin-left: 0;
}