Nacos能做什么?

Nacos(官网:nacos.io)是一个易于使用的平台,专为动态服务发现和配置以及服务管理而设计。它可以帮助您轻松构建云原生应用程序和微服务平台。 服务是Nacos的关键。 Nacos 支持几乎所有类型的服务,例如,Dubbo/gRPC 服务、Spring Cloud RESTFul 服务或 Kubernetes 服务。

Nacos 提供了四大主要功能。

  • 服务发现和服务健康检查

    Nacos 使服务通过 DNS 或 HTTP 接口注册自己和发现其他服务变得简单。 Nacos 还提供服务的实时健康检查,以防止向不健康的主机或服务实例发送请求。

  • 动态配置管理

    动态配置服务允许您在所有环境中以集中和动态的方式管理所有服务的配置。 Nacos 无需在更新配置时重新部署应用程序和服务,这使得配置更改更加高效和敏捷。

  • 动态 DNS 服务

    Nacos 支持加权路由,让您更容易在数据中心内的生产环境中实现中层负载均衡、灵活的路由策略、流量控制和简单的 DNS 解析服务。它可以帮助您轻松实现基于 DNS 的服务发现,并防止应用程序耦合到特定于供应商的服务发现 API。

  • 服务和元数据管理

    Nacos 提供了一个易于使用的服务仪表板,帮助您管理您的服务元数据、配置、kubernetes DNS、服务健康和指标统计。

本文主要讲讲Nacos作为注册中心和Nacos作为配置中心的使用


Nacos 地图

一图看懂 Nacos


Nacos 快速开始

预备环境准备

Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用:

  1. 64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。
  2. 64 bit JDK 1.8+;下载 & 配置
  3. Maven 3.2.x+;下载 & 配置

下载

您可以从 最新稳定版本 下载 nacos-server-$version.zip 包。

1
2
unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz
cd nacos/bin

启动服务器

Linux/Unix/Mac

启动命令(standalone代表着单机模式运行,非集群模式):

1
sh startup.sh -m standalone

Windows

启动命令(standalone代表着单机模式运行,非集群模式):

1
startup.cmd -m standalone

或双击bin中的startup.cmd 文件

访问http://localhost:8848/nacos/
账户密码使用默认的nacos/nacos 进行登录


SpringCloud Alibaba-Nacos[作为注册中心]

首先,修改pom.xml 文件,引入Nacos Discovery Starter。

如果要在您的项目中使用 Nacos 来实现服务注册/发现,使用 group ID 为 com.alibaba.cloud 和 artifact ID 为 spring-cloud-starter-alibaba-nacos-discovery 的 starter。

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

修改application.yml配置

  • 在应用application.yml配置文件中配置Nacos Server 地址
1
2
3
4
5
6
# nacos服务地址
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
  • 在应用application.yml配置文件中配置服务的名称
1
2
3
4
# nacos服务地址
spring:
application:
name: product

通过Spring Cloud原生注解 @EnableDiscoveryClient 开启服务注册发现功能

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {

public static void main(String[] args) {
SpringApplication.run(NacosProviderApplication.class, args);
}

}

启动应用

启动成功后再去nacos服务列表里,观察nacos 服务列表是否已经注册上服务,如下图所示已经注册上了


SpringCloud Alibaba-Nacos[作为配置中心]

我们先了解一下 Spring Cloud 应用如何接入 Nacos Config,步骤如下

  1. 首先,修改 pom.xml 文件,引入 Nacos Config Starter。

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  1. 在应用的 /src/main/resources/bootstrap.properties 配置文件中配置 Nacos Config 元数据

    1
    2
    spring.application.name=nacos-config-example
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
  2. 需要给配置中心默认添加一个叫数据集(Data Id)gulimall-coupon.properties。默认规则,应用名.properties

  3. 给应用名.properties中添加配置

  4. 完成上述步骤后,应用会从 Nacos Config 中获取相应的配置,并添加在 Spring Environment 的 PropertySources 中。这里我们使用 @Value 注解来将对应的配置注入到 SampleController 的 userName 和 age 字段,并添加 @RefreshScope 打开动态刷新功能

1
2
3
4
5
6
7
8
9
10
@RefreshScope
class SampleController {

@Value("${user.name}")
String userName;

@Value("${user.age}")
int age;
}

1
如果配置中心和当前应用的配置文件中都配置了相同的项,优先使用配置中心的配置

细节:

1)命名空间

用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的Group 或Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。

默认:public(保留空间);默认新增的所有配置都在public

  • 开发,测试,生产:利用命名空间来做环境隔离

  • 我们需要在我们的项目的bootstrap.properties配置上我们需要使用的命名空间配置。namespace一定要写命名空间的唯一id

每一个微服务之间相互隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置。

2)配置集

一组相关或不相关的配置项的集合称为配置集。在系统中一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。

3)配置集ID

Data ID:类似于文件名

4)配置分组

默认所有的配置集都属于:DEFAULT_GROUP

我们也可以创建自己的分组例如1111,618,1212

然后在项目的bootstrap.properties文件中指定分组


实际项目中的应用

每个微服务创建自己的命名空间,使用配置分组来区分开发环境,如dev,test,prod。


同时加载多个配置集

微服务的任何配置信息,任何配置文件都可以放在配置中心中

例如我们要读取配置中心的datasource.yml、mybatis.yml、 others.yml三个配置文件

在项目中进行读取,如下图所示

简介

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。


主要功能

  • 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

组件

Sentinel把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Nacos一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

RocketMQ一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。

DubboApache Dubbo™ 是一款高性能 Java RPC 框架。

Seata阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。


版本管理规范

项目的版本号格式为 x.x.x 的形式,其中 x 的数值类型为数字,从 0 开始取值,且不限于 0~9 这个范围。项目处于孵化器阶段时,第一位版本号固定使用 0,即版本号为 0.x.x 的格式。

由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且 spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟 SpringBoot 版本号一致的版本:

  • 1.5.x 版本适用于 Spring Boot 1.5.x
  • 2.0.x 版本适用于 Spring Boot 2.0.x
  • 2.1.x 版本适用于 Spring Boot 2.1.x
  • 2.2.x 版本适用于 Spring Boot 2.2.x
  • 2021.x 版本适用于 Spring Boot 2.4.x

如何使用

如何引入依赖

如果需要使用已发布的版本,在 dependencyManagement 中添加如下配置。

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后在 dependencies 中添加自己所需使用的依赖即可使用。


Debug常用断点调试快捷键

图标 快捷键 作用
F8 Step Over:步过,一行一行地往下走,如果这一行上有方法不会进入方法。
F7 Step Into:步入,如果当前行有方法,可以进入方法内部,一般用于进入自定义方法内,不会进入官方类库的方法。
Alt+Shift+F7 Force Step Into:强制步入,能进入任何方法,查看底层源码的时候可以用这个进入官方类库的方法。
Shift+F8 Step Out:步出,从步入的方法内退出到方法调用处,此时方法已执行完毕,只是还没有完成赋值。
Alt+F9 Run to Cursor:运行到光标处,你可以将光标定位到你需要查看的那一行,然后使用这个功能,代码会运行至光标行,而不需要打断点。
Ctrl+F2 Stop ‘xxx’:停止运行
Ctrl+Shift+F8 View Breakpoints:查看所有断点
Mute Breakpoints:选择这个后,所有断点变为灰色,断点失效,按F9则可以直接运行完程序。
F9 Resume Program:恢复程序,如果下面还有断点则运行到下一个断点。
Drop Frame:断点回退,断点会回退到我们打断点的位置。

条件断点

在我们打断点的地方鼠标右击,然后在condition输入框里面输入条件


求值表达式Evaluate Expression

在断点调试过程中,我们想查看某些变量或某些对象的属性值,我们可以通过快捷键Alt+F8打开表达式求值


setValue

Intellij IDEA在debug模式下可以改变变量的值,下面这段代码为例,比如我们想看看i=12时会输出什么

此时i==5,我们把i设置成12

按快捷键F8看看i是否会进入if条件里面



异常断点

Java Exception Breakpoints可以使当程序遇见被选择的异常时,自动停在相应异常的位置处。

如下面这段代码

加上条件断点,使用快捷键Ctrl+Shift+F8或者点击图标

再使用Debug模式来运行程序,当程序出现异常,会之间停留在我们出现异常的代码处


避免操作资源

如上图所示,我们在断点调试时,后续的一些操作可能会涉及操作一些资源,我们又不想后面的代码执行,直接让代码终止。通常我们想到的是点击终止按钮

但是点击这个按钮之后后续的操作还会执行,如下图所示


Debug调试界面,选择正在运行的Stack,鼠标右键选择Force Return,如下图所示

上面的操作完成之后,代码会跑到Thread.java类中的exit()方法里。再点击

程序就运行结束。我们就会看到后续的操作并没有执行

最近使用git连接github时一直报下面这个错:

git 连接远程仓库有两种协议:ssh协议https协议

根据英文可以看出,ssh协议连接超时,我们切换成 https协议连接github。

移除掉远程仓库的配置

1
$ git remote rm origin

重新添加新的远程仓库,以https的形式:

1
git remote add origin https://github.com/wotzc/wotzc.github.io.git

https的地址其实就是github上项目对应的地址,如下图:

再次尝试push代码,可以看到已经成功。

但是我们在部署hexo d的时候输入账号密码后还是会报错,错误如下:

1
Password authentication is temporarily disabled as part of a brownout. Please use a personal access token instead [duplicate]

因为2021年8月13日之后密码身份验证由 GitHub 禁用,不再支持。创建并使用个人访问令牌PAT而不是密码。

此消息意味着您使用密码而不是个人访问令牌通过 HTTPS 访问 GitHub,这已不再允许。GitHub 禁用了密码身份验证,因为人们经常会意外泄露密码,虽然个人访问令牌可以限制损坏,但密码不能。

如果您没有在提示时明确输入密码,那么您很可能有一个凭据管理器来保存您的密码,并在不提示您的情况下发送密码。

所以我们需要使用个人访问令牌进行身份验证,步骤如下:

  1. 在 Github 上创建个人访问令牌

    From your Github account, go to Settings => Developer Settings => Personal Access Token => Generate New Token (Give your password) => Fillup the form => click Generate token => Copy the generated Token, it will be something like ghp_sFhFsSHhTzMDreGRLjmks4Tzuzgthdvfsrta

  2. 对于Windows操作系统

    控制面板中访问凭证管理器,选择Windows凭据,找到git:https://github.com,点击编辑,把上一步复制的密码粘贴到密码处。

  3. 对于Mac操作系统

Click on the Spotlight icon (magnifying glass) on the right side of the menu bar. Type Keychain access then press the Enter key to launch the app => In Keychain Access, search for => Find the internet password entry for => Edit or delete the entry accordingly => You are donegithub.com

现在将给定记录存入计算机以记住令牌:

1
$ git config --global credential.helper cache

如果在进行hexo d操作时,还是提示下面这种错误:

我们就修改根目录的_config.yml文件,把repo修改为https连接的地址

再次hexo d成功!

IDEA查看一个类所有的方法

快捷键Ctrl+F12


IDEA查看一个类的继承关系

方式一 以图的方式显示

1、找到当前类所在的位置,右键选择Diagrams,然后选择Show Diagrams……

2、在弹出的框中选择Java Class Diagrams:

3、可以看到如下的结果,所有的父类继承关系:

4、在页面点击右键,选择 show categories,根据需要可以展开类中的属性、方法、构造方法等等。当然,第二种方法也可以直接使用上面的工具栏:

然后你就会得到:

方式二 通过hierarchy面板

可以点击编辑器最上端的Navigate,下拉选择Type Hierarchy,或者使用快捷键Ctrl + H(mac对应的是Ctrl+H),就会出现一个有层级关系的关系列表,如下图所示,展示所有的父类或子类:


代码提示不区分大小写

settings -> Editor -> General -> Code Completion

  • IntelliJ IDEA 的代码提示和补充功能有一个特性:区分大小写。如上图标注所示,默认就是 First letter 区分大小写的。
  • 区分大小写的情况是这样的:比如我们在 Java 代码文件中输入 stringBuffer,IntelliJ IDEA 默认是不会帮我们提示或是代码补充的,但是如果我们输入StringBuffer 就可以进行代码提示和补充。
  • 如果想不区分大小写的话,取消勾选Match case选项即可。

tab 多行显示

settings -> Editor Tabs -> Configure Editor Tabs…,取消勾选 Show Tabs In Single Row选项。

效果如下:


代码块包裹功能 - Surround With

Ctrl + Alt + T 提供的是代码块包裹功能 - Surround With。可以快速将选中的代码块,包裹到选择的语句块中。

(mac对应的是Command+Option+T)


设置文件头

File–> Settings–> Editor–> File and Code Templates–> Includes–> File Header–> 添加以下代码

${USER}会读取当前电脑的用户名

1
2
3
4
/**
* @Author ${USER}
* @Date ${DATE}
*/

新建一个文件效果如下:


设置方法注释模板

先看效果

IDEA还没有智能到自动为我们创建方法注释,这就是要我们手动为方法添加注释,使用Eclipse时我们生成注释的习惯是

/+Enter**,这里我们也按照这种习惯来设置IDEA的方法注释

1、File–>Settings–>Editor–>Live Templates

2、点+号新建一个组或模板,选择Live template

3、新建模板:命名为*,修改生成注释的快捷键为Enter

4、设置模板:模板内容如下,注意第一行,只有一个*而不是/*,在设置参数名时必须用${参数名}$的方式

1
2
3
4
5
6
7
8
*
* @author $USER$
* @Description $description$
$param$
$return$
* @Date $date$ $time$
**/

5、设置参数的获取方式,选择右侧的Edit variables按钮

让params 多行显示,而非数组显示

其中params变量的内容一定要放在Default value中!!!内容为:

1
groovyScript("if(\"${_1}\".length() == 2) {return '';} else {def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList();for(i = 0; i < params.size(); i++) {if(i==0){result+='* @param ' + params[i] + ': '}else{result+='\\n' + ' * @param ' + params[i] + ': '}}; return result;}", methodParameters());

其中return变量的内容也一定要放在Default value中!!!内容为:

1
groovyScript("def returnType = \"${_1}\"; def result = '* @return: ' + returnType; return result;", methodReturnType());

6、设置模板的应用场景,点击模板页面最下方的警告,来设置将模板应用于那些场景,一般选择EveryWhere–>Java即可(如果曾经修改过,则显示为change而不是define)


全屏显示

我们可以使用【Presentation Mode】,将IDEA弄到最大,可以让你只关注一个类里面的代码,进行毫无干扰的coding。

可以使用Alt+V快捷键(mac对应的快捷键是option+v),弹出View视图,然后选择Appearance->Enter Presentation Mode。效果如下:

退出进入全屏模式 ,点击 View—>Appearance —> Exit Persenetation Mode。


分屏操作

在某一个类上面右键

效果如下:

屏幕太多了,一直用鼠标也比较麻烦,这里我们可以直接 Ctrl+E进行页面切换

除了 Ctrl+E(mac对应的是command+E) 还可以只用 ,Ctrl+Alt+ 方向键进行切换视图


快速的开发常见操作 :

判空

如图,通过字符串.null,然后点回车

效果如下:

循环

通过.fori遍历集合或数组

效果如下:


变量抽取

代码抽取

抽取方法,很多时候,在俺们开发当中,经常会方法调用方法,有时候一段代码过于的冗余,所以需要进行抽取。

选中代码,然后快捷键Ctrl+Alt+M(mac对应的快捷键是command+option+M),就会自动抽取成方法

效果如下


快速完成声明 if while 等语句

智能提示,该功能可以基于上下文环境,智能帮你过滤可以使用方法,推导出最适合的方法。该快捷键为 Ctrl+Shift+Space(mac对应的快捷键是ctrl+option+space)。

快速完成语句在 IDEA 中,可以使用快捷键 Ctrl+Shift+Enter(mac对应的是Command+shift+enter)快速完成声明 if while 等语句。在下面的例子中,我们输入 while ,接着我们输入快捷键,我们可以看到 IDEA 自动帮我们完整这个结构,然后只需要输入判断条件即可。


IDEA 光标操作

快捷键alt+j在相同的字符后生成光标,mac对应的快捷键是option+G

快捷键ctrl+alt+shift+j在相同的字符后生成光标,mac对应的快捷键是command+option+G

折叠代码

折叠代码,ctrl+'-',mac对应的快捷键是command+'-'

展开代码,ctrl+'+',mac对应的快捷键是command+'+'

折叠所有代码,ctrl+shift+'-',mac对应的快捷键是command+shift+'-'

展开所有代码,ctrl+shift+'+',mac对应的快捷键是command+shift+'+'

IDEA设置鼠标悬浮提示

效果如下:


设置行号与方法的分隔符

效果如下:


设置自动编译

Intellij Idea 默认状态为不自动编译状态,Eclipse 默认为自动编译。


使用模板

IDEA中代码代码模板所在的位置(Editor – Live Templates 和 Editor – General – Postfix Completion)

Live Templates(实时代码模板)功能介绍

它的原理就是配置一些常用代码字母缩写,在输入简写时可以出现你预定义的固定模式的代码,使得开发效率大大提高,同时也可以增加个性化。最简单的例子
就是在Java中输入sout会出现System.out.println();

已有的常用模板

Postfix Completion 默认如下

Live Templates默认如下

二者的区别:Live Templates可以自定义,而Postfix Completion不可以。

常用模板

psvm : 可生成main方法

sout : System.out.println()快捷输出

类似的:soutp

soutv=System.out.println("变量名 = " + 变量);

soutm=System.out.println("当前类名.当前方法");

“abc”.sout => System.out.println("abc");

list.for : 可生成集合 list 的 的 for 循环

输入: list.for 即可输出

1
2
for(String s:list){
}

又如:list.forilist.forr

1
List<String> list = new ArrayList<String>();

输入: list.for 即可输出

