java jstat 用法

jstat是jdk自带的JVM内存统计工具,用于查看heap的内存和垃圾回收情况。

用法:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

参数定义:

  • options:选项,通过jstat -options查看支持的选项,目前支持
    • -class
    • -compiler
    • -gc
    • -gccapacity
    • -gccause
    • -gcmetacapacity
    • -gcnew
    • -gcnewcapacity
    • -gcold
    • -gcoldcapacity
    • -gcutil
    • -printcompilation
  • lines: 使用interval参数,会在间隔指定时间后输出当前JVM内存的状态,这个参数是指定输出多少行后,再输出title,这样就不需要翻屏看这一列的title了。
  • vmid: 虚拟机的pid
  • interval:间隔多少时间后循环输出,不指定的话,就输出一次
  • count:指定输出多少次

实例:

class

jstat -class pid

输出如下:

显示的是加载的类的数量和占用的字节数、卸载的类的数量和字节数,以及加载卸载所花时间。

compiler

jstat -compiler pid

  • Compiled: 编译的数量
  • Failed: 编译失败的数量
  • Invalid: 编译失效的数量
  • Time: 编译任务消耗时间
  • FailedType: 最后一次编译失败类型
  • FailedMethod:最后一次编译失败的类和方法

complier展示的是编译的统计

gc

jstat -gc

  • S0C 年轻代第一个survivor区的容量
  • S1C 年轻代第二个survivor区的容量
  • S0U 年轻代第一个survivor区的已用空间(字节)
  • S1U 年轻代第一个survivor区的已用空间(字节)
  • EC 年轻代eden区的容量
  • EU 年轻代eden区的已用空间
  • OC 老年代的容量
  • OU 老年代的已用空间
  • MC 元空间(Metaspace)的容量
  • MU 元空间的已用空间
  • CCSC 压缩的类空间容量
  • CCSU 压缩的类空间已用空间
  • YGC 年轻代垃圾回收事件的数量
  • YGCT 年轻代垃圾回收时间
  • FGC Full GC事件数量
  • FGCT Full GC时间
  • GCT GC总时间

gc展示的是垃圾回收的统计,这个例子基于java 8,java 8之前是没有元空间(MetaSpace)概念的,而是永久代(PC,PU)

gccapacity

jstat -gccapacity pid

  • NGCMN 年轻代最小(初始化)大小
  • NGCMX 年轻代最大容量
  • NGC 年轻代当前大小
  • S0C 年轻代第一个survivor区容量
  • S1C 年轻代第二个survivor区容量
  • EC 年轻代Eden区容量
  • OGCMN 老年代初始化容量
  • OGCMX 老年代最大容量
  • OGC 老年代当前占用容量
  • OC 老年代空间容量
  • MCMN 元空间初始容量
  • MCMX 元空间最大容量
  • CCSMN 压缩类空间初始容量
  • CCSMX 压缩类空间最大容量
  • CCSC 当前压缩类空间容量
  • YGC 年轻代垃圾回收事件数量
  • FGC Full GC事件数量

gccapacity展示的是各个空间的初始容量、最大容量、以及当前容量

gcnew

  • S0C 年轻代第一个survivor区容量
  • S1C 年轻代第二个survivor区容量
  • S0U 年轻代第一个survivor区已用空间
  • S1U 年轻代第二个survivor区已用空间
  • TT 持有次数限制
  • MTT 最大持有次数限制(超过这个值放入old)
  • EC 年轻代的容量
  • EU 年轻代的已用空间
  • YGC 年轻代GC事件次数
  • YGCT 年轻代GC消耗的时间

gcnew展示的年轻代的信息

后面的-gcnewcapacity -gcold -gcoldcapacity -gcutil字段都类似,不再演示。-printcompilation展示的是编译相关的信息。

Sonatype Nexus mavn私服安装与maven上传jar包

为什么要使用Maven私服?

某些jar包仅供内部使用,不适合在中央库发布。不用私服,团队间只能私底下传输打出来的jar包了,私服就是用来管理这些包。

Sonatype Nexus是什么?

Sonatype Nexus就是一个maven私服,也可以称做代理服务器。原理是代理中央库,同时管理私有库,在使用时,我们只要把maven的镜像地址配置成nexus地址即可。

Sonatype Nexus的安装

懒得折腾,所以直接用docker了。使用的是Sonatype Nexus Repository Manager 2镜像。当然,如果你有钻研学习精神,还是自己按官方教程一步一步来,否则建议直接用docker,一个字,快!

docker安装不再介绍

step1: 创建数据目录

Sonatype Nexus会有一些索引之类的数据需要放到外部目录,我们先创建它,使用docker都是这么个套路,需要把数据放到外部。假设这里的目录是/Users/xxx/docker/nexus-data

step2: 下载并启动Sonatype Nexus

docker run -d -p 8081:8081 --name nexus -v /Users/xxx/docker/nexus-data:/sonatype-work sonatype/nexus