又如:list.forilist.forr

1
2
for(String s:list){
}

又如:list.forilist.forr

ifn :可生成 if(xxx = null)
类似的:
inn:可生成if(xxx != null)xxx.nnxxx.null

prsf :可生成private static final
类似的:
psf:可生成public static final
psfi:可生成public static final int
psfs:可生成public static final String


自定义模板

IDEA 提供了很多现成的 Templates。但你也可以根据自己的需要创建新的Template

  1. 自定义模板组,并命名为CustomDefine

  2. 在自定义的模板组里点“+”号新建自定义模板

  1. Abbreviation:模板的缩略名称
  2. Description:模板的描述
  3. Template text:模板的代码片段
  4. 应用范围。Define

生成 javadoc

Tools—>Generate JavaDoc…

输入:

1
2
Locale:输入语言类型:zh_CN
Other command line arguments:-encoding UTF-8 -charset UTF-8

IDEA常用快捷键Windows版

作用 快捷键
返回上次编辑位置(Last Edit Location) Ctrl+Shift+Backspace
Back返回上次光标所在位置 Ctrl+Alt+向左箭头
Forward前进到下次光标所在位置 Ctrl+Alt+向右箭头
搜索类Class Ctrl+N
搜索文件File Ctrl+Shift+N
搜索符号Symbol Ctrl+Alt+Shift+N
全局搜索 Ctrl+Shift+F
当前文件搜索 Ctrl+F
执行(run) Shift+F10
提示补全、导包、万能快捷键 Alt+Enter
单行注释 Ctrl+/
多行注释 Ctrl+Shift+/
向下复制一行(Duplicate Entire Lines) Ctrl+D
删除一行或选中行 Ctrl+Y
向下移动行(move statement down) Ctrl+Shift+向下箭头
向上移动行(move statement up) Ctrl+Shift+向上箭头
向下开始新的一行 Shift+Enter
向上开始新的一行 (Start New Line before current) Ctrl+Alt+Enter
查看继承关系(type hierarchy) Ctrl+H
格式化代码(reformat code) Ctrl+Alt+L
提示方法参数类型(Parameter Info) Ctrl+P
反撤销 Ctrl+Shift+Z
选中数行,整体往后移动 Tab
选中数行,整体往前移动 Shift+Tab
查看类的结构:类的方法与变量 Alt+7
大写转小写/ 小写转大写(toggle case) Ctrl+Shift+U
生成构造 器/get/set/toString Alt+Insert
收起所有的方法(collapse all) Ctrl+Shift+减号
打开所有方法(expand all) Ctrl+Shift+加号
打开代码所在硬盘文件夹(show in explorer) Ctrl+Shift+X(需要自己设置)
生成 try-catch 等(surround with) Ctrl+Alt+T
查看方法的多层重写结构(method hierarchy) Ctrl+Shift+H
抽取方法(Extract Method) Ctrl+Alt+M
打开 最近 修改的文件(Recently Files) Ctrl+E
关闭当前打开的代码栏(close) Ctrl+F4
关闭其他所有代码栏(close others) Ctrl+Shift+O(需要自己设置)
快速搜索类中的错误(next highlighted error) F2
选择要粘贴的内容(Show in Explorer) Ctrl+Shift+V
查找方法在哪里被调用(Call Hierarchy) Ctrl+Alt+H

IDEA常用快捷键Mac版

作用 快捷键
返回上次编辑位置(Last Edit Location) command+Shift+Delete
Back返回上次光标所在位置 Command+[
Forward前进到下次光标所在位置 Command+]
搜索类Class Command+O
搜索文件File Command+shift+O
搜索符号Symbol Command+option+O
全局搜索 Command+option+F
当前文件搜索 Command+F
执行(run) option+R
提示补全、导包、万能快捷键 option+Enter
单行注释 Command+/
多行注释 Command+Option+/
向下复制一行(Duplicate Entire Lines) Command+D
删除一行或选中行 Ctrl+Y
向下移动行(move statement down) Command+option+向下箭头
向上移动行(move statement up) command+option+向上箭头
向下开始新的一行 Shift+Enter
向上开始新的一行 (Start New Line before current) Command+option+Enter
查看继承关系(type hierarchy) Ctrl+H
格式化代码(reformat code) Command+option+L
提示方法参数类型(Parameter Info) Command+P
反撤销 Command+Shift+Z
选中数行,整体往后移动 Tab
选中数行,整体往前移动 Shift+Tab
查看类的结构:类的方法与变量 command+7
大写转小写/ 小写转大写(toggle case) Command+Shift+U
生成构造 器/get/set/toString Command+N
收起所有的方法(collapse all) Command+Shift+减号
打开所有方法(expand all) Command+Shift+加号
打开代码所在硬盘文件夹(show in explorer) Command+Shift+X(需要自己设置)
生成 try-catch 等(surround with) Command+option+T
查看方法的多层重写结构(method hierarchy) Command+Shift+H
抽取方法(Extract Method) Command+option+M
打开 最近 修改的文件(Recently Files) Ctrl+E
关闭当前打开的代码栏(close) Command+W
关闭其他所有代码栏(close others) Ctrl+Shift+O(需要自己设置)
快速搜索类中的错误(next highlighted error) F2
选择要粘贴的内容(Show in Explorer) Ctrl+Shift+V
查找方法在哪里被调用(Call Hierarchy) Ctrl+Alt+H

Alt+Enter快捷键功能

自动创建函数


格式化代码


为接口创建实现类和方法


单词拼写

如果idea检查到我们的单词拼错了会有下波浪线的提示,使用alt+enter的快捷键可以帮我们进行修正


导包


寻找代码修改轨迹

annotate

使用annotate,可以在某个文件内部查看哪一行代码是谁提交的,而且还有提交时间、提交备注信息、提交人姓名、提交的版本号、第几次提交等


Revert Changes撤销修改

想撤销对当前文件的修改,在空白处使用快捷键Ctrl+alt+Z


Local History

在IntelliJ IDEA中一不小心将你本地代码给覆盖了,或者忘记了修改某些地方,出现了一些无法修复的异常,这个时候可以通过历史记录回到以前未修改前,并且记录的机制是修改文件会触发记录的时间点,所以很多天以前的记录也能找到

容器

组件添加

@ComponentScan

在配置类上添加@ComponentScan注解。该注解默认会扫描该类所在的包下所有的配置类,相当于之前的 context:component-scan@ComponentScan注解默认就会装配标识了@Controller@Service@Repository@Component注解的类到spring容器中。

basePackages与value: 用于指定包的路径,进行扫描

basePackageClasses:指定扫描类

includeFilters:指定某些定义Filter满足条件的组件 FilterType有5种类型如:

  • ANNOTATION,注解类型(默认)
  • ASSIGNABLE_TYPE,指定固定类
  • ASPECTJ, ASPECTJ类型
  • REGEX,正则表达式
  • CUSTOM,自定义类型

excludeFilters:过滤器,和includeFilters作用刚好相反,用来对扫描的类进行排除的,被排除的类不会被注册到容器中

1
2
3
4
5
6
7
8
@ComponentScan(value="com.wotzc",useDefaultFilters=true,
includeFilters={
@Filter(type=FilterType.ANNOTATION,classes={Controller.class}),
@Filter(type=FilterType.ASSIGNABLE_TYPE,classes={UserService2.class})
})
@Configuration
public class MainScanConfig {
}

@Scope

@Scope注解是springIoc容器中的一个作用域,在 Spring IoC 容器中具有以下几种作用域:基本作用域singleton(单例)默认值、prototype(多例),Web 作用域(reqeust、session、globalsession),自定义作用域

1
2
3
@Scope("prototype")//多实例,IOC容器启动创建的时候,并不会创建对象放在容器在容器当中,当你需要的时候,需要从容器当中取该对象的时候,就会创建。@Scope("singleton")//单实例 IOC容器启动的时候就会调用方法创建对象,以后每次获取都是从容器当中拿同一个对象(map当中)。
@Scope("request")//同一个请求创建一个实例
@Scope("session")//同一个session创建一个实例

@Lazy

Spring IoC (ApplicationContext) 容器一般都会在启动的时候实例化所有单实例 bean 。如果我们想要 Spring 在启动的时候延迟加载 bean,即在调用某个 bean 的时候再去初始化,那么就可以使用@Lazy注解。

1
2
3
4
5
@Lazy
@Bean
public Person person() {
return new Person("李四", 55);
}

@Conditional

@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件就给容器注册bean。

@Conditional的定义:

1
2
3
4
5
6
7
8
9
//此注解可以标注在类和方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

Class<? extends Condition>[] value();

}

从代码中可以看到,需要传入一个Class数组,并且需要继承Condition接口:

1
2
3
4
5
public interface Condition {

boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);

}

Condition是个接口,需要实现matches方法,返回true则注入bean,false则不注入。

首先,创建一个WindowsCondition类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class WindowsCondition implements Condition {

/**
* @param conditionContext:判断条件能使用的上下文环境
* @param annotatedTypeMetadata:注解所在位置的注释信息
*
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {

//获取ioc使用的beanFactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
//获取类加载器
ClassLoader classLoader = conditionContext.getClassLoader();
//获取当前环境信息
Environment environment = conditionContext.getEnvironment();
//获取bean定义的注册类
BeanDefinitionRegistry registry = conditionContext.getRegistry();
//获得当前系统名
String property = environment.getProperty("os.name");

//包含Windows则说明是windows系统,返回true
if (property.contains("Windows")){
return true;
}
return false;
}

}

matches方法的两个参数的意思在注释中讲述了,值得一提的是,conditionContext提供了多种方法,方便获取各种信息,也是SpringBoot中@ConditonalOnXX注解多样扩展的基础。

接着,创建LinuxCondition类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LinuxCondition implements Condition {

@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {

Environment environment = conditionContext.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Linux")){
return true;
}
return false;
}

}

接着就是使用这两个类了,因为此注解可以标注在方法上和类上,

修改BeanConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class BeanConfig {

//只有一个类时,大括号可以省略
//如果WindowsCondition的实现方法返回true,则注入这个bean
@Conditional({WindowsCondition.class})
@Bean(name = "bill")
public Person person1(){
return new Person("Bill Gates",62);
}

//如果LinuxCondition的实现方法返回true,则注入这个bean
@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person2(){
return new Person("Linus",48);
}

}

@Bean

Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。 产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。@Bean明确地指示了一种方法,什么方法呢?产生一个bean的方法,并且交给Spring容器管理;从这我们就明白了为啥@Bean是放在方法的注释上了,因为它很明确地告诉被注释的方法,你给我产生一个Bean,然后交给Spring容器,剩下的你就别管了。记住,@Bean就放在方法上,就是让方法去产生一个Bean,然后交给Spring容器。

为什么要有@Bean注解?

不知道大家有没有想过,用于注册Bean的注解的有那么多个,为何还要出现@Bean注解?

原因很简单:类似@Component , @Repository , @ Controller , @Service 这些注册Bean的注解存在局限性,只能局限作用于自己编写的类,如果是一个jar包第三方库要加入IOC容器的话,这些注解就手无缚鸡之力了,是的,@Bean注解就可以做到这一点!当然除了@Bean注解能做到还有@Import也能把第三方库中的类实例交给spring管理,而且@Import更加方便快捷

@Bean 基本构成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {

@AliasFor("name")
String[] value() default {};

@AliasFor("value")
String[] name() default {};

Autowire autowire() default Autowire.NO;

String initMethod() default "";

String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;

}

value: name属性的别名,在不需要其他属性时使用,也就是说value 就是默认值

name: 此bean 的名称,或多个名称,主要的bean的名称加别名。如果未指定,则bean的名称是带注解方法的名称。如果指定了,方法的名称就会忽略,如果没有其他属性声明的话,bean的名称和别名可能通过value属性配置

autowire : 此注解的方法表示自动装配的类型,返回一个Autowire类型的枚举,装配方式 有三个选项
Autowire.NO(默认设置)
Autowire.BY_NAME
Autowire.BY_TYPE
指定 bean 的装配方式, 根据名称根据类型装配, 一般不设置,采用默认即可。autowire的默认值为No,默认表示不通过自动装配。

bean 的初始化方法, 直接指定方法名称即可,不用带括号

destroyMethod: bean 的销毁方法, 在调用 IoC 容器的close()方法时,会执行到该属性指定的方法。不过,只是单实例的 bean 才会调用该方法,如果是多实例的情况下,不会调用该方法


@Import

@Import只能用在类上@Import通过快速导入的方式实现把实例加入spring的IOC容器中

@Import的三种用法主要包括:

1、直接填class数组方式
2、ImportSelector方式【重点】
3、ImportBeanDefinitionRegistrar方式

第一种用法:直接填class数组

语法如下:

1
2
3
4
@Import({ 类名.class , 类名.class... })
public class TestDemo {

}

对应的import的bean都将加入到spring容器中,这些在容器中bean名称是该类的全类名

第二种用法:ImportSelector方式

这种方式的前提就是一个类要实现ImportSelector接口,假如我要用这种方法,目标对象是Myclass这个类,分析具体如下:

创建Myclass类并实现ImportSelector接口

1
2
3
4
5
6
7
8
public class Myclass implements ImportSelector {

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 方法不要返回null,否则会报空指针异常!
return new String[0];
}
}

分析实现接口的selectImports方法:

  • 1、返回值: 就是我们实际上要导入到容器中的组件全类名
  • 2、参数: AnnotationMetadata表示当前被@Import注解给标注的所有注解信息
1
2
3
4
5
6
public class Myclass implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{"com.wotzc.Test.TestDemo3"};
}
}

第三种用法:ImportBeanDefinitionRegistrar方式

同样是一个接口,类似于第二种ImportSelector用法,相似度80%,只不过这种用法比较自定义化注册,具体如下:

第一步:创建Myclass2类并实现ImportBeanDefinitionRegistrar接口

1
2
3
4
5
6
7
public class Myclass2 implements ImportBeanDefinitionRegistrar {
//该实现方法默认为空
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {

}
}

参数分析:

  • 第一个参数:annotationMetadata 和之前的ImportSelector参数一样都是表示当前被@Import注解给标注的所有注解信息
  • 第二个参数表示用于注册定义一个bean

@FactoryBean

该类是SpringIOC容器是创建Bean的一种形式,这种方式创建Bean会有加成方式,融合了简单的工厂设计模式于装饰器模式
Interface to be implemented by objects used within a {@link BeanFactory} which are themselves factories for individual objects. If a bean implements this interface, it is used as a factory for an object to expose, not directly as a bean instance that will be exposed itself.

在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在<bean>中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占有重要的地位。

1
2
3
4
5
6
7
8
9
public interface FactoryBean<T> {

//返回的对象实例
T getObject() throws Exception;
//Bean的类型
Class<?> getObjectType();
//true是单例,false是非单例 在Spring5.0中此方法利用了JDK1.8的新特性变成了default方法,返回true
boolean isSingleton();
}

@PostConstruct

在bean创建完成并且属性赋值完成,来执行初始化方法

1
2
3
4
5

@PostConstruct
public void someMethod(){

}

@PreDestroy

容器移除对象之前调用,@PostConstruct@PreDestroy都只能用在方法上

BeanPostProcessor

  • BeanPostProcessor也称为Bean后置处理器,它是Spring中定义的接口,在Spring容器的创建过程中(具体为Bean初始化前后)会回调BeanPostProcessor中定义的两个方法。BeanPostProcessor的源码如下
1
2
3
4
5
6
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}
  • 其中postProcessBeforeInitialization方法会在每一个bean对象的初始化方法调用之前回调;postProcessAfterInitialization方法会在每个bean对象的初始化方法调用之后被回调。

组件赋值

@Value

不通过配置文件的注入属性的情况,通过@Value将外部的值动态注入到Bean中

使用的情况有:

  1. 基本数值

    1
    2
    @Value("normal")
    private String normal; // 注入普通字符串
  1. 可以写SpEL获取某个bean的属性:#{}

    1
    2
    @Value("#{20-2}")
    private Integer age;
  2. 可以写${}注入配置文件的值

通过@Value将外部配置文件的值动态注入到Bean中。配置文件主要有两类:

  • application.properties。application.properties在spring boot启动时默认加载此文件
  • 自定义属性文件。自定义属性文件通过@PropertySource加载。@PropertySource可以同时加载多个文件,也可以加载单个文件。
1
2
3
# 自己配置的参数
savePath : /Users/a/Desktop/test998/
libraryPath : /opt/local/share/OpenCV/java/libopencv_java347.dylib
1
2
3
4
5
6
@Service
public class TesseractOrcServiceImpl implements TesseractOrcService {

@Value("${savePath}")
private String savePath ;
}

@Autowired

@autowired注释来源于英文单词 autowire,这个单词的意思是自动装配的意思。自动装配又是什么意思?这个词语本来的意思是指的一些工业上的用机器代替人口,自动将一些需要完成的组装任务,或者别的一些任务完成。而在 Spring 的世界当中,自动装配指的就是使用将 Spring 容器中的 bean 自动的和我们需要这个 bean 的类组装在一起。

@Autowired是用在JavaBean中的注解,通过byType形式,用来给指定的字段或方法注入所需的外部资源

自动装备一定要将属性赋值好,没有就会报错。

默认情况下,@Autowired注释意味着依赖是必须的,它类似于@Required注释,然而,你可以使用@Autowired(required=false) 选项关闭默认行为。

即使你不为 age 属性传递任何参数,下面的示例也会成功运行,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tutorialspoint;
import org.springframework.beans.factory.annotation.Autowired;
public class Student {
private Integer age;
private String name;
@Autowired(required=false)
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
@Autowired
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

@Autowired可以标注在方法、参数、构造器、属性上,都是从容器中获取参数组件的值

标在构造器上,如果组件只有一个有参构造器,这个有参构造器的@Autowired可以省略

@Bean标注的方法创建对象的时候,方法参数的值从容器中获取

@Qualifier

可能会有这样一种情况,当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。

@Primary

当一个接口有2个不同实现时,使用@Autowired注解时会报org.springframework.beans.factory.NoUniqueBeanDefinitionException异常信息。

Primary可以理解为默认优先选择,不可以同时设置多个,内部实质是设置BeanDefinition的primary属性

注解 备注
@Primary 优先方案,被注解的实现,优先被注入
@Qualifier 先声明后使用,相当于多个实现起多个不同的名字,注入时候告诉我你要注入哪个

@Resource

可以和@Autowired一样实现自动装配,默认是按组件名称进行装配的

@Resource 注释使用一个 ‘name’ 属性,该属性以一个 bean 名称的形式被注入。你可以说,它遵循 by-name 自动连接语义

如果没有明确地指定一个 ‘name’,默认名称源于字段名或者 setter 方法。在字段的情况下,它使用的是字段名;在一个 setter 方法情况下,它使用的是 bean 属性名称。

@Inject

  • @Inject是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject;实现注入。
  • @Inject是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named;
  • @Inject可以作用在变量、setter方法、构造函数上。

@Resource,@Autowired,@Inject这3种都是用来注入bean的,它们属于不同的程序中。

img

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

@Resource 没有支持@Primary功能;没有支持@Autowired(required = false);

@Inject 需要导入javax.inject的包;没有支持@Autowired(required = false);

@Profile

指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件

  1. 加了环境标识的bean,只有这个环境被激活的时候才能注册到容器中。默认是default环境

  2. 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Value("${db.user}")
private String user;

private String driverClass;

@Profile("default")
@Bean("test")
public DataSource testDataSource(@Value("${db.password}")String password) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(password);
dataSource.setDriverClass(driverClass);
return dataSource;
}

@Profile("dev")
@Bean("dev")
public DataSource devDataSource(@Value("${db.password}")String password) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(password);
dataSource.setDriverClass(driverClass);
return dataSource;
}

@Profile("master")
@Bean("master")
public DataSource masterDataSource(@Value("${db.password}")String password) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(password);
dataSource.setDriverClass(driverClass);
return dataSource;
}

切换运行环境的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class IOCTestProfile {
//1. 使用命令行动态参数:在虚拟机参数位置加载 -Dspring.profiles.active=test
//2. 使用代码的方式激活某种环境;
@Test
public void test01() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfProfile.class);
//1. 创建一个applicationContext
//2. 设置需要激活的环境
applicationContext.getEnvironment().setActiveProfiles("dev","master");
//3. 注册主配置类
applicationContext.register(MainConfigOfProfile.class);
//4. 启动刷新容器
applicationContext.refresh();

String[] beanNamesForType = applicationContext.getBeanNamesForType(DataSource.class);
System.out.println(Arrays.toString(beanNamesForType));

applicationContext.close();
}


AOP

@EnableAspectJAutoProxy

开启AOP功能

@Pointcut

通过@Pointcut定义切入点

格式:@Pointcut(value=“表达标签(表达式格式) ”)
如:@Pointcut (value=“execution(* com.cn.spring.aspectj.NotVeryUsefulAspectService.*(…))”)

表达式标签

  • execution():用于匹配方法执行的连接点
  • args(): 用于匹配当前执行的方法传入的参数为指定类型的执行方法
  • this(): 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
  • target(): 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
  • within(): 用于匹配指定类型内的方法执行;
  • @args():于匹配当前执行的方法传入的参数持有指定注解的执行;
  • @target():用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
  • @within():用于匹配所以持有指定注解类型内的方法;
  • @annotation:用于匹配当前执行方法持有指定注解的方法;

其中execution是用的最多的,execution格式:

execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
其中带?号的modifiers-pattern?declaring-type-pattern?hrows-pattern?是可选项
ret-type-patternname-patternparameters-pattern是必选项;

  • modifier-pattern? 修饰符匹配,如public 表示匹配公有方法
  • ret-type-pattern 返回值匹配* 表示任何返回值,全路径的类名等
  • declaring-type-pattern? 类路径匹配
  • name-pattern 方法名匹配,*代表所有,set*代表以set开头的所有方法
  • (param-pattern)参数匹配,指定方法参数(声明的类型),
    (..)代表所有参数,
    (*)代表一个参数,
    (*,String)代表第一个参数为任何值,第二个为String类型.
  • throws-pattern? 异常类型匹配

例子

  • execution(public * *(..)) 定义任意公共方法的执行
  • execution(* set*(..)) 定义任何一个以”set”开始的方法的执行
  • execution(* com.xyz.service.AccountService.*(..)) 定义AccountService 接口的任意方法的执行
  • execution(* com.xyz.service..(..)) 定义在service包里的任意方法的执行
  • execution(* com.xyz.service ...(..)) 定义在service包和所有子包里的任意类的任意方法的执行
  • execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…)) 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:

AspectJ类型匹配的通配符:

  • *匹配任何数量字符;
  • ..匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
  • +匹配指定类型的子类型;仅能作为后缀放在类型模式后边 。

如:

  • java.lang.String匹配String类型;
  • java.*.String匹配java包下的任何“一级子包”下的String类型;如匹配java.lang.String,但不匹配java.lang.ss.String
  • java..* 匹配java包及任何子包下的任何类型; 如匹配java.lang.Stringjava.lang.annotation.Annotation
  • java.lang.*ing匹配任何java.lang包下的以ing结尾的类型;
  • java.lang.Number+匹配java.lang包下的任何Number的自类型;如匹配java.lang.Integer,也匹配java.math.BigIntege

@Aspect

定义切面类

@Before

用@Before标识的方法为前置方法,在目标方法的执行之前执行,即在连接点之前进行执行。

1
2
3
4
5
@Before(value = "pointcut1()")
public void before(JoinPoint joinPoint) {
//输出连接点的信息
System.out.println("前置通知," + joinPoint);
}

@After

后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。

@AfterReturning

当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。返回通知方法在目标方法执行成功后才会执行,所以,返回通知方法可以拿到目标方法(连接点方法)执行后的结果。

@AfterThrowing

异常通知方法只在连接点方法出现异常后才会执行,否则不执行。在异常通知方法中可以获取连接点方法出现的异常。在切面类中异常通知方法

1
2
3
4
5
@AfterThrowing(value = "pointcut1()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
//发生异常之后输出异常信息
System.out.println(joinPoint + ",发生异常:" + e.getMessage());
}

@Around

环绕通知方法可以包含上面四种通知方法,环绕通知的功能最全面。环绕通知需要携带ProceedingJoinPoint类型的参数,且环绕通知必须有返回值, 返回值即为目标方法的返回值。在切面类中创建环绕通知方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Around("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public Object aroundMethod(ProceedingJoinPoint pdj){
/*result为连接点的放回结果*/
Object result = null;
String methodName = pdj.getSignature().getName();

/*前置通知方法*/
System.out.println("前置通知方法>目标方法名:" + methodName + ",参数为:" + Arrays.asList(pdj.getArgs()));

/*执行目标方法*/
try {
result = pdj.proceed();

/*返回通知方法*/
System.out.println("返回通知方法>目标方法名" + methodName + ",返回结果为:" + result);
} catch (Throwable e) {
/*异常通知方法*/
System.out.println("异常通知方法>目标方法名" + methodName + ",异常为:" + e);
}

/*后置通知*/
System.out.println("后置通知方法>目标方法名" + methodName);

return result;
}
}

声明式事务

@EnableTransactionManagement

开启基于注解的事务管理功能

注解原理:

@EnableXXX原理:注解上有个XXXRegistrar,或通过XXXSelector引入XXXRegistrarXXXRegistrar实现了
ImportBeanDefinitionRegistrarregisterBeanDefinitions方法,给容器注册XXXCreator。这个Creator实现了后置处理器, 后置处理器在对象创建以后,包装对象,返回一个代理对象,代理对象执行方法利用拦截器链进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
* 1)、@EnableTransactionManagement
* 利用TransactionManagementConfigurationSelector给容器中会导入组件
* 导入两个组件
* AutoProxyRegistrar
* ProxyTransactionManagementConfiguration
* 2)、AutoProxyRegistrar:
* 给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件;
* 利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用;
* 3)、ProxyTransactionManagementConfiguration是个@Configuration
* 1、给容器中注册事务增强器transactionAdvisor;
* 1)、事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解
* 2)、事务拦截器transactionInterceptor:
* TransactionInterceptor;保存了事务属性信息,事务管理器;
* TransactionInterceptor是一个 MethodInterceptor;
* 在目标方法执行的时候;
* 执行拦截器链;
* 只有事务拦截器:
* 1)、先获取事务相关的属性
* 2)、再获取PlatformTransactionManager,如果事先没有添加指定任何transactionmanger
* 最终会从容器中按照类型获取一个PlatformTransactionManager;
* 3)、执行目标方法
* 如果异常,获取到事务管理器,利用事务管理回滚操作;
* 如果正常,利用事务管理器,提交事务

@Transactional

@Target({ ElementType.METHOD, ElementType.TYPE })可以标注在类上,以及方法上

@Transactional注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。
系统设计:将标签放置在需要进行事务管理的方法上,而不是放在所有接口实现类上:只读的接口就不需要事务管理,由于配置了@Transactional就需要AOP拦截及事务的处理,可能影响系统性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

boolean readOnly() default false;

Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class<? extends Throwable>[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

}

propagation属性:事务的传播行为,一个事务方法被另外一个事务方法调用时,当前的事务如何使用事务.,属性的内容主要先择在一下的Propagation的枚举类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

1. TransactionDefinition.PROPAGATION_REQUIRED:
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。

2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:
创建一个新的事务,如果当前存在事务,则把当前事务挂起。

3. TransactionDefinition.PROPAGATION_SUPPORTS:
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
以非事务方式运行,如果当前存在事务,则把当前事务挂起。

5. TransactionDefinition.PROPAGATION_NEVER:
以非事务方式运行,如果当前存在事务,则抛出异常。

6. TransactionDefinition.PROPAGATION_MANDATORY:
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

7. TransactionDefinition.PROPAGATION_NESTED:
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

isolation属性:事务的隔离级别,也在org.springframework.transaction.annotation.Isolation枚举类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public enum Isolation {

/**数据库的默认级别*/
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

/**读未提交 脏读*/
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

/**读已提交 不可重复读(update)*/
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

/**可重复读 幻读(插入操作)*/
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

/** 串行化 效率低*/
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);


private final int value;

Isolation(int value) { this.value = value; }

public int value() { return this.value; }

}

timeout:事物的超时时间,设置事务在强制回滚之前可以占用的时间,默认为-1,不超时,单位为s(测试为单位s)

readOnly:是否只读

  1. true: 只读 ;代表着只会对数据库进行读取操作, 不会有修改的操作,如果确保当前的事务只有读取操作,就有必要设置为只读,可以帮助数据库,引擎优化事务
  2. false: 非只读 不仅会读取数据还会有修改操作

扩展原理

BeanFactoryPostProcessorbeanFactory的后置处理器,在BeanFactory标准初始化之后调用,所有的bean定义已经保存加载到beanFactory,但是bean实例还未创建。

BeanFactoryPostProcessor原理

  1. ioc容器创建对象

  2. invokeBeanFactoryPostProcessors(beanFactory)

    如何找到所有的BeanFactoryPostProcessor并执行他们的方法:

1)直接在BeanFactory中找到所有类型是BeanFactoryPostProcessor的组件,并执行他们的方法

2)在初始化创建其他组件前面执行

BeanDefinitionRegistryPostProcessor

BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