docker会去下载相应的镜像,并运行,端口号8081,打开http://localhost:8081/nexus/确认成功

到此安装完成,默认账号密码admin/admin123

Sonatype Nexus 配置

此时如果使用搜索功能,会发现什么都找不到,因为现在还没有中央库的索引。

上面说过,Sonatype Nexus需要代理中央库,所有maven下载库的请求都会通过Nexus,如果Nexus上没有,去中央库下载,如果有,直接返回了。

更新中央库索引。
更新索索

如果你现在在国外,或者连着很快的vpn,那么,过一会,应该就可以更新完索引了,因为中央库服务器在国外,国内下载速度慢。

国内用户怎么办呢?在架这个nexus之前,大家应该都用阿里云的镜像吧?速度还算快,那么我们把默认的中央库地址换成阿里云的
替换地址
阿里云上还有个jcenter,也加上,再更新索引。

在服务器上执行需谨慎,我的服务器昨天被封,到目前为止还是访问不了阿里云maven镜像,服务器换IP不容易。

大概过了一两个小时,我这下完了,下完其实还要在本地建索引什么的,需要点时间,反正我这边监控网络已经没速度了,docker的cpu占用还是很高,cpu风扇狂转,这一步需要时间,请耐心。

索引建完以后(Administration/Scheduled Tasks是可以看到当前任务的),就可以搜索到中央库上的包了。

此时,Nexus已经可以使用了,当然,改密码,配权限,不用说了,自己改.

客户端配置settings.xml

settings.xml在~/.m2/下,编辑:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

    <servers>
        <server>
            <id>nexus</id>
            <username>admin</username>
            <password>admin123</password>
        </server>
    </servers>

    <mirrors>
        <mirror>
            <id>Nexus</id>
            <mirrorOf>*</mirrorOf>
            <name>Nexus Repository</name>
            <url>http://localhost:8081/nexus/content/groups/public</url>
        </mirror>
    </mirrors>

    <profiles>
        <profile>
            <id>nexus</id>
            <!--Enable snapshots for the built in central repo to direct -->
            <!--all requests to nexus via the mirror -->
            <repositories>
                <repository>
                    <id>nexus</id>
                    <url>http://localhost:8081/nexus/content/content/groups/public</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>nexus</id>
                    <url>http://localhost:8081/nexus/content/content/groups/public</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>

    <activeProfiles>
        <!--make the profile active all the time -->
        <activeProfile>nexus</activeProfile>
    </activeProfiles>
</settings>

账号密码和服务器地址改一下就可以了。

Maven项目配置

打开pom.xml,加入:

<distributionManagement>
        <snapshotRepository>
            <id>nexus</id>
            <name>nexus snapshots</name>
            <url>http://localhost:8081/nexus/content/repositories/snapshots
            </url>
        </snapshotRepository>
        <repository>
            <id>nexus</id>
            <name>nexus releases</name>
            <url>http://localhost:8081/nexus/content/repositories/releases
            </url>
        </repository>
    </distributionManagement>

发布项目到nexus

mvn package

搭建Spring boot + mybatis + freemarker项目

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.mapper`和`com.pocketdigi.dao.po`包,这两个包是分别用来放mybatis generator生成的mapper接口文件和po,example类的,mybatis_mapper在resources目录下,放mybatis generator生成的mapper xml文件。请在生成后放到对应目录,怎么生成就不介绍了。
  1. 开始编写业务逻辑
    创建service,controller包,开始写业务逻辑吧
    完成时,整体项目结构大概是这样的:

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

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

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

Angular2 模块懒加载

当项目变得复杂庞大以后,如果所有页面都在一个模块里,就会出现首页加载慢的问题,因为首页就已经把整个项目加载进来了。所以,很有必要根据业务将不同的功能分模块,以便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,看看不同的页面是不是加载了不同的文件。

SpringMVC Angular2 Intellij Idea 开发环境搭建

整合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`里的配置,切换到正式环境
  1. 添加运行配置
    Run/Debug Configurations(Run-Edit Configurations)窗口,添加一个npm配置:

    npm

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

  2. 修改路由策略
    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 }`
  1. git忽略dist目录
    因为dist里所有文件都是Angular生成的,我们不需要维护到git上,在
    .gitignore里将dist目录加上,添加一行:

    /web/src/main/webapp/dist/
如果之前已经跟踪之个目录,执行下面的命令取消:

    git rm --cached -r web/src/main/angular/
  1. 使用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 通过路由页面导航

因为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>`标签
  1. 手工处理上面的步骤

    当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

Angular2 自定义指令

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;
      }
    }
  1. 应用属性

    <h1 color="green">
      { {title}}
    </h1>
此时,该h1标签内容的颜色为绿色

自定义结构指令

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

Angular2 使用管道Pipe以及自定义管道格式数据

管道(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>

Angular2 响应式表单验证

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

响应式表单使用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));
  }



}

Angular2 模板驱动表单校验

模板驱动表单,指的是通过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':'性别必填'
    }

  };
}