/**
* Modify the application context's internal bean definition registry after its
* standard initialization. All regular bean definitions will have been loaded,
* but no beans will have been instantiated yet. This allows for adding further
* bean definitions before the next post-processing phase kicks in.
* @param registry the bean definition registry used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

}

postProcessBeanDefinitionRegistry();在所有bean定义信息将要被加载,bean实例还未创建的时候调用

优于BeanFactoryPostProcessor执行

利用BeanDefinitionRegistryPostProcessor给容器再额外添加一些组件

原理:

1)ioc创建对象

2)refresh() -》invokeBeanFactoryPostProcessors(beanFactory)

3)从容器或获取到所有的BeanDefinitionRegistryPostProcessor组件。依次触发所有的postProcessBeanDefinitionRegistry()方法 ,再触发postProcessBeanFactory方法BeanFactoryPostProcessor

4)再从容器中找到BeanFactoryPostProcessor组件,然后依次触发postProcessBeanFactory()方法

ApplicationListener

ApplicationListener:监听容器中发布的事件。事件驱动模型开发

1
public interface ApplicationListener<E extends ApplicationEvent>

监听ApplicationEvent及其下面的子事件

步骤:

1)写一个监听器来监听某个事件(ApplicationEvent及其子类)

2)把监听器加入到容器中

3)只要容器中有相关的事件发布,我们就能监听到这个事件

ContextRefreshedEvent:容器刷新完成(所有Bean都完全创建)会发布这个事件

ContextClosedEvent:关闭容器会发布这个事件

4)发布一个事件:

applicationContext.publishEvent();

配置文件

properties

  • SpringBoot自动加载特地目录下的application.properties配置文件,在实际的使用中,一般放在resources文件夹下。
  • application.properties采用key=value配置形式

SpringBoot的设计思想是约定大于配置,让开发人员从Spring繁琐的XML配置中解放出来。当然springboot 还通过Java Config。propertiesyml是SpringBoot项目常用的两种文件配置方式。SpringBoot项目默认加载以下目录下的application.properties文件。在实际的使用中,一般放在resources文件夹下。

参考资料
SpringApplication loads properties from application.properties files in the following locations and adds them to the Spring Environment:
A /config subdirectory of the current directory The current directory A classpath /config package The classpath root

yaml

YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:”Yet Another Markup Language”(仍是一种标记语言)。

非常适合用来做以数据为中心的配置文件。

基本语法

  • key: valuekv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • #表示注释
  • 字符串无需加引号,如果要加''""表示字符串内容会被转义/不转义

数据类型

●字面量:单个的、不可再分的值。date、boolean、string、number、null

1
k: v

●对象:键值对的集合。map、hash、set、object

1
2
3
4
5
6
7
#行内写法:  
k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3

●数组:一组按次序排列的值。array、list、queue

1
2
3
4
5
6
7
#行内写法:
k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class Person {

private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
private String name;
private Double weight;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

配置提示

自定义的类和配置文件绑定一般没有提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

WEB开发

SpringMVC自动配置概览

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
  • 内容协商视图解析器和BeanName视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document)).
    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    • 注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).
    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).
    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.
    • 静态index.html 页支持
  • Custom Favicon support (covered later in this document).
    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC


静态资源访问

静态资源访问

By default, Spring Boot serves static content from a directory called /static (or /public or
/resources or /META-INF/resources) in the classpath.

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**。

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面

改变默认的静态资源路径

1
2
3
4
5
6
spring:
mvc:
static-path-pattern: /res/**
#改变默认的静态资源路径
resources:
static-locations: [classpath:/haha/]

静态资源访问前缀

默认无前缀

1
2
3
spring:
mvc:
static-path-pattern: /res/**

当前项目 + res + 静态资源名 = 静态资源文件夹下找

欢迎页

Spring Boot supports both static and templated welcome pages. It first looks for an index.html file in
the configured static content locations. If one is not found, it then looks for an index template. If
either is found, it is automatically used as the welcome page of the application.

静态资源路径下 index.html

  • 可以配置静态资源路径
  • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问

自定义Favicon

favicon.ico 放在静态资源目录下即可。

请求参数处理

普通参数与基本注解

注解:
@PathVariable@RequestHeader@RequestParam@CookieValue@RequestBody

注解 作用
@PathVariable 通过 @PathVariable 可以将URL中占位符参数{xxx}绑定到处理器类的方法形参中@PathVariable(“xxx“)
@RequestHeader 是获取请求头中的数据,通过指定参数 value 的值来获取请求头中指定的参数值。**
@RequestParam @RequestParam主要用于将请求参数区域的数据映射到控制层方法的参数上
@CookieValue @CookieValue注解主要是将请求的Cookie数据,映射到功能处理方法的参数上。
@RequestBody 主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。

@PathVariable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){

Map<String,Object> map = new HashMap<>();

// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}

关于AvalonJs

avalon是一个简单易用迷你的MVVM框架,它最早发布于2012.09.15,为解决同一业务逻辑存在各种视图呈现而开发出来的。 事实上,这问题其实也可以简单地利用一般的前端模板加jQuery 事件委托 搞定,但随着业务的膨胀, 代码就充满了各种选择器与事件回调,难以维护。因此彻底的将业务与逻辑分离,就只能求助于架构。 最初想到的是MVC,尝试过backbone,但代码不降反升,很偶尔的机会,碰上微软的WPF, 优雅的MVVM架构立即吸引住了作者,作者觉得这就是他一直追求的解决之道。

MVVM将所有前端代码彻底分成两部分,视图的处理通过绑定实现(angular有个更炫酷的名词叫指令), 业务逻辑则集中在一个个叫VM的对象中处理。我们只要操作VM的数据,它就自然而然地神奇地同步到视图。

我们从一个完整的例子开始认识 avalon :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="avalon.js"></script>
</head>
<body>
<div ms-controller="box">
<div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h" ms-click="click"></div>
<p>{{ w }} x {{ h }}</p>
<p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p>
<p>H: <input type="text" ms-duplex="h" /></p>
</div>
<script>
var vm = avalon.define({
$id: "box",//告诉avalon这个Model是作用于哪个ms-controller的
w: 100,
h: 100,
click: function() {
vm.w = parseFloat(vm.w) + 10;
vm.h = parseFloat(vm.h) + 10;
}
})
</script>
</body>
</html>

上面的代码中,我们可以看到在JS中,没有任何一行操作DOM的代码,也没有选择器,非常干净。在HTML中, 我们发现就是多了一些以ms-开始的属性与 {{ }} 标记,有的是用于渲染样式, 有的是用于绑定事件。这些属性或标记,实质就是avalon的绑定系统的一部分。绑定(有的框架也将之称为指令), 负责帮我们完成视图的各种操作,相当于一个隐形的jQuery。正因为有了绑定,我们就可以在JS代码专注业务逻辑本身, 写得更易维护的代码!


视图模型

视图模型,ViewModel,也经常被略写成VM,是通过avalon.define方法进行定义。生成的对象会默认放到avalon.vmodels对象上。 每个VM在定义时必须指定$id。如果你有某些属性不想监听,可以直接将此属性名放到$skipArray数组中。

接着我们说一些重要的概念:

  • $id, 每个VM都有$id,VM的ID,方便在avalon.vmodels里查找到它,或用在ms-controller、ms-important上。如果VM的某一个属性是对象(并且它是可监控的),也会转换为一个VM,这个子VM也会默认加上一个$id。 但只有用户添加的那个最外面的$id会注册到avalon.vmodels对象上。
  • 监控属性,一般地,VM中的属性都会转换为此种属性,当我们以vm.aaa = yyy这种形式更改其值时,就会同步到视图上的对应位置上。
  • 计算属性,定义时为一个对象,并且只存在set,get两个函数或只有一个get一个函数。它是监控属性的高级形式,表示它的值是通过函数计算出来的,是依赖于其他属性合成出来的。
  • 监控数组,定义时为一个数组,它会添加了许多新方法,但一般情况下与普通数组无异,但调用它的push, unshift, remove, pop等方法会同步视图。
  • 非监控属性,这包括框架添加的$id属性,以$开头的属性,放在$skipArray数组中的属性,值为函数、元素节点、文本节点的属性,总之,改变它们的值不会产生同步视图的效果。

$skipArray 是一个字符串数组,只能放当前对象的直接属性名,想禁止子对象的某个属性的监听,在那个子对象上再添加一个$skipAray数组就行了。

视图里面,我们可以使用ms-controller, ms-important指定一个VM的作用域。

此外,在ms-each, ms-with,ms-repeat绑定属性中,它们会创建一个临时的VM,我们称之为代理VM, 用于放置$key, $val, $index, $last, $first, $remove等变量或方法。

另外,avalon不允许在VM定义之后,再追加新属性与方法,比如下面的方式是错误的:

1
2
3
4
5
6
7
var vm = avalon.define({
$id: "test",
test1: "点击测试按钮没反应 绑定失败";
});
vm.one = function() {
vm.test1 = "绑定成功";
};

也不允许在define里面直接调用方法或ajax。

我们再看看如何更新VM中的属性(重点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<script>
var model : avalon.define({
$id: "update",
aaa : "str",
bbb : false,
ccc : 1223,
time : new Date,
simpleArray : [1, 2, 3, 4],
objectArray : [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}],
object : {
o1: "k1",
o2: "k2",
o3: "k3"
},
simpleArray : [1, 2, 3, 4],
objectArray : [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}],
object : {
o1: "k1",
o2: "k2",
o3: "k3"
}
})

setTimeout(function() {
//如果是更新简单数据类型(string, boolean, number)或Date类型
model.aaa = "这是字符串"
model.bbb = true
model.ccc = 999999999999
var date = new Date
model.time = new Date(date.setFullYear(2005))
}, 2000)

setTimeout(function() {
//如果是数组,注意保证它们的元素的类型是一致的
//只能全是字符串,或是全是布尔,不能有一些是这种类型,另一些是其他类型
//这时我们可以使用set方法来更新(它有两个参数,第一个是index,第2个是新值)
model.simpleArray.set(0, 1000)
model.simpleArray.set(2, 3000)
model.objectArray.set(0, {name: "xxxxxxxxxxxxxxxx", value: "xxx"})
}, 2500)
setTimeout(function() {
model.objectArray[1].name = "5555"
}, 3000)
setTimeout(function() {
//如果要更新对象,直接赋给它一个对象,注意不能将一个VM赋给它,可以到VM的$model赋给它(要不会在IE6-8中报错)
model.object = {
aaaa: "aaaa",
bbbb: "bbbb",
cccc: "cccc",
dddd: "dddd"
}
}, 3000)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <div ms-controller="update">
<div>{{aaa}}</div>
<div>{{bbb}}</div>
<div>{{ccc}}</div>
<div>{{time | date("yyyy - MM - dd mm:ss")}}</div>
<ul ms-each="simpleArray">
<li>{{el}}</li>
</ul>
<div> <select ms-each="objectArray">
<option ms-value="el.value">{{el.name}}</option>
</select>
</div>
<ol ms-with="object">
<li>{{$key}} {{$val}}</li>
</ol>
</div>

绑定

avalon的绑定(或指令),拥有以下三种类型:

  • {{ }} 插值表达式, 这是开标签与闭标签间,换言之,也是位于文本节点中,innerText里。 {{ }} 里面可以添加各种过滤器(以|进行标识)。值得注意的是 {{ }} 实际是文本绑定(ms-text)的一种形式。
  • ms-绑定属性, 这是位于开标签的内部, 95%的绑定都以这种形式存在。 它们的格式大概是这样划分的“ms” + type + “-“ + param1 + “-“ + param1 + “-“ + param2 + … + number = value
1
2
3
4
5
6
7
8
9
10
11
12
ms-skip                //这个绑定属性没有值
ms-controller="expr" //这个绑定属性没有参数
ms-if="expr" //这个绑定属性没有参数
ms-if-loop="expr" //这个绑定属性有一个参数
ms-repeat-el="array" //这个绑定属性有一个参数
ms-attr-href="xxxx" //这个绑定属性有一个参数
ms-attr-src="xxx/{{a}}/yyy/{{b}}" //这个绑定属性的值包含插值表达式,注意只有少部分表示字符串类型的属性可以使用插值表达式
ms-click-1="fn" //这个绑定属性的名字最后有数字,这是方便我们绑定更多点击事件 ms-click-2="fn" ms-click-3="fn"
ms-on-click="fn" //只有表示事件与类名的绑定属性的可以加数字,如这个也可以写成 ms-on-click-0="fn"
ms-class-1="xxx" ms-class-2="yyy" ms-class-3="xxx" //数字还表示绑定的次序
ms-css-background-color="xxx" //这个绑定属性有两个参数,但在css绑定里,相当于一个,会内部转换为backgroundColor
ms-duplex-aaa-bbb-string="xxx"//这个绑定属性有三个参数,表示三种不同的拦截操作
  • data-xxx-yyy=”xxx”,辅助指令,比如ms-duplex的某一个辅助指令为data-duplex-event=”change”,ms-repeat的某一个辅助指令为data-repeat-rendered=”yyy”

指令

avalon的指令是一个非常重要的东西,它用来引入一些新的HTML语法, 使元素拥有特定的行为。 举例来说,静态的HTML不知道如何来创建和展现一个日期选择器控件。 让HTML能识别这个语法,我们需要使用指令。 指令通过某种方法来创建一个能够支持日期选择的元素。

指令一共拥有3种形式

  1. 插值表达式
  2. 自定义标签
  3. 绑定属性

其中绑定属性的种类是最多的,它们都位置于元素节点中,以ms-开头或以:开头(avalon2.1.7新增)

绑定属性的属性名是以-分成几段 其中第二个就是指令的名字, 如ms-css, ms-attr, ms-html, ms-text, ms-on都是来源于jQuery同名方法名, 简单好记.

1
<p ms-on-click="@clickFn" ms-if="@toggle">{{@name}}</p>


插值表达式

位于文本节点中的双重花括号,当然这个可以配置.此指令其中文本ms-text指令的简单形式.

1
2
3
4
5
6
7
8
9
10
11
12
13
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: 'aaa',
bbb: 'bbb'
})

</script>
<p>{{@aaa}}{{@bbb}} 这个性能差些</p>
<p>{{@aaa+@bbb}} 这个性能好些</p>
<p>{{@aaa+@bbb | uppercase}} 选择器必须放在表达值的后端</p>
</body>

{{ }} 里面可以添加各种过滤器(以|进行标识)。值得注意的是 {{ }} 实际是文本绑定(ms-text)的一种形式。

忽略扫描绑定(ms-skip)

这是ms-skip负责。只要元素定义了这个属性,无论它的值是什么,它都不会扫描其他属性及它的子孙节点了。

1
2
3
4
5
6
7
8
9
<div ms-controller="test" ms-skip>
<p
ms-repeat-num="cd"
ms-attr-name="num"
ms-data-xxx="$index">
{{$index}} - {{num}}
</p>
A:<div ms-each="arr">{{yy}}</div>
</div>

ms-controller

这个指令是用于圈定某个VM的作用域范围(换言之,这个元素的outerHTML会被扫描编译,所有ms-*及双花括号替换成vm中的内容),ms-controller的属性值只能是某个VM的$id

ms-controller的元素节点下面的其他节点也可以使用ms-controller

每个VM的$id可以在页面上出现一次, 因此不要在ms-for内使用ms-controller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
avalon.define({
$id: "AAA",
name: "liger",
color: "green"  
});
avalon.define({
$id: "BBB",
name: "sphinx",
color: "red"  
});  
avalon.define({
$id: "CCC",
name: "dragon" //不存在color

});
avalon.define({
$id: "DDD",
name: "sirenia" //不存在color

}); 
</script>
<div ms-controller="AAA">
<div>{{@name}} : {{@color}}</div>
<div ms-controller="BBB">
<div>{{@name}} : {{@color}}</div>
<div ms-controller="CCC">
<div>{{@name}} : {{@color}}</div>
</div>
<div ms-important="DDD">
<div>{{@name}} : {{@color}}</div>
</div>
</div>
</div>

当avalon的扫描引擎打描到ms-controller/ms-important所在元素时, 会尝试移除ms-controller类名.因此基于此特性,我们可以在首页渲染页面时, 想挡住双花括号乱码问题,可以尝试这样干(与avalon1有点不一样):

1
2
3
.ms-controller{
visibility: hidden;
}

ms-important

这个指令是用于圈定某个VM的作用域范围(换言之,这个元素的outerHTML会被扫描编译,所有ms-*及双花括号替换成vm中的内容),ms-important的属性值只能是某个VM的$id

ms-important的元素节点下面的其他节点也可以使用ms-controller或ms-important

与ms-controller不一同的是,当某个属性在ms-important的VM找不到时,就不会所上寻找

不要在ms-for内使用ms-important.

ms-important这特性有利协作开发,每个人的VM都不会影响其他人,并能大大提高性能

ms-important只能用于ms-controller的元素里面

1
2
3
4
5
6
7
8
9
10
<div ms-important='aaa'>
<div ms-controller='ccc'>
<div ms-important='ddd'>

</div>
</div>
<div ms-controller='bbb'>

</div>
</div>

ms-attr

属性绑定用于为元素节点添加一组属性, 因此要求属性值为对象或数组形式. 数组最后也会合并成一个对象.然后取此对象的键名为属性名, 键值为属性值为元素添加属性

如果键名如果为for, char这样的关键字,请务必在两边加上引号

如果键名如果带横杠,请务必转换为驼峰风格或两边加上引号

注意,不能在ms-attr中设置style属性

1
<p ms-attr="{style:'width:20px'}">这样写是错的,需要用ms-css指令!!</p>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
obj: {title: '普通 ', algin: 'left'},
active: {title: '激活'},
width: 111,
height: 222,
arr: [{img: 'aaa'}, {img: 'bbb'}, {img: 'ccc'}],
path: '../aaa/image.jpg',
toggle: false,
array: [{width: 1}, {height: 2}]
})

</script>
<span ms-attr="@obj">直接引用对象</span>
<img ms-attr="{src: @path}" />
<ul>
<li ms-for="el in @arr"><a ms-attr="{href:'http://www.ccc.xxx/ddd/'+ el.img}">下载</a></li>
</ul>
<span :attr="{width: @width, height: @height}">使用对象字面量</span><br/>
<span :attr="@array">直接引用数组</span><br/>
<span :attr="[@obj, @toggle && @active ]" :click="@toggle = !@toggle">选择性添加多余属性或重写已有属性</span>
</body>

ms-css

CSS绑定用于为元素节点添加一组样式, 因此要求属性值为对象或数组形式. 数组最后也会合并成一个对象.然后取此对象的键名为样式名, 键值为样式值为元素添加样式

如果键名为表示长宽,字体大小这样的样式, 那么键值不需要加单位,会自动加上px

如果键名如果为float,请务必在两边加上引号

如果键名如果为font-size,请务必转换为驼峰风格或两边加上引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
obj: {backgroundColor: '#3bb0d0',width:300, height:50, 'text-align': 'center'},//属性名带-,必须用引号括起
active: {color: 'red'},
width: 300,
height: 60,
toggle: true,
array: [{width:100},{height:50},{border: '1px solid #5cb85c'}]
})

</script>
<div ms-css="@obj">直接引用对象</div>
<div :css="{width: @width, height: @height,background: 'pink'}">使用对象字面量</div>
<div :css="@array">直接引用数组</div>
<div :css="[@obj, @toggle && @active ]" :click="@toggle = !@toggle">选择性添加多余属性或重写已有属性</div>
</body>

需要注意的是 设置背景图片是比较复杂

1
2
<span :css="{background: 'url('+@imageUrl + ') no-repeat center center;'}">图片</span>
<span :css="{backgroundImage: 'url('+@imageUrl + ')'}">图片</span>

ms-text

文本绑定是最简单的绑定,它其实是双花括号插值表达式的一种形式

它要求VM对应的属性的类型为字符串, 数值及布尔, 如果是null, undefined将会被转换为空字符串

1
2
<span ms-text="@aaa">不使用过滤器</span>
<span ms-text="@aaa | uppercase">使用过滤器</span>

ms-html

HTML绑定类似于文本绑定,能将一个元素清空,填上你需要的内容

它要求VM对应的属性的类型为字符串

1
2
<span ms-html="@aaa">不使用过滤器</span>
<span ms-html="@aaa | uppercase">使用过滤器</span>

我们可以通过ms-html异步加载大片内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body :controller="test">
<script>
var vm = avalon.define({
$id: "test",
aaa: "loading..."
})
jQuery.ajax({
url:'action.do',
success: function(data){
vm.aaa = data.html
}
})
</script>
<div ms-html="@aaa"></div>
</body>

ms-duplex

双工绑定是MVVM框架中最强大的指令.react推崇单向数据流,没有双工绑定, 那么需要rudex等额外的库来实现相同的功能.

双工绑定只要用于表单元素上.或当一个div设置了contenteditable为true,也可以用ms-duplex指令.

注意:ms-duplex与ms-checked不能在同时使用于一个元素节点上。

注意:如果表单元素同时绑定了ms-duplex=xxx与ms-click或ms-change,而事件回调要立即得到这个vm.xxx的值,input[type=radio]是存在问题,它不能立即得到当前值,而是之前的值,需要在回调里面加个setTimeout。

各个表单元素的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: 'aaa',
bbb: 'bbb',
ccc: 'ccc'
})

</script>

<input ms-duplex="@aaa"/>{{@aaa}}
<input ms-duplex="@bbb" type="password"/>{{@bbb}}
<textarea ms-duplex="@ccc" /></textarea>{{@ccc}}
</body>

上面有三个控件,text, password, textarea它们都是属于输入型控件, 只要每为控件敲入一个字符, 后面的文本都会立即变化.那是因为它们默认是绑定oninput事件,如果想控件全部输入好,失去焦点时 才同步,那么可以使用change过滤器

1
<input ms-duplex="@aaa | change"/>{{@aaa}}

如果你是做智能提示, 控件是绑定了一个AJAX请求与后端不断交互, 使用oninput事件会太频繁, 使用onchange事件会太迟钝,那么我们可以使用debounce过滤器

1
<input ms-duplex="@aaa | debounce(300)"/>{{@aaa}}

300ms同步一次.

另外,可编辑元素的用法与过滤器与上面三种控件一样.

1
2
<div contenteditable="true" ms-duplex="@aaa | debounce(300)"/></div>
<p>{{@aaa}}</p>

这两个过滤器只能适用于上面的情况.

此外, 控件还有许多种, 像checkbox, radio,它们的同步机制也不一样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: '33',
bbb: ['22']
})

</script>

<input type="radio" value="11" ms-duplex="@aaa"/>
<input type="radio" value="22" ms-duplex="@aaa"/>
<input type="radio" value="33" ms-duplex="@aaa"/>
<input type="checkbox" value="11" ms-duplex="@bbb"/>
<input type="checkbox" value="22" ms-duplex="@bbb"/>
<input type="checkbox" value="33" ms-duplex="@bbb"/>
<p>radio: {{@aaa}}; checkbox:{{@bbb}}</p>
</body>

checkbox与radio是一点击就会更新.radio要求在vm中为一个简单数据类型数据,字符串,数字或布尔. 而checkbox则要求是一个数组.并且在最开始时,ms-duplex会令radio钩上其value值等vm属性的控件, checkbox则可以勾选多个.如此一来,vm中的属性些总是等于radio与checkbox的属性值.但我们也可以让 vm的属性值等于此控件的勾选状态,这时需要用上ms-duplex-checked转换器.

1
2
3
4
5
6
7
8
9
10
11
12
13
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: false,
bbb: false
})

</script>
<input type="radio" ms-duplex-checked="@aaa"/>
<input type="checkbox" ms-duplex-checked="@bbb"/>
<p>radio: {{@aaa}}; checkbox:{{@bbb}}</p>
</body>

最后表单元素还有select控件,它根据其multiple属性分为单选下拉框与复选下拉框, 其在vm中的值与radio,checkbox一样.即单选时,必须是一个简单数据类型, 复选时为一个数组. 在最开始时, 当option元素的value值或innerText(不在value值)与数据相同,它们就会被选上.

1
2
3
4
5
6
7
8
9
10
11
12
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: 'bbb'
bbb: ['bbb','ccc'],
})

</script>
<select :duplex="@aaa"><option>aaa</option><option>bbb</option><option>ccc</option></select>
<select multiple="true" :duplex="@bbb"><option>aaa</option><option>bbb</option><option>ccc</option></select>
</body>
控件 触发时机 数据
text,password,textarea及可编辑元表 oninput,onchange, debounce 简单数据
radio,checkbox onclick 简单数据或数组
select onchange 简单数据或数组

数据转换

上面我们已经提到一个数据转换器ms-duplex-checked了.那只能用于checkbox与radio.

为什么会有这种东西呢?因为无论我们原来的数据类型是什么,跑到表单中都会变成字符串,然后我们通过事件取出来 它们也是字符串,不会主动变回原来的类型.我们需要一种机制保持数据原来的类型,这就是数据转换器.

avalon内置了4种过滤器

  1. ms-duplex-string=”@aaa”
  2. ms-duplex-number=”@aaa”
  3. ms-duplex-boolean=”@aaa”
  4. ms-duplex-checked=”@aaa”

前三个是将元素的value值转换成string, number, boolean(只有为’false’时转换为false)

最后是根据当前元素(它只能是radio或checkbox)的checked属性值转换为vm对应属性的值。

它们都是放在属性名上。当数据从元素节点往vmodel同步时,转换成预期的数据。

1
<input value="11"  ms-duplex-number="@aaa"/>

数据格式化

一般来说,数据格式化是由过滤器实现的,如

1
<input value="11"  ms-duplex="@aaa | uppercase"/>

但这里有一个隐患,可能导致死循环, 因此建议放在事件回调中实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body ms-controller="test">
<script>
var vm = avalon.define({
$id: 'test',
aaa: '111',
bbb: '222',
format1: function(e){//只能输入数字
vm.aaa = e.target.value.replace(/\D/g,'')
},
format1: function(e){//只能输入数字
vm.bbb = avalon.filter.date(e.target.value, 'yyyy-MM-dd')
}
})

</script>

<input :duplex="@aaa" :on-input="@format1"/>{{@aaa}}
<input :duplex="@bbb" :on-change="@format2"/>{{@bbb}}
</body>

数据格式化是放在属性值时,以过滤器形式存在,如

1
2
ms-duplex='@aaa | uppercase'
ms-duplex='@aaa | date('yyyy:MM:dd')'

数据验证

这必须在所有表单元素的上方form元素加上ms-validate指令, 当前元素加上ms-rules才会生效

1
2
3
4
5
6
<form ms-validate="@validation">
<input ms-duplex='@aaa'
ms-rules='require,email,maxlength'
data-maxlength='4'
data-maxlength-message='太长了' >
</form>

详见ms-rules指令

同步后的回调

ms-duplex还有一个回调,data-duplex-changed,用于与事件绑定一样, 默认第一个参数为事件对象。如果传入多个参数,那么使用$event为事件对象占位。

1
<input value="11"  ms-duplex-number="@aaa" data-duplex-changed="@fn"/>

示例

现在我们来一些实际的例子!

全选与非全选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var vm = avalon.define({
$id: "duplex1",
data: [{checked: false}, {checked: false}, {checked: false}],
allchecked: false,
checkAll: function (e) {
var checked = e.target.checked
vm.data.forEach(function (el) {
el.checked = checked
})
},
checkOne: function (e) {
var checked = e.target.checked
if (checked === false) {
vm.allchecked = false
} else {//avalon已经为数组添加了ecma262v5的一些新方法
vm.allchecked = vm.data.every(function (el) {
return el.checked
})
}
}
})

<table ms-controller=" duplex1" border="1">
<tr>
<td><input type="checkbox"
ms-duplex-checked="@allchecked"
data-duplex-changed="@checkAll"/>全选</td>
</tr>
<tr ms-for="($index, el) in @data">
<td><input type="checkbox" ms-duplex-checked="el.checked" data-duplex-changed="@checkOne" />{{$index}}::{{el.checked}}</td>
</tr>
</table>

我们仔细分析其源码,allchecked是用来控制最上面的复选框的打勾情况, 数组中的checked是用来控制下面每个复选框的下勾情况。由于是使用ms-duplex,因此会监听用户行为, 当复选框的状态发生改变时,就会触发data-duplex-changed回调,将当前值传给回调。 但这里我们不需要用它的value值,只用它的checked值。

最上面的复选框对应的回调是checkAll,它是用来更新数组的每个元素的checked属性,因此一个forEach循环赋值就是。

下面的复选框对应的checkOne,它们是用来同步最上面的复选框,只要它们有一个为false上面的复选框就不能打勾, 当它们被打勾了,它们就得循环整个数组,检查是否所有元素都为true,是才给上面的checkall属性置为true。

现在我们学了循环指令,结合它来做一个表格看看。现在有了强大无比的orderBy, limitBy, filterBy, selectBy。 我们做高性能的大表格是得心应手的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 if (!Date.now) {//fix 旧式IE
Date.now = function() {
return new Date - 0;
}
}
avalon.define({
$id: "duplex2",
selected: "name",
options: ["name", "size", "date"],
trend: 1,
data: [
{name: "aaa", size: 213, date: Date.now() + 20},
{name: "bbb", size: 4576, date:Date.now() - 4},
{name: "ccc", size: 563, date: Date.now() - 7},
{name: "eee", size: 3713, date: Date.now() + 9},
{name: "555", size: 389, date: Date.now() - 20}
]
})
<div ms-controller=" duplex2">
<div style="color:red">
<p>本例子用于显示如何做一个简单的表格排序</p>
</div>
<p>
<select ms-duplex="@selected">
<option ms-for="el in @options">{{el}}</option>
</select>
<select ms-duplex-number="@trend">
<option value="1">up</option>
<option value="-1">down</option>
</select>
</p>
<table width="500px" border="1">
<tbody >
<tr ms-for="el in @data | orderBy(@selected, @trend)">
<td>{{el.name}}</td> <td>{{el.size}}</td> <td>{{el.date}}</td>
</tr>
</tbody>
</table>
</div>

我们再来一个文本域与下拉框的联动例子,它只用到ms-duplex,不过两个控件都是绑定同一个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
avalon.define({
$id: "fruit",
options: ["苹果", "香蕉", "桃子", "雪梨", "葡萄",
"哈蜜瓜", "橙子", "火龙果", "荔技", "黄皮"],
selected: "桃子"
})
<div ms-controller=" fruit">
<h3>文本域与下拉框的联动</h3>
<input ms-duplex="@selected" />
<select ms-duplex="@selected" >
<option ms-for="el in @options" ms-attr="{value: el}" >
{{el}}
</option>
</select>
</div>

下拉框三级联动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var map = {
"中国": ["江南四大才子", "初唐四杰", "战国四君子"],
"日本": ["日本武将", "日本城堡", "幕府时代"],
"欧美": ["三大骑士团", "三大魔幻小说", "七大奇迹"],
"江南四大才子": ["祝枝山", "文征明", "唐伯虎", "周文宾"],
"初唐四杰": ["王勃", "杨炯", "卢照邻", "骆宾王"],
"战国四君子": ["楚国春申君黄歇", "齐国孟尝君田文", "赵国平原君赵胜", "魏国信陵君魏无忌"],
"日本武将": ["织田信长", "德川家康", "丰臣秀吉"],
"日本城堡": ["安土城", "熊本城", "大坂城", "姬路城"],
"幕府时代": ["镰仓", "室町", "丰臣", "江户"],
"三大骑士团": ["圣殿骑士团", "医院骑士团", "条顿骑士团"],
"三大魔幻小说": ["冰与火之歌", "时光之轮", "荆刺与白骨之王国"],
"七大奇迹": ["埃及胡夫金字塔", "奥林匹亚宙斯巨像", "阿尔忒弥斯月神殿", "摩索拉斯陵墓", "亚历山大港灯塔", "巴比伦空中花园", "罗德岛太阳神巨像"]
}
var vm = avalon.define({
$id: 'linkage',
first: ["中国", "日本", "欧美"],
second: map['日本'].concat(),
third: map['日本武将'].concat(),
firstSelected: "日本",
secondSelected: "日本武将",
thirdSelected: "织田信长"
})


vm.$watch("firstSelected", function (a) {
vm.second = map[a].concat()
vm.secondSelected = vm.second[0]
})
vm.$watch("secondSelected", function (a) {
vm.third = map[a].concat()
vm.thirdSelected = vm.third[0]
})
<div ms-controller=" linkage">
<h3>下拉框三级联动</h3>
<select ms-duplex="@firstSelected" >
<option ms-for="el in @first" ms-attr="{value:el}" >{{el}}</option>
</select>
<select ms-duplex="@secondSelected" >
<option ms-for="el in @second" ms-attr="{value:el}" >{{el}}</option>
</select>
<select ms-duplex="@thirdSelected" >
<option ms-for="el in @third" ms-attr="{value:el}" >{{el}}</option>
</select>
</div>

这里的技巧在于使用$watch回调来同步下一级的数组与选中项。注意,使用concat方法来复制数组。


ms-for

avalon2.0的ms-for绑定集齐了ms-repeat, ms-each, ms-with的所有功能, 并且更好用, 性能提升七八倍

ms-for可以同时循环对象与数组

1
2
3
<ul>
<li ms-for="el in @aaa">{{el}}</li>
</ul>

现在采用类似angular的语法, in前面如果只有一个变量,那么它就是数组元素或对象的属性名

1
2
3
4
5
6
7
8
vm.aaa = ['aaa','bbb','ccc']
vm.bbb = {a: 1, b: 2, c: 3}
<ul>
<li ms-for="(aaa, el) in @aaa">{{aaa}}-{{el}}</li>
</ul
<ul>
<li ms-for="(k, v) in @bbb">{{k}}-{{v}}</li>
</ul>

依次输出的LI元素内容为0-aaa,1-bbb,2-ccc, a-1,b-2,c-3

in 前面有两个变量, 它们需要放在小括号里,以逗号隔开, 那么分别代表数组有索引值与元素, 或对象的键名与键值, 这个与jQuery或avalon的each方法的回调参数一致。

小括号里面的变量是随便起的,主要能符合JS变量命名规范就行,当然,也不要与window, this这样变量冲突.

1
2
<li ms-for="($index, el) in @arr">{{$index}}-{{el}}</li>
<li ms-for="($key, $val) in @obj">{{$key}}-{{$val}}</li>

写成这样,就与avalon1.*很相像了

ms-for还可以配套data-for-rendered回调,当列表渲染好时执行此方法

1
2
3
<ul>
<li ms-for="el in @arr" data-for-rendered='@fn'>{{el}}</li>
<ul>

fn为vm中的一个函数,用法与ms-on-*差不多,如果不传参,默认第一个参数为事件对象,类型type为rendered, target为当前循环区域的父节点,这里为`ul`元素。并且回调中的this指向vm。

1
{type: 'rendered', target: ULElement }

你也可以在回调里面传入其他东西,使用$event代表事件对象

1
2
3
<ul>
<li ms-for="el in @arr" data-for-rendered="@fn('xxx',$event)">{{el}}</li>
<ul>

如果你想截取数组的一部分出来单独循环,可以用limitBy过滤器, 使用as来引用新数组

1
2
3
<ul>
<li ms-for="el in @aaa | limitBy(10) as items">{{el}}</li>
</ul>

上例是显示数组的前10个元素, 并且将这10个元素存放在items数组中, 以保存过滤或排序结果

使用注释节点实现循环,解决同时循环多个元素的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../dist/avalon.js"></script>
<script>
vm = avalon.define({
$id: 'for4',
arr: [1, 2, 3, 4]
})
</script>
</head>
<body>
<div ms-controller='for4' >
<!--ms-for: el in @arr-->
<p>{{el}}</p>
<p>{{el}}</p>
<!--ms-for-end:-->
</div>
</body>
</html>

avalon 不需要像angular那样要求用户指定trace by或像react 那样使用key属性来提高性能,内部帮你搞定一切

如果你只想循环输出数组的其中一部分,请使用filterBy,只想循环输出对象某一些键值并设置默认值,则用selectBy. 不要在同一个元素上使用ms-for与ms-if,因为这样做会在页面上生成大量的注释节点,影响页面性能

可用于ms-for中的过滤器有limitBy, filterBy, selectby, orderBy

ms-for支持下面的元素节点继续使用ms-for,形成双重循环与多级循环, 但要求双重循环对应的二维数组.几维循环对应几维数组

1
2
3
4
5
vm.array = [{arr: [111,222, 333]},{arr: [111,222, 333]},{arr: [111,222, 333]}]
<p>array的元素里面有子数组,形成2维数组</p>
<ul>
<li ms-for="el in @array"><div ms-for='elem in el.arr'>{{elem}}</div></li>
</ul>

如何双向绑定ms-for中生成的变量?

由于 循环生成的变量前面不带@, 因此就找不到其对应的属性,需要特别处理一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div ms-controller="test">
<div ms-for="(key,el) in @styles">
<label>{{ key }}::{{ el }}</label>
<input type="text" ms-duplex="@styles[key]" >
<!--不能ms-duplex="el"-->
</div>
</div>

<script type="text/javascript">
var root = avalon.define({
$id: "test",
styles: {
width: 200,
height: 200,
borderWidth: 1,
borderColor: "red",
borderStyle: "solid",
backgroundColor: "gray"
}
})
</script>

ms-class

类名绑定

属性绑定用于为元素节点添加几个类名, 因此要求属性值为字符串或字符串数组.

字符串形式下,可以使用空格隔开多个类名

字符串数组形下, 可以在里面使用三元运算符或与或号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body ms-controller="test">
<script>
avalon.define({
$id: 'test',
aaa: "aaa bbb ccc",
bbb: 'ddd',
ccc: ['xxx', 'yyy', 'zzz'],
ddd: 'eee',
toggle: true,
toggle2: false
})

</script>

<span :class="@aaa">直接引用字符串</span>
<span :class="@ccc">直接引用数组</span>
<span :class="[@aaa, @bbb]">使用数组字面量</span>
<span :class="['aaa', 'bbb',(@toggle? 'ccc':'ddd')]">选择性添加类名</span>
<span :class="[@toggle && 'aaa']">选择性添加类名</span>
<span :class="[ @ddd + 4]">动态生成类名</span>
</body>

ms-if

通过属性值决定是否渲染目标元素, 为否时原位置上变成一个注释节点

avalon1.*中ms-if-loop指令已经被废掉,请使用limitBy, selectBy, filterBy过滤器代替相应功能

1
2
3
4
5
6
7
8
9
10
11
<body :controller="test">
<script>
var vm = avalon.define({
$id: "test",
aaa: "这是被隐藏的内容"
toggle: false
})
</script>
<p><button type="button" :click='@toggle = !@toggle'>点我</span></button></p>
<div :if="@toggle">{{@aaa}}</div>
</body>

注意,有许多人喜欢用ms-if做非空处理,这是不对的,因此ms-if只是决定它是否插入DOM树与否,ms-if里面的 **ms-**指令还是会执行.

1
2
3
4
5
6
7
8
9
avalon.define({
$id: 'test',
aaa: {}
})
<div ms-controller="test">
<div ms-if="@aaa.bbb">
{{@aaa.bbb.ccc}}这里肯定会出错
</div>
</div>

正确的做法是,当你知道这里面有非空判定,需要用方法包起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
avalon.define({
$id: 'test',
aaa: {},
show: function(aaa, bbb, ccc){
var obj = aaa[bbb]
if(obj){
return obj[ccc]
}
return ''
}
})
<div ms-controller="test">
<div ms-if="@aaa.bbb">
{{@show(@aaa, 'bbb', 'ccc')}}
</div>
</div>

ms-visible

这是通过修改元素的style.display值改变元素的可见性, 要求属性值对应一个布尔,如果不是布尔, avalon会自动转换值为布尔。

1
2
3
4
5
6
7
8
9
10
11
<body :controller="test">
<script>
var vm = avalon.define({
$id: "test",
aaa: "这是被隐藏的内容"
toggle: false
})
</script>
<p><button type="button" :click='@toggle = !@toggle'>点我</span></p>
<div :visible="@toggle">{{@aaa}}</div>
</body>

ms-on

事件绑定

此绑定为元素添加交互功能,对用户行为作出响应. ms-on-*="xxx"是其使用形式, *代表click, mouseover, touchstart等事件名,只能与小写形式定义, xxx是事件回调本身,可以是方法名,或表达式。 默认,事件回调的第一个参数是事件对象,并进行标准化处理. 如果你是用ms-on-click="@fn(el, 1)"这样的传参方式,第一个传参被你占用, 而你又想用事件对象,可以使用**$event**标识符,即ms-on-click="@fn(el, 1, $event)" 那么第三个参数就是事件对象。

如果你想绑定多个点击事件,可以用ms-on-click-1="@fn(el)", ms-on-click-2="@fn2(el)",ms-on-click-3="@fn3(el)"来添加。

并且,avalon对常用的事件,还做了快捷处理,你可以省掉中间的on。

avalon默认对以下事件做快捷处理:

1
animationend、 blur、 change、 input、 click、 dblclick、 focus、 keydown、 keypress、 keyup、 mousedown、 mouseenter、 mouseleave、 mousemove、 mouseout、 mouseover、 mouseup、 scroll、 submit

此外,avalon2相对avalon1,还做了以下强化:

以前ms-on-*的值只能是vm中的一个函数名ms-on-click="fnName", 现在其值可以是表达式,如ms-on-click="el.open = !el.open", 与原生的onclick定义方式更相近. 以前ms-on-*的函数,this是指向绑定事件的元素本身,现在this是指向vm, 元素本身可以直接从e.target中取得.

ms-on-*会优先考虑使用事件代理方式绑定事件,将事件绑在根节点上!这会带来极大的性能优化! ms-on-*的值转换为函数后,如果发现其内部不存在ms-for动态生成的变量,框架会将它们缓存起来! 添加了一系列针对事件的过滤器 对按键进行限制的过滤器esc,tab,enter,space,del,up,left,right,down 对事件方法stopPropagation, preventDefault进行简化的过滤器stop, prevent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var vm = avalon.define({
$id: "test",
firstName: "司徒",
array: ["aaa", "bbb", "ccc"],
argsClick: function(e, a, b) {
alert([].slice.call(arguments).join(" "))
},
loopClick: function(a, e) {
alert(a + " " + e.type)
},
status: "",
callback: function(e) {
vm.status = e.type
},
field: "",
check: function(e) {
vm.field = e.target.value + " " + e.type
},
submit: function() {
var data = vm.$model
if (window.JSON) {
setTimeout(function() {
alert(JSON.stringify(data))
})
}
}
})
<fieldset ms-controller="test">
<legend>有关事件回调传参</legend>
<div ms-mouseenter="@callback" ms-mouseleave="@callback">{{@status}}<br/>
<input ms-on-input="@check"/>{{@field}}
</div>
<div ms-click="@argsClick($event, 100, @firstName)">点我</div>
<div ms-for="el in @array" >
<p ms-click="@loopClick(el, $event)">{{el}}</p>
</div>
<button ms-click="@submit" type="button">点我</button>
</fieldset>

绑定多个同种事件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var count = 0
var model = avalon.define({
$id: "multi-click",
str1: "1",
str2: "2",
str3: "3",
click0: function() {
model.str1 = "xxxxxxxxx" + (count++)
},
click1: function() {
model.str2 = "xxxxxxxxx" + (count++)
},
click2: function() {
model.str3 = "xxxxxxxxx" + (count++)
}
})
<fieldset>
<legend>一个元素绑定多个同种事件的回调</legend>
<div ms-controller="multi-click">
<div ms-click="@click0" ms-click-1="@click1" ms-click-2="@click2" >请点我</div>
<div>{{@str1}}</div>
<div>{{@str2}}</div>
<div>{{@str3}}</div>
</div>
</fieldset>

回调执行顺序的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 avalon.define({
$id: "xxx",
fn: function() {
console.log("11111111")
},
fn1: function() {
console.log("2222222")
},
fn2: function() {
console.log("3333333")
}
})
<div ms-controller="xxx"
ms-on-mouseenter-3="@fn"
ms-on-mouseenter-2="@fn1"
ms-on-mouseenter-1="@fn2"
style="width:100px;height:100px;background: red;"
>
</div>

avalon已经对ms-mouseenter, ms-mouseleave进行修复,可以在这里与这里了解这两个事件。 到chrome30时,所有浏览器都原生支持这两个事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
avalon.define({
$id: "test",
text: "",
fn1: function (e) {
this.text = e.target.className + " "+ e.type
},
fn2: function (e) {
this.text = e.target.className + " "+ e.type
}
})
.bbb{
background: #1ba9ba;
width:200px;
height: 200px;
padding:20px;
box-sizing:content-box;
}
.ccc{
background: #168795;
width:160px;
text-align: center;
line-height: 160px;
height: 160px;
margin:20px;
box-sizing:content-box;
}
<div class="aaa" ms-controller="test">
<div class="bbb" ms-mouseenter="@fn1" ms-mouseleave="@fn2">
<div class="ccc" >
{{@text}}
</div>
</div>
</div>

最后是mousewheel事件的修改,主要问题是出现firefox上, 它死活也不愿意支持mousewheel,在avalon里是用DOMMouseScroll或wheel实现模拟的。 我们在事件对象通过wheelDelta属性是否为正数判定它在向上滚动。

1
2
3
4
5
6
7
8
9
10
11
12
 avalon.define({
$id: "event4",
text: "",
callback: function(e) {
this.text = e.wheelDelta + " " + e.type
}
})
<div ms-controller="event4">
<div ms-on-mousewheel="@callback" id="aaa" style="background: #1ba9ba;width:200px;height: 200px;">
{{@text}}
</div>
</div>

此外avalon还对input,animationend事件进行修复,大家也可以直接用avalon.bind, avalon.fn.bind来绑定这些事件。但建议都用ms-on绑定来处理。


ms-rules

验证规则绑定

avalon2砍掉了不少功能(如ms-include,ms-data),腾出空间加了其他更有用的功能。 数据验证就是其中之一。现在avalon2内置的验证指令是参考之前的oniui验证框架与jquery validation。

此指令只能用于添加ms-duplex指令的表单元素上。

avalon内置验证规则有

规则 描述
required(true) 必须输入的字段
norequired(true) 不是必填的字段
email(true) 必须输入正确格式的电子邮件
url(true) 必须输入正确格式的网址
date(true或正则) 必须输入正确格式的日期。默认是要求YYYY-MM-dd这样的格式
number(true) 必须输入合法的数字(负数,小数)
digits(true) 必须输入整数
pattern(正则或true) 让输入数据匹配给定的正则,如果没有指定,那么会到元素上找pattern属性转换成正则再匹配
equalto(ID名) 输入值必须和 #id 元素的value 相同
maxlength:5 输入长度最多是 5 的字符串(汉字算一个字符)
minlength:10 输入长度最小是 10 的字符串(汉字算一个字符)
chs(true) 要求输入全部是中文
max:5 输入值不能大于 5
min:10 输入值不能小于 10

这些验证规则要求使用ms-rules指令表示,要求为一个普通的JS对象。

此外要求验征框架能动起来,还必须在所有表单元素外包一个form元素,在form元素上加ms-validate指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 var vm = avalon.define({
$id: "validate1",
aaa: "",
bbb: '',
ccc: '',
validate: {
onError: function (reasons) {
reasons.forEach(function (reason) {
console.log(reason.getMessage())
})
},
onValidateAll: function (reasons) {
if (reasons.length) {
console.log('有表单没有通过')
} else {
console.log('全部通过')
}
}
}
})
<div ms-controller="validate1">
<form ms-validate="@validate">
<p><input ms-duplex="@aaa" placeholder="username"
ms-rules='{required:true,chs:true}' >{{@aaa}}</p>
<p><input type="password" id="pw" placeholder="password"
ms-rules='{required:true}'
ms-duplex="@bbb" /></p>
<p><input type="password"
ms-rules="{required:true,equalto:'pw'}" placeholder="再填一次"
ms-duplex="@ccc | change" /></p>
<p><input type="submit" value="submit"/></p>
</form>
</div>

因此,要运行起avalon2的内置验证框架,必须同时使用三个指令。ms-validate用于定义各种回调与全局的配置项(如什么时候进行验证)。ms-duplex用于将单个表单元素及相关信息组成一个Field对象,放到ms-validater指令的fields数组中。ms-rules用于定义验证规则。如果验证规则不满足你,你可以自行在avalon.validators对象上添加。

现在我们可以一下ms-validate的用法。其对应一个对象。

配置项 描述
fields 框架自行添加,用户不用写。为一个数组,放置ms-duplex生成的Field对象。
onSuccess 空函数,单个验证成功时触发,this指向被验证元素this指向被验证元素,传参为一个对象数组外加一个可能存在的事件对象。
onError 空函数,单个验证失败时触发,this与传参情况同上
onComplete 空函数,单个验证无论成功与否都触发,this与传参情况同上。
onValidateAll 空函数,整体验证后或调用了validateAll方法后触发;有了这东西你就不需要在form元素上ms-on-submit=”submitForm”,直接将提交逻辑写在onValidateAll回调上
onReset 空函数,表单元素获取焦点时触发,this指向被验证元素,大家可以在这里清理className、value
validateInBlur true,在blur事件中进行验证,触发onSuccess, onError, onComplete回调
validateInKeyup true, 在keyup事件中进行验证,触发onSuccess, onError, onComplete回调。当用户在ms-duplex中使用change debounce过滤器时会失效
validateAllInSubmit true,在submit事件中执行onValidateAll回调
resetInFocus true,在focus事件中执行onReset回调
deduplicateInValidateAll false,在validateAll回调中对reason数组根据元素节点进行去重

我们看一下如何自定义验证规则.

比如说我们有一个变态的需求,一个字段可以不填,但如果要填的话一定要是合法的数字,并且大于零. 这就需要自定义规则了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html>
<head>
<title>ms-validate</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="../dist/avalon.js"></script>
<script>
avalon.validators.aaa = {
message: '必须数字并大于0',
get: function (value, field, next) {
//想知道它们三个参数是什么,可以console.log(value, field,next)
var ok = (value === '' || (Number(value) > 0))
next(ok)
return value
}
}
var vm = avalon.define({
$id: "test",
aaa: '',
validate: {
onError: function (reasons) {
reasons.forEach(function (reason) {
console.log(reason.getMessage())
})
},
onValidateAll: function (reasons) {
if (reasons.length) {
console.log('有表单没有通过')
} else {
console.log('全部通过')
}
}
}
})
</script>
</head>

<body ms-controller="test">
<form class="cmxform" ms-validate="@validate" >
<fieldset>
<legend>自定义规则</legend>
<p>
<input
ms-duplex="@aaa"
ms-rules="{aaa: true}"
>
</p>
</fieldset>
<p>
<input class="submit" type="submit" value="提交">
</p>
</fieldset>
</form>
</body>
</html>

在上表还有一个没有提到的东西是如何显示错误信息,这个avalon不帮你处理。但提示信息会帮你拼好,如果你没有写,直接用验证规则的message,否则在元素上找data-message或data-required-message这样的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 <!DOCTYPE html>
<html>
<head>
<title>ms-validate</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="../dist/avalon.js"></script>
<script>
var vm = avalon.define({
$id: "test",
rules:{required:true,email:true},
email:'',
validate: {
onError: function (reasons) {
reasons.forEach(function (reason) {
console.log(reason.getMessage())
})
},
onValidateAll: function (reasons) {
if (reasons.length) {
console.log('有表单没有通过')
} else {
console.log('全部通过')
}
}
}
})
</script>
</head>

<body ms-controller="test">
<form class="cmxform" ms-validate="@validate" >
<fieldset>
<legend>验证完整的表单</legend>
<p>
<label for="email">Email</label>
<input id="email"
name="email"
type="email"
ms-duplex="@email"
ms-rules="@rules"
data-required-message="请输入"
data-email-message="请输入一个正确的邮箱"
>
</p>
</fieldset>
<p>
<input class="submit" type="submit" value="提交">
</p>
</fieldset>
</form>
</body>
</html>

最后给一个复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<script>
var vm = avalon.define({
$id: "validate2",
firstname: '司徒正美',
lastname: '',
username: '',
password: '',
confirm_password: '',
email: '',
agree: false,
topic: [],
toggle: false,
validate: {
onError: function (reasons) {
reasons.forEach(function (reason) {
console.log(reason.getMessage())
})
},
onValidateAll: function (reasons) {
if (reasons.length) {
console.log('有表单没有通过')
} else {
console.log('全部通过')
}
}
}
})
avalon.validators.checked = {
message: '必须扣上',
get: function (value, field, next) {
next(value)
return value
}
}
avalon.validators.selecttwo = {
message: '至少选择两个',
get: function (value, field, next) {
next(!vm.toggle || value.length >= 2)
return value
}
}
</script>

<div ms-controller="validate2">
<form class="cmxform" ms-validate="@validate" >
<fieldset>
<legend>验证完整的表单</legend>
<p>
<label for="firstname">名字</label>
<input id="firstname"
name="firstname"
ms-duplex="@firstname"
ms-rules="{required:true, pattern: /[\u4e00-\u9fa5a-z]{2-8}/i }"
data-required-message="必须是中文或字母(3-8个字符)" >
</p>
<p>
<label for="lastname">姓氏</label>
<input id="lastname"
name="lastname"
ms-duplex="@lastname"
ms-rules="{required:true}"
data-required-message="请输入您的姓氏"
>
</p>
<p>
<label for="username">用户名</label>
<input id="username"
name="username"
ms-duplex="@username | change"
ms-rules="{required:true, minlength:2}"
>
</p>
<p>
<label for="password">密码</label>
<input id="password"
name="password"
type="password"
ms-duplex="@password"
ms-rules="{required:true,minlength:5}"
data-required-message="请输入密码"
data-required-message="密码长度不能小于 5 个字母"

>
</p>
<p>
<label for="confirm_password">验证密码</label>
<input id="confirm_password"
name="confirm_password"
type="password"
ms-duplex="@confirm_password | change"
ms-rules="{required:true,equalto:'password'}"
data-equalto-message="两次密码输入不一致"
>
</p>
<p>
<label for="email">Email</label>
<input id="email"
name="email"
type="email"
ms-duplex="@email"
ms-rules="{email:true}"
data-email-message="请输入一个正确的邮箱"
>
</p>
<p>
<label for="agree">请同意我们的声明</label>
<input type="checkbox" class="checkbox" id="agree" name="agree"
ms-duplex-checked="@agree"
ms-rules="{checked:true}"
>
</p>
<p>
<label for="newsletter">我乐意接收新信息</label>
<input type="checkbox" class="checkbox"
id="newsletter"
name="newsletter"
ms-duplex-checked="@toggle"
>
</p>
<fieldset id="newsletter_topics" ms-visible="@toggle" >
<legend>主题 (至少选择两个) </legend>
<label for="topic_marketflash">
<input type="checkbox"
id="topic_marketflash"
value="marketflash"
name="topic[]"
ms-duplex="@topic"
ms-rules="{selecttwo:true}"
>Marketflash
</label>
<label for="topic_fuzz">
<input type="checkbox"
id="topic_fuzz"
value="fuzz"
name="topic[]"
ms-duplex="@topic"
ms-rules="{selecttwo:true}"
>Latest fuzz
</label>
<label for="topic_digester">
<input type="checkbox"
id="topic_digester"
value="digester"
name="topic[]"
ms-duplex="@topic"
ms-rules="{selecttwo:true}"
>Mailing list digester
</label>
<label for="topic" class="error" style="display:none">至少选择两个</label>
</fieldset>
<p>
<input class="submit" type="submit" value="提交">
</p>
</fieldset>
</form>
</div>

过滤器

格式化过滤器

用于处理数字或字符串,多用于 {{ }} 或ms-attr 或ms-class

注意: avalon的过滤器与ng的过滤器在传参上有点不一样,需要用()括起来

uppercase

将字符串全部大写

1
2
3
vm.aaa = "aaa"

<div>{{@aaa | uppercase}}</div>

lowercase

将字符串全部小写

1
2
3
vm.aaa = "AAA"

<div>{{@aaa | lowercase}}</div>

truncate

对长字符串进行截短,有两个可选参数

number,最后返回的字符串的长度,已经将truncation的长度包含在内,默认为30。 truncation,告知用户它已经被截短的一个结尾标识,默认为”…”

1
2
3
vm.aaa = "121323234324324"

<div>{{@aaa | truncate(10,'...')}}</div>

camelize

驼峰化处理, 如”aaa-bbb”变成”aaaBBB”

escape

对类似于HTML格式的字符串进行转义,如将<、 >转换为<、 >

sanitize

对用户输入的字符串进行反XSS处理,去掉onclick, javascript:alert<script>等危险属性与标签。

number

对需要处理的数字的整数部分插入千分号(每三个数字插入一个逗号),有一个参数fractionSize,用于保留小数点的后几位。

fractionSize:小数部分的精度,默认为3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script src="avalon.js"></script>
<script>
avalon.define({
$id: "number",
aaa: 1234.56789
})
</script>
</head>

<body>
<div ms-controller="number">
<p>输入数字:
<input ms-duplex="@aaa">
</p>
<p>不处理:{{@aaa}}</p>
<p>不传参:{{@aaa|number}}</p>
<p>不保留小数:{{@aaa|number(0)}}</p>
<p>负数:{{-@aaa|number(4)}}</p>
</div>
</body>

</html>

currency

用于格式化货币,类似于number过滤器(即插入千分号),但前面加了一个货币符号,默认使用人民币符号\uFFE5

symbol, 货币符号,默认是\uFFE5 fractionSize,小数点后保留多少数,默认是2

date

对日期进行格式化,date(formats), 目标可能是符合一定格式的字符串,数值,或Date对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script src="avalon.js"></script>
<script>
avalon.define({
$id: 'testtest',
name: "大跃进右",
d1: new Date,
d2: "2011/07/08",
d3: "2011-07-08",
d4: "01-01-2000",
d5: "03 04,2000",
d6: "3 4,2000",
d7: 1373021259229,
d8: "1373021259229",
d9: "2014-12-07T22:50:58+08:00",
d10: "\/Date(1373021259229)\/"

})
</script>
</head>
<body>
<div ms-controller="testtest">
<p>生成于{{ @d1 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d2 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d3 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d4 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d5 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d6 | date("yyyy MM dd")}}</p>
<p>生成于{{ @d7 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d8 | date("yyyy MM dd:HH:mm:ss")}}</p>
<p>生成于{{ @d9 | date("yyyy MM dd:HH:mm:ss")}} //这是ISO8601的日期格式</p>
<p>生成于{{ @d10| date("yyyy MM dd:HH:mm:ss")}} //这是ASP.NET输出的JSON数据的日期格式</p>
</div>
</body>

</html>
标记 说明
yyyy 将当前的年份以4位数输出,如果那一年为300,则补足为0300
yy 将当前的年份截取最后两位数输出,如2014变成14, 1999变成99, 2001变成01
y 将当前的年份原样输出,如2014变成2014, 399变成399, 1变成1
MMMM 在中文中,MMMM与MMM是没有区别,都是”1月”,”2月”……英语则为该月份的单词全拼
MMM 在中文中,MMMM与MMM是没有区别,都是”1月”,”2月”……英语则为该月份的单词缩写(前三个字母)
MM 将月份以01-12的形式输出(即不到两位数,前面补0)
M 将月份以1-12的形式输出
dd 以日期以01-31的形式输出(即不到两位数,前面补0)
d 以日期以1-31的形式输出
EEEE 将当前天的星期几以“星期一”,“星期二”,“星期日”的形式输出,英语则Sunday-Saturday
EEE 将当前天的星期几以“周一”,“周二”,“周日”的形式输出,英语则Sun-Sat
HH 将当前小时数以00-23的形式输出
H 将当前小时数以0-23的形式输出
hh 将当前小时数以01-12的形式输出
h 将当前小时数以0-12的形式输出
mm 将当前分钟数以00-59的形式输出
m 将当前分钟数以0-59的形式输出
ss 将当前秒数以00-59的形式输出
s 将当前秒数以0-59的形式输出
a 将当前时间是以“上午”,“下午”的形式输出
Z 将当前时间的时区以-1200-+1200的形式输出
fullDate 相当于y年M月d日EEEE 2014年12月31日星期三
longDate 相当于y年M月d日EEEE 2014年12月31日
medium 相当于yyyy-M-d H:mm:ss 2014-12-31 19:02:44
mediumDate 相当于yyyy-M-d 2014-12-31
mediumTime 相当于H:mm:ss 19:02:44
short 相当于yy-M-d ah:mm 14-12-31 下午7:02
shortDate 相当于yy-M-d 14-12-31
shortTime 相当于ah:mm 下午7:02

循环过滤器

用于ms-for指令中

limitBy

只能用于ms-for循环,对数组与对象都有效, 限制输出到页面的个数, 有两个参数

  1. limit: 最大个数,必须是数字或字符, 当个数超出数组长或键值对总数时, 等于后面
  2. begin: 开始循环的个数, 可选,默认0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
avalon.define({
$id: "limitBy",
array: [1, 2, 3, 4, 5, 6],
object: {a: 1, b: 2, c: 3, d: 4, e: 5},
num: 3
})
</script>
<div ms-controller='limitBy'>
<select ms-duplex-number='@num'>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
<ul>
<li ms-for='el in @array | limitBy(@num)'>{{el}}</li>
</ul>
<ul>
<li ms-for='el in @object | limitBy(@num)'>{{el}}</li>
</ul>
</div>

orderBy

只能用于ms-for循环,对数组与对象都有效, 用于排序, 有两个参数

  1. key: 要排序的属性名
  2. dir: -1或1, 顺序或倒序,可选,默认1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
avalon.define({
$id: "orderBy",
array: [{a: 1, b: 33},{a: 2, b: 22}, {a: 3, b: 11}],
order: 'a',
dir: -1
})
</script>
<div ms-controller='orderBy'>
<select ms-duplex='@order'>
<option>a</option>
<option>b</option>
</select>
<select ms-duplex-number='@dir'>
<option>1</option>
<option>-1</option>
</select>
<table border='1' width='200'>
<tr ms-for="el in @array | orderBy(@order, @dir)">
<td ms-for='elem in el'>{{elem}}</td>
</tr>
</table>
</div>

filterBy

只能用于ms-for循环,对数组与对象都有效, 用于获取它们的某一子集, 有至少一个参数

search,如果为函数时, 通过返回true决定成为子集的一部分; 如果是字符串或数字, 将转换成正则, 如果数组元素或对象键值匹配它,则成为子集的一部分,但如果是空字符串则返回原对象 ;其他情况也返回原对象。 其他参数, 只有当search为函数时有效, 这时其参数依次是数组元素或对象键值(对象的情况下), 索引值或对象键名(对象的情况下), 多余的参数 此过滤多用于自动完成的模糊匹配!

img

filterBy例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
avalon.define({
$id: "filterBy",
array: ['aaaa', 'aab', 'acb', 'ccc', 'dddd'],
object: {a: 'aaaa', b: 'aab', c: 'acb', d: 'ccc', e: 'dddd'},
searchs: "a",
searchFn: function (el, i) {
return i > 2
},
searchFn2: function (el, i) {
return el.length === 4
},
searchFn3: function (el, i) {
return i === 'b' || i === 1
}
})
</script>
<div ms-controller='filterBy'>
<select ms-duplex='@search'>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p><button ms-click="@search = @searchFn | prevent">变成过滤函数</button></p>
<p><button ms-click="@search = @searchFn2 | prevent">变成过滤函数2</button></p>
<p><button ms-click="@search = @searchFn3 | prevent">变成过滤函数3</button></p>
<ul>
<li ms-for='el in @array | filterBy(@search)'>{{el}}</li>
</ul>
<ul>
<li ms-for='el in @object | filterBy(@search)'>{{el}}</li>
</ul>
</div>

filterBy例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<script>
var vm = avalon.define({
$id: 'test',
arr: [{color: 'red'},{color:'green'},{color:'red'}],
fn: function(el, index, xxx){
console.log(el, index, xxx)
return el.color === 'red'
}
})
</script>
<style>

</style>
<div ms-controller="test">
<ul>
<li ms-for="el in @arr | filterBy(@fn, 'xxx')">{{el.color}}</li>

</ul>
</div>
</body>

filterBy例子3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
var vm = avalon.define({
$id: 'test',
arr: [{name: 'wanglin', age: 11},
{name: 'lin', age: 3},
{name: 'Hunt', age: 22},
{name: 'Joe', age: 33}],
fn: function(a){//过滤数组中 name属性值带lin中的元素
return /lin/.test(a.name)
}
})
</script>
<div ms-controller='test' >
<div ms-for="(index,el) in @arr as items | filterBy(@fn)" >
{{el.name}} -- {{items.length}}
</div>
</div>

selectBy

只能用于ms-for循环,只对对象有效, 用于抽取目标对象的几个值,构成新数组返回.

  1. array,要抽取的属性名
  2. defaults,如果目标对象不存在这个属性,那么从这个默认对象中得到默认值,否则为空字符串, 可选 这个多用于表格, 每一列的对象可能存在属性顺序不一致或缺少的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
avalon.define({
$id: "selectBy",
obj: {a: 'aaa', b: 'bbb', c: 'ccc', d: 'ddd', e: 'eee'},
grid: [{a: 1, b: 2, c: 3}, {c: 11, b: 22, a: 33}, {b: 23, a: 44}],
defaults: {
a:'@@@',
b:'$$$',
c:'###'
}
})
</script>
<div ms-controller='selectBy'>
<ul>
<li ms-for='el in @obj | selectBy(["c","a","b"])'>{{el}}</li>
</ul>
<table border='1' width='200'>
<tr ms-for="tr in @grid">
<td ms-for="td in tr | selectBy(['a','b','c'],@defaults)">{{td}}</td>
</tr>
</table>
</div>

事件过滤器

事件过滤器只要是对一些常用操作进行简化处理

对按键事件(keyup,keydown,keypress)到底按下了哪些功能键 或方向键进行友好的处理.许多人都记不清回车退格的keyCode是多少. 对阻止默认行为与防止冒泡进行封装

esc

当用户按下esc键时,执行你的回调

tab

当用户按下tab键时,执行你的回调

enter

当用户按下enter键时,执行你的回调

space

当用户按下space键时,执行你的回调

del

当用户按下del键时,执行你的回调

up

当用户按下up键时,执行你的回调

down

当用户按下down键时,执行你的回调

left

当用户按下left键时,执行你的回调

当用户按下right键时,执行你的回调

prevent

阻止默为行为,多用于form的submit事件防止页面跳转,相当于调用了event.preventDefault

1
<a href='./api.html' ms-click='@fn | prevent'>阻止跳转</a>

stop

阻止事件冒泡,相当于调用了event.stopPropagation

页面的过滤器只能用于事件绑定

同步频率过滤器

这两个过滤器只用于ms-duplex

change

在文本域或文本区使用ms-duplex时,默认是每输入一个字符就同步一次. 当我们想在失去焦点时才进行同步, 那么可以使用此过滤器

1
<input ms-duplex='@aaa | change'>{{@aaa}}

debounce

当我们实现搜索框的自动完成时, 每输入一个字符可能就会向后台请求一次(请求关键字列表), 这样太频繁,后端撑不住,但使用change过滤器,则又太慢了.改为每隔几十毫秒请求一次就最好. 基于此常用需要开发出此过滤器. 拥有一个参数.

  1. debounceTime: 数字, 不写默认是300,不能少于4,否则做无效处理

    1
    <input ms-duplex='@aaa | debounce(200)'>

编写过滤器

编写一个过滤器是非常简单的. 目前用户可编写的过滤器有两种, 不带参数的及带参数.

比方说uppercase,就是不带参数

1
2
3
vm.aaa = "aaa"

<div>{{@aaa | uppercase}}</div>

输出:

1
<div>AAA</div>

那它是怎么实现的呢? 源码是这样的

1
2
3
avalon.filters.uppercase = function (str) {
return String(str).toUpperCase()
}

过滤器总是把它前方的表达式生成的东西作为过滤器的第一个参数,然后返回一个值

同理lowercase的源码也很简单. 之所以用String,因为我们总想返回一个字符串

1
2
3
avalon.filters.lowercase = function (str) {
return String(str).toLowerCase()
}

那么我们自定义一个过滤器,就首先要看一下文档,注意不要与现有的过滤器同名. 比如我们定义一个haha的过滤器

1
2
3
4
5
6
7
8
9
10
<script>
avalon.filters.haha = function(a){
return a +'haha'
}
avalon.define({
$id:'test',
aaa: '111'
})
</script>
<div ms-controller='tesst'>{{@aaa | haha}}</div>// 111haha

我们再看带参数的,带参数的必须写的括号,把第二个,第三个,放到里面

1
<div>{{@aaa | truncate(10,'...')}}</div>

truncate要传两个参数,那么看一下其源码是这样的:

1
2
3
4
5
6
7
8
avalon.filters.truncate = function (str, length, truncation) {
//length,新字符串长度,truncation,新字符串的结尾的字段,返回新字符串
length = length || 30
truncation = typeof truncation === "string" ? truncation : "..."
return str.length > length ?
str.slice(0, length - truncation.length) + truncation :
String(str)
}

好了,我们看一下如何写一个带参数的过滤器,里面重复利用已有的过滤器

众所周知,ms-attr是返回一个对象. 我们只想对其中的一个字段进行格式化. 比如我们要处理title. 那么就起名为title.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="../dist/avalon.js"></script>
<script>
avalon.filters.title = function (obj, a, b) {
var title = obj.title
var newTitle = avalon.filters.truncate(title, a, b)
obj.title = newTitle
return obj
}
var vm = avalon.define({
$id: 'test',
el: '123456789qwert'
})

</script>
</head>
<body ms-controller="test">
<div ms-attr="{title:@el} | title(10,'...')">333</div>
</body>
</html>

$watch,$fire, $unwatch

模块间通信及属性监控 $watch,$fire, $unwatch

avalon内置了一个强大的自定义事件系统,它在绑定在每一个VM上。每一个VM都拥有$watch, $unwatch, $fire这三个方法,及一个$events对象。$events是用于储存各种回调。先从单个VM说起,如果一个VM拥有aaa这个属性,如果我们在VM通过$watch对它监控,那么当aaa改变值时,它对应的回调就会被触发!

1
2
3
4
5
6
7
8
9
10
11
var vmodel = avalon.define({
$id: "test",
aaa: 111
})
vmodel.$watch("aaa", function(newValue, oldValue){
avalon.log(newValue) //222
avalon.log(oldValue) //111
})
setTimeout(function(){
vmodel.aaa = 222
}, 1000)

注意,它只能监听当前属性的变动。

我们还可以通过$unwatch方法,移除对应的回调。如果传入两个参数,第一个是属性名,第二个是回调,那么只移除此回调;如果只传入一个属性名,那么此属性关联的所有回调都会被移除掉。

有时,我们还绑定了一些与属性名无关的事件回调,想触发它,那只能使用$fire方法了。$fire方法第一个参数为属性名(自定义事件名),其他参数随意。

1
2
3
4
5
6
7
8
9
10
var vmodel = avalon.define({
$id: "test",
aaa: 111
})
vmodel.$watch("cat", function(){
avalon.log(avalon.slice(arguments)) //[1,2,3]
})
setTimeout(function(){
vmodel.$fire("cat",1,2,3)
}, 1000)

更高级的玩法,有时我们想在任何属性变化时都触发某一个回调,这时我们就需要$watch一个特殊的属性了——“$all”。不同的是,$watch回调的参数多了一个属性名,排在最前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vmodel = avalon.define({
$id: "test",
aaa: 111,
bbb: 222,

})
vmodel.$watch("$all", function(){
avalon.log(avalon.slice(arguments))
// ["aaa", 2, 111]
// ["bbb", 3, 222]
})
setTimeout(function(){
vmodel.aaa = 2
vmodel.bbb = 3
}, 1000)

手动触发$fire是位随着高风险的,框架内部是做了处理(只有前后值发生变化才会触发),因此万不得已使用它,但又爆发死循环怎么办?这样就需要暂时中断VM的属性监控机制。使用$unwatch(),它里面什么也不传,就暂时将监控冻结了。恢复它也很简单,使用$watch(),里面也什么也不传!

不过最强大的用法是实现模块间的通信(因为在实际项目中,一个页面可能非常大,有多少人分块制作,每个人自己写自己的VM,这时就需要通过某种机制来进行数据与方法的联动了),这是使用$fire方法达成的。只要在$fire的自定义事件名前添加”up!”, “down!”, “all!”前缀,它就能实现angular相似的$emit,$broadcast功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html>
<head>
<title>by 司徒正美</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="avalon.js"></script>
<script>
var vm1 = avalon.define({
$id: "ancestor",
aaa : '1111111111',
click : function() {
avalon.log("向下广播")
vm1.$fire("down!aaa", "capture")
}
})
vm1.$watch("aaa", function(v) {
avalon.log(v)
avalon.log("ancestor.aaa事件被触发了")
})
var vm2 = avalon.define({
$id: "parent",
text : "222222222"
aaa : '3333333333',
click : function() {
console.log("全局扩播")
vm2.$fire("all!aaa", "broadcast")
}
})
vm2.$watch("aaa", function(v) {
avalon.log(v)
avalon.log("parent.aaa事件被触发了")
})
var vm3 = avalon.define(
$id: "son",
click : function() {
console.log("向上冒泡")
vm3.$fire("up!aaa", "bubble")
}
})
vm3.$watch("aaa", function(v) {
avalon.log(v)
avalon.log("son.aaa事件被触发了")
})
</script>
<style>

</style>
</head>
<body class="ms-controller" ms-controller="ancestor">
<h3>avalon vm.$fire的升级版 </h3>
<button type="button" ms-click="click">
capture
</button>
<div ms-controller="parent">
<button type="button" ms-click="click">broadcast</button>
<div ms-controller="son">
<button type="button" ms-click="click">
bubble
</button>
</div>
</div>
</body>
</html>

与jQuery共存

jQuery是世界上最流行的DOM库,它拥有各式各样的插件,在日常开发中我们可能还是离不开它.因此与它一起使用是常态, 下面是一些注意

domReady后如何扫描

1
2
3
4
5
$(function(){
var vm = avalon.define({/* */})
//如果你将vm定义在jQuery的ready方法内部,那么avalon的扫描就会失效,需要手动扫描
avalon.scan(document.body) //现在只要传入扫描范围的根节点就行
})

如何AJAX提交数据

提交整个VM

1
2
3
4
5
6
7
jQuery.ajax({
method: "POST",
url: 'url-adress',
//这里是取vm的数据模型 ,通过JSON.stringify会去掉其所有方法, 变成JSON字符串
//再用JSON.parse变回纯JS对象
data: JSON.parse(JSON.stringify(vm.$model))
})

提交VM中的某个对象属性

1
2
3
4
5
6
7
data: JSON.parse(JSON.stringify(vm.data.$model))
``

提交VM中的某个`数组`属性

```javascript
data: JSON.parse(JSON.stringify(vm.data.$model))

如何让后台回来的数据更新VM

后台的数据更新VM,只能是更新VM的某些已经定义属性. 如果后台数据很大,那么我们可以定义一个空对象(假如后台数据是对象类型)或一个空数组(假如后台数据是数组类型)来占位

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = avalon.define({
$id: 'aaa',
array: []
})

jQuery.ajax({
method: "POST",
url: 'url-adress',
data: {/**/},
success: function(data){
vm.array = data.array
}
})

如何同步表单的数据

假如我的某个表单是用于jQuery的日历插件,那么它数据如何同步到vm

1
2
3
$(datepick_input_css_selector).input(function(){
vm.aaa = this.value
})

如何同步复选框 在avalon中,checkbox要对应一个数组 首选是取得所有同名的checkbox,并要求它们在选中状态,然后用map方法收集它们的value值

1
2
3
4
5
6
$('checkbox').change(function(){
var array = $('checkbox[name="'+this.name+'"]:checked').map(function(){
return $(this).val();
})
vm.checkboxProps = array
})

如何同步下拉框

1
2
3
4
$('select').change(function(){

vm.selectProps = $(this).val()
})

FreeMarker

相关概念

FreeMarker

FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。

而FreeMarker最初的设计,是被用来在MVC模式的Web开发框架中生成HTML页面的,它没有被绑定到 Servlet或HTML或任意Web相关的东西上。它也可以用于非Web应用环境中。

模板

假设在一个在线商店的应用系统中需要一个HTML页面,和下面这个页面类似:

output

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome Big Doe!</h1>
<p>Our latest product:
<a href="products/greenmouse.html">green mouse</a>!
</body>
</html>

这里的用户名(上面的”Big Joe”),应该是登录这个网页的访问者的名字, 并且最新产品的数据应该来自于数据库,这样它才能随时更新。那么不能直接在HTML页面中输入它们, 不能使用静态的HTML代码。此时,可以使用要求输出的 模板。 模板和静态HTML是相同的,只是它会包含一些 FreeMarker 将它们变成动态内容的指令:

template

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome ${user}!</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>

模板文件存放在Web服务器上,就像通常存放静态HTML页面那样。当有人来访问这个页面, FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中 ${...} 的部分, 之后将结果发送到访问者的Web浏览器中。访问者的Web浏览器就会接收到例如第一个HTML示例那样的内容 (也就是没有FreeMarker指令的HTML代码),访问者也不会察觉到服务器端使用的FreeMarker。 (当然,存储在Web服务器端的模板文件是不会被修改的;替换也仅仅出现在Web服务器的响应中。)

请注意,模板并没有包含程序逻辑来查找当前的访问者是谁,或者去查询数据库获取最新的产品。 显示的数据是在 FreeMarker 之外准备的,通常是一些 “真正的” 编程语言(比如Java) 所编写的代码。模板作者无需知道这些值是如何计算出的。事实上,这些值的计算方式可以完全被修改, 而模板可以保持不变,而且页面的样式也可以完全被修改而无需改动模板。 当模板作者(设计师)和程序员不是同一人时,显示逻辑和业务逻辑相分离的做法是非常有用的, 即便模板作者和程序员是一个人,这么来做也会帮助管理应用程序的复杂性。 保证模板专注于显示问题(视觉设计,布局和格式化)是高效使用模板引擎的关键。

模型

为模板准备的数据整体被称作为 数据模型。 模板作者要关心的是,数据模型是树形结构(就像硬盘上的文件夹和文件),在视觉效果上, 数据模型可以是:

1
2
Note:
上面只是一个形象化显示;数据模型不是文本格式,它来自于Java对象。 对于Java程序员来说,root就像一个有 getUser() 和 getLatestProduct() 方法的Java对象, 也可以有 "user" 和 "latestProducts" 键值的Java Map对象。相似地,latestProduct 就像是有 getUrl() 和 getName() 方法的Java对象。

正如已经看到的,数据模型的基本结构是树状的。 这棵树可以很复杂,并且可以有很大的深度,比如:

上图中的变量扮演目录的角色(比如 root, animals, mouse, elephant, python, misc) 被称为 hashes (哈希表或哈希,译者注)。哈希表存储其他变量(被称为 子变量), 它们可以通过名称来查找(比如 “animals”, “mouse” 或 “price”)。

存储单值的变量 (size, price, messagefoo) 称为 scalars (标量,译者注)。

如果要在模板中使用子变量, 那应该从根root开始指定它的路径,每级之间用点来分隔开。要访问 mouseprice ,要从root开始,首先进入到 animals ,之后访问 mouse ,最后访问 price 。就可以这样来写 animals.mouse.price

另外一种很重要的变量是 sequences (序列,译者注)。 它们像哈希表那样存储子变量,但是子变量没有名字,它们只是列表中的项。 比如,在下面这个数据模型中, animalsmisc.fruits 就是序列:

Data Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(root)
|
+- animals
| |
| +- (1st)
| | |
| | +- name = "mouse"
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- (2nd)
| | |
| | +- name = "elephant"
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- (3rd)
| |
| +- name = "python"
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- misc
|
+- fruits
|
+- (1st) = "orange"
|
+- (2nd) = "banana"

要访问序列的子变量,可以使用方括号形式的数字索引下标。 索引下标从0开始(从0开始也是程序员的传统),那么第一项的索引就是0, 第二项的索引就是1等等。要得到第一个动物的名称的话,可以这么来写代码 animals[0].name。要得到 misc.fruits 中的第二项(字符串"banana")可以这么来写 misc.fruits[1]。(实践中,通常按顺序遍历序列,而不用关心索引, 这点会在 后续介绍。)

标量类型可以分为如下的类别:

  • 字符串:就是文本,也就是任意的字符序列,比如上面提到的 ‘’m’’, ‘’o’’, ‘’u’’, ‘’s’’, ‘’e’’。比如 namesize 也是字符串。
  • 数字:这是数值类型,就像上面的 price。 在FreeMarker中,字符串 "50" 和数字 50 是两种完全不同的东西。前者是两个字符的序列 (这恰好是人们可以读的一个数字),而后者则是可以在数学运算中直接被使用的数值。
  • 日期/时间: 可以是日期-时间格式(存储某一天的日期和时间), 或者是日期(只有日期,没有时间),或者是时间(只有时间,没有日期)。
  • 布尔值:对应着对/错(是/否,开/关等值)类似的值。 比如动物可以有一个 protected (受保护的,译者注) 的子变量, 该变量存储这个动物是否被保护起来的值。

总结:

  • 数据模型可以被看成是树形结构。
  • 标量用于存储单一的值。这种类型的值可以是字符串,数字,日期/时间或者是布尔值。
  • 哈希表是一种存储变量及其相关且有唯一标识名称的容器。
  • 序列是存储有序变量的容器。存储的变量可以通过数字索引来检索,索引通常从0开始。

模板一览

interpolation 插值

${...}: FreeMarker将会输出真实的值来替换大括号内的表达式,这样的表达式被称为 interpolation插值

FTL标签

  • FTL 标签 (FreeMarker模板的语言标签): FTL标签和HTML标签有一些相似之处,但是它们是FreeMarker的指令,是不会在输出中打印的。 这些标签的名字以 # 开头。(用户自定义的FTL标签则需要使用 @ 来代替 #,但这属于更高级的话题了。
  • FTL标签也被称为 指令。 这些指令在HTML的标签 (比如: <table></table>) 和HTML元素 (比如: table 元素) 中的关系是相同的。(如果现在还没有感觉到它们的不同, 那么把“FTL标签”和“指令”看做是同义词即可。)

注释

  • 注释: 注释和HTML的注释也很相似, 但是它们使用 <#-- and --> 来标识。 不像HTML注释那样,FTL注释不会出现在输出中(不出现在访问者的页面中), 因为 FreeMarker会跳过它们。

if 指令

使用 if 指令可以有条件地跳过模板的一些片段, 比如,假设在 最初的示例中, 想向你的老板Big Joe特别地问好,可其他人不同:

Template

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>
Welcome ${user}<#if user == "Big Joe">, our beloved leader</#if>!
</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>

此时,告诉 FreeMarker,当和 "Big Joe" 相同时 “, our beloved leader” (我们最尊敬的领导,译者注) 才是if条件中那唯一的 user 变量的值。 通常来讲,如果 *condition* 是false(布尔值),那么介于 <#if *condition*></#if> 标签中的内容会被略过。

我们来详细说说 *condition* 的使用: == 是用来判断它两侧的值是否相等的操作符, 比较的结果是布尔值,也就是true或者false。在 == 的左侧,是被引用的变量, 我们很熟悉这样的语法结构;最终它会被变量的值所替代。通常来说, 在指令或插值中没有被引号标注的内容都被视为变量的引用。右侧则是指定的字符串, 在模板中的字符串 只能 放在引号内。

if-else if-else示例

1
2
3
4
5
6
7
<#if animals.python.price < animals.elephant.price>
Pythons are cheaper than elephants today.
<#elseif animals.elephant.price < animals.python.price>
Elephants are cheaper than pythons today.
<#else>
Elephants and pythons cost the same today.
</#if>

list 指令

当需要列表显示内容时,list指令是必须的。

TEMPLATE

1
2
3
4
5
6
<p>We have these animals:
<table border=1>
<#list animals as animal>
<tr><td>${animal.name}<td>${animal.price} Euros
</#list>
</table>

那么输出结果将会是这样的:

OUTPUT

1
2
3
4
5
6
<p>We have these animals:
<table border=1>
<tr><td>mouse<td>50 Euros
<tr><td>elephant<td>5000 Euros
<tr><td>python<td>4999 Euros
</table>

list 指令的一般格式为: <#list sequence as loopVariable*repeatThis</#list>repeatThis 部分将会在给定的 sequence 遍历时在每一项中重复, 从第一项开始,一个接着一个。在所有的重复中, loopVariable 将持有当前遍历项的值。 这个变量仅存在于 <#list ...></#list> 标签内。

include 指令

使用 include 指令, 我们可以在模板中插入其他文件的内容。

假设要在一些页面中显示版权声明的信息。那么可以创建一个文件来单独包含这些版权声明, 之后在需要它的地方插入即可。比方说,我们可以将版权信息单独存放在页面文件 copyright_footer.html 中:

TEMPLATE

1
2
3
4
5
6
<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>

当需要用到这个文件时,可以使用 include 指令来插入:

TEMPLATE

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<#include "/copyright_footer.html">
</body>
</html>

此时,输出的内容为:

OUTPUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>
</body>
</html>

使用内建函数

内建函数很像子变量(如果了解Java术语的话,也可以说像方法), 它们并不是数据模型中的东西,是 FreeMarker 在数值上添加的。 为了清晰子变量是哪部分,使用 ?(问号)代替 .(点)来访问它们。常用内建函数的示例:

  • user?html 给出 user 的HTML转义版本, 比如 & 会由 & 来代替。
  • user?upper_case 给出 user 值的大写版本 (比如 “JOHN DOE” 来替代 “John Doe”)
  • animal.name?cap_first 给出 animal.name 的首字母大写版本(比如 “Mouse” 来替代 “mouse”)
  • user?length 给出 user 值中 字符的数量(对于 “John Doe” 来说就是8)
  • animals?size 给出 animals 序列中 项目 的个数(我们示例数据模型中是3个)
  • 如果在 <#list animals as animal> 和对应的 </#list> 标签中:
    • animal?index 给出了在 animals 中基于0开始的 animal的索引值
    • animal?counter 也像 index, 但是给出的是基于1的索引值
    • animal?item_parity 基于当前计数的奇偶性,给出字符串 “odd” 或 “even”。在给不同行着色时非常有用,比如在 <td class="${animal?item_parity}Row">中。

一些内建函数需要参数来指定行为,比如:

  • animal.protected?string("Y", "N") 基于 animal.protected 的布尔值来返回字符串 “Y” 或 “N”。
  • animal?item_cycle('lightRow','darkRow') 是之前介绍的 item_parity 更为常用的变体形式。
  • fruits?join(", ") 通过连接所有项,将列表转换为字符串, 在每个项之间插入参数分隔符(比如 “orange,banana”)
  • user?starts_with("J") 根据 user 的首字母是否是 “J” 返回布尔值true或false。

内建函数应用可以链式操作,比如user?upper_case?html 会先转换用户名到大写形式,之后再进行HTML转义。(这就像可以链式使用 .(点)一样)

可以阅读 全部内建函数参考

处理不存在的变量

数据模型中经常会有可选的变量(也就是说有时并不存在)。 除了一些典型的人为原因导致失误外,FreeMarker 绝不能容忍引用不存在的变量, 除非明确地告诉它当变量不存在时如何处理。这里来介绍两种典型的处理方法。

这部分对程序员而言: 一个不存在的变量和一个是 null 值的变量, 对于FreeMarker来说是一样的,所以这里所指的”丢失”包含这两种情况。

不论在哪里引用变量,都可以指定一个默认值来避免变量丢失这种情况, 通过在变量名后面跟着一个 !(叹号,译者注)和默认值。 就像下面的这个例子,当 user 不存在于数据模型时, 模板将会将 user 的值表示为字符串 "visitor"。(当 user 存在时, 模板就会表现出 ${user} 的值):

1
<h1>Welcome ${user!"visitor"}!</h1>

也可以在变量名后面通过放置 ?? 来询问一个变量是否存在。将它和 if 指令合并, 那么如果 user 变量不存在的话将会忽略整个问候的代码段:

1
<#if user??><h1>Welcome ${user}!</h1></#if>

关于多级访问的变量,比如 animals.python.price, 书写代码:animals.python.price!0 当且仅当 animals.python 永远存在, 而仅仅最后一个子变量 price 可能不存在时是正确的 (这种情况下我们假设价格是 0)。 如果 animalspython 不存在, 那么模板处理过程将会以”未定义的变量”错误而停止。为了防止这种情况的发生, 可以如下这样来编写代码 (animals.python.price)!0。 这种情况就是说 animalspython 不存在时, 表达式的结果是 0。对于 ?? 也是同样用来的处理这种逻辑的; 将 animals.python.price?? 对比 (animals.python.price)??来看。


快速浏览(备忘单)

这里给已经了解 FreeMarker 的人或有经验的程序员的提个醒:

  • 直接指定值

    • 字符串: "Foo" 或者 'Foo' 或者 "It's \"quoted\"" 或者 'It\'s "quoted"' 或者 r"C:\raw\string"
    • 数字: 123.45
    • 布尔值: truefalse
    • 序列: ["foo", "bar", 123.45]; 值域: 0..9, 0..<10 (或 0..!10), 0..
    • 哈希表: {"name":"green mouse", "price":150}
  • 检索变量

    • 顶层变量: user
    • 从哈希表中检索数据: user.nameuser["name"]
    • 从序列中检索数据: products[5]
    • 特殊变量: .main
  • 字符串操作

    • 插值(或连接): "Hello ${user}!" (或 "Hello " + user + "!")
    • 获取一个字符: name[0]
    • 字符串切分: 包含结尾: name[0..4],不包含结尾: name[0..<5],基于长度(宽容处理): name[0..*5],去除开头: name[5..]
  • 序列操作

    • 连接: users + ["guest"]
    • 序列切分:包含结尾: products[20..29], 不包含结尾: products[20..<30],基于长度(宽容处理): products[20..*10],去除开头: products[20..]
  • 哈希表操作

    • 连接: passwords + { "joe": "secret42" }
  • 算术运算: (x * 1.5 + 10) / 2 - y % 100

  • 比较运算: x == yx != yx < yx > yx >= yx <= yx lt yx lte yx gt yx gte y, 等等。。。。。。

  • 逻辑操作: !registered && (firstVisit || fromEurope)

  • 内建函数: name?upper_case, path?ensure_starts_with('/')

  • 方法调用: repeat("What", 3)

  • 处理不存在的值

    • 默认值: name!"unknown" 或者 (user.name)!"unknown" 或者 name! 或者 (user.name)!
    • 检测不存在的值: name?? 或者 (user.name)??
  • 赋值操作: =, +=, -=, *=, /=, %=, ++, --

请参考: 运算符优先级


转义

转义序列 含义
\" 引号 (u0022)
\' 单引号(又称为撇号) (u0027)
\{ 起始花括号:{
\\ 反斜杠 (u005C)
\n 换行符 (u000A)
\r 回车 (u000D)
\t 水平制表符(又称为tab) (u0009)
\b 退格 (u0008)
\f 换页 (u000C)
\l 小于号:<
\g 大于号:>
\a &符:&
\xCode 字符的16进制 Unicode 码 (UCS码)

\x 之后的 *Code* 是1-4位的16进制码。下面这个示例中都是在字符串中放置版权符号: "\xA9 1999-2001""\x0A9 1999-2001""\x00A9 1999-2001"。 如果紧跟16进制码后一位的字符也能解释成16进制码时, 就必须把4位补全,否则FreeMarker就会误解你的意图。

原生字符串是一种特殊的字符串。在原生字符串中, 反斜杠和 ${ 没有特殊含义, 它们被视为普通的字符。为了表明字符串是原生字符串, 在开始的引号或单引号之前放置字母r,例如:\

TEMPLATE

1
2
${r"${foo}"}
${r"C:\foo\bar"}

将会输出:

OUTPUT

1
2
${foo}
C:\foo\bar

值域

值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。比如: 0..<m,这里假定 m 变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]。值域的主要作用有:使用 <#list...> 来迭代一定范围内的数字,序列切分和 字符串切分。

值域表达式的通用形式是( startend 可以是任意的结果为数字表达式):

  • start..end: 包含结尾的值域。比如 1..4 就是 [1, 2, 3, 4], 而 4..1 就是 [4, 3, 2, 1]。当心一点, 包含结尾的值域不会是一个空序列,所以 0..length-1 就是 错误的,因为当长度是 0 时, 序列就成了 [0, -1]

  • start..<endstart..!end: 不包含结尾的值域。比如 1..<4 就是 [1, 2, 3]4..<1 就是 [4, 3, 2], 而 1..<1 表示 []。请注意最后一个示例; 结果可以是空序列,和 ..<..! 没有区别; 最后这种形式在应用程序中使用了 < 字符而引发问题(如HTML编辑器等)。

  • start..length: 限定长度的值域,比如 10..4 就是 [10, 11, 12, 13]10..-4 就是 [10, 9, 8, 7],而 10..0 表示 []。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题;请参考 序列切分 来获取更多信息。

  • start..: 无右边界值域。这和限制长度的值域很像,只是长度是无限的。 比如 1.. 就是 [1, 2, 3, 4, 5, 6, ... ],直到无穷大。 但是处理(比如列表显示)这种值域时要万分小心,处理所有项时, 会花费很长时间,直到内存溢出应用程序崩溃。 和限定长度的值域一样,当它们被切分时, 遇到切分后的序列或字符串结尾时,切分就结束了。

    Warning!

    无右边界值域在 FreeMarker 2.3.21 版本以前只能用于切分, 若用于其它用途,它就像空序列一样了。要使用新的特性, 使用 FreeMarker 2.3.21 版本是不够的,程序员要设置 incompatible_improvements 至少到2.3.21版本。

值域的进一步注意事项:

  • 值域表达式本身并没有方括号,比如这样编写代码 <#assign myRange = 0..<x>, 而不是 <#assign myRange = [0..<x]>。 后者会创建一个包含值域的序列。方括号是切分语法的一部分,就像 seq[myRange]
  • 可以在 .. 的两侧编写算术表达式而不需要圆括号, 就像 n + 1 ..< m / 2 - 1
  • ....<..!.. 是运算符, 所以它们中间不能有空格。就像 n .. <m 这样是错误的,但是 n ..< m 这样就可以。
  • 无右边界值域的定义大小是2147483647 (如果 incompatible_improvements 低于2.3.21版本,那么就是0), 这是由于技术上的限制(32位)。但当列表显示它们的时候,实际的长度是无穷大。
  • 值域并不存储它们包含的数字,那么对于 0..10..100000000 来说,创建速度都是一样的, 并且占用的内存也是一样的。
0%