JavaScript设计模式之模板方法模式

模板方法是我们在编码中最常接触的一种设计模式,只需使用继承就可以实现的非常简单的模式。

定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常 在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺 序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。其UML图如下所示

  • Abstract Class 在抽象模板中,定义算法框架,主要步骤等
  • Concrete Class 在具体模板中来实现父类中的具体步骤

实现

下面我们以茶与咖啡这个经典例子来向大家清楚的介绍什么是模板方法。想象一下,我们在泡茶与咖啡时通常有以下的共同点:

  • 将水煮沸
  • 用沸水冲泡茶或咖啡
  • 将冲泡好的茶或咖啡倒入杯中
  • 加入辅料,例如茶可以加些柠檬、咖啡可以加些牛奶和糖

经过以上分析,我们发现咖啡和茶的冲泡过程是大同小异的,因此我们可以将这些相似的步骤抽象出来一个饮料来当做模板方法中的抽象模板,而茶和咖啡作为具体模板继承饮料抽象模板实现其相应的方法就可以了。

问题一

在 Java 中编译器会保证子类会重写父类中的抽象方法,但在 JavaScript 中却没有进行这些检查工作。我们在编写代码的时候得不到任何形式的警告,完全寄托于程序员的记忆力和自觉性是很危险的,特别是当我们使用模板方法模式这种完全依赖继承而实现的设计模式时。

问题二

虽然以上4个步骤适用于绝大多数的人,但是存在一些人不喜欢在泡茶或咖啡的时候加入辅料。对于已经规定好了冲泡饮料的 4 个步骤,那么有什么办法可以让子类不受这个约束呢?

解决方案

针对于问题一,我们可以在抽象模板中需要子类重写的方法中抛出一个错误,如果子类忘记重写该方法,则程序在运行时会报错。

针对于问题二,钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。

下面我用代码来模拟这个例子,首先我们来实现抽象模板——饮料类

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
// 抽象模板--饮料类
class Beverage {

init() {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments()) { // 如果挂钩返回true,则需要调料
this.addCondiments()
}
}

/**
* 烧水
*/
boilWater() {
console.log('把水煮沸')
}

/**
* 用沸水冲泡
*/
brew() {
throw new Error('子类必须重写brew方法')
}

/**
* 倒进杯中
*/
pourInCup() {
throw new Error('子类必须重写pourInCup方法')
}

/**
* 添加辅料
*/
addCondiments() {
throw new Error('子类必须重写addCondiments方法')
}

/**
* 是否需要添加辅料钩子函数
*/
customerWantsCondiments() {
return true //默认为true 需要
}
}

茶类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Tea extends Beverage{
brew() {
console.log('用沸水冲泡茶')
}

pourInCup() {
console.log('将茶倒入杯中')
}

addCondiments() {
console.log('添加柠檬')
}

customerWantsCondiments() {
return false // 不要辅料
}
}

咖啡类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Coffee extends Beverage{
brew() {
console.log('用沸水冲咖啡')
}

pourInCup() {
console.log('将咖啡倒入杯中')
}

addCondiments() {
console.log('添加糖和牛奶')
}

customerWantsCondiments() {
return true // 添加辅料
}
}

const coffee = new Coffee()
coffee.init()

使用场景

假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。

基于Docker搭建CNPM私有仓库

步骤

  • git仓库下克隆cnpmjs.org项目

    1
    git clone https://github.com/cnpm/cnpmjs.org.git
  • 进入到cnpmjs.org项目内

    1
    cd cnpmjs.org
  • 查看Dockerfile文件

    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
    ## 引用node镜像
    FROM node:6.11
    ## 作者
    MAINTAINER Bono Lv <lvscar {aT} gmail.com>

    # Working enviroment工作环境
    ENV \
    CNPM_DIR="/var/app/cnpmjs.org" \
    CNPM_DATA_DIR="/var/data/cnpm_data"

    # 执行命令,在容器中创建目录
    RUN mkdir -p ${CNPM_DIR}

    ## 设置工作目录
    WORKDIR ${CNPM_DIR}

    ## 将package.json文件拷贝到容器的指定目录下
    COPY package.json ${CNPM_DIR}

    ## 设置容器中的npm镜像地址
    RUN npm set registry https://registry.npm.taobao.org

    ## 下载依赖
    RUN npm install

    ## 将宿主机中cnpmjs.org项目下的所有文件拷贝到容器的指定目录中
    COPY . ${CNPM_DIR}

    ## 将宿主机中config.js文件拷贝到容器中指定路径下config目录下,此步非常关键,也就是说容器中将使用## config.js来当做cnpmjs的配置文件
    COPY docs/dockerize/config.js ${CNPM_DIR}/config/

    ## 开放7001和7002端口
    EXPOSE 7001/tcp 7002/tcp

    ## 挂载数据卷绑定给宿主机
    VOLUME ["/var/data/cnpm_data"]

    # Entrypoint容器启动命令
    CMD ["node", "dispatch.js"]
  • 找到docs/dockerize/config.js文件,因为其是cnpmjs的配置文件,所以要修改其中几处配置

    • 在mysql中新建数据库cnpmjs,cnpmjs.org/docs/db.sql文件导入数据库,完成建表工作
    • 修改config.js文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    ## enableCompress设置为true
    enableCompress: true

    ...

    ## 修改数据库配置
    database: {
    db: 'cnpmjs',
    username: 'root',
    // 填写数据库密码
    password: '',
    // 数据源设置为mysql
    dialect: 'mysql',
    // 设置数据库Host地址(注意:如果mysql也运行在docker容器中的话,此处应填写宿主机的ip地址)
    host: '192.168.2.112',
    // 设置mysql端口号
    port: 3306,
    // 其他不需要修改,略...
    }

    ...

    ## 修改镜像地址
    registryHost: '你的Host地址:7001'
  • 接下来我们要构建镜像,在cnpmjs.org文件夹下执行docker build命令,构建镜像(构建过程中需要下载依赖包,时间较长请耐心等待)

    1
    docker build -t cjh/cnpmjs:20180911

    当出现successfully字样的时候就说明已经构建成功了,此时我们执行查看镜像列表命令:

    1
    docker image ls

    此时我们刚刚构建的镜像就在列表中。

  • 然后我们使用构建好的镜像来运行cnpmjs容器

    1
    2
    3
    4
    5
    6
    7
    8
    docker run 
    -d \
    -p 7001:7001 \
    -p 7002:7002 \
    -v /home/cnpm/cnpm_data:/var/data/cnpm_data \
    --restart always \
    --name cnpmjs \
    cjh/cnpmjs:20180911
    • -d 容器在后台运行,并在成功启动容器后输出容器的完整ID
    • -p 设置宿主机端口与容器内端口的映射关系
    • -v 宿主机与容器内文件映射

    待容器启动成功之后,我们来查看正在运行的容器列表:

    1
    docker ps

  • 通过浏览器访问,CNPM服务:http://192.168.2.112:7002

大功告成!关于怎么使用cnpm私有仓库请参考cnpm私有仓库之正确打开方式

JavaScript设计模式之组合模式

上周学习了命令模式,本周我们来学习一个类似于宏命令的一种设计模式——组合模式。什么是组合模式?组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都 可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。

定义

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。其UML图如下所示:

  • Component 是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
  • Leaf 在组合中表示叶子结点对象,叶子结点没有子结点。
  • Composite 定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。

JavaScript中的组合模式

JavaScript 中实现组合模式的难点在于要保证组合对象和叶对象对象拥有同样的方法,这通常需要用鸭子类型的思想对它们进行接口检查。在 JavaScript 中实现组合模式,看起来缺乏一些严谨性,我们的代码算不上安全,但能更快速和自由地开发,这既是 JavaScript 的缺点,也是它的优点。

实现

下面我们用组合模式来实现一个常见的例子,文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树。首先我们需要构造的文件结构如下图所示:

代码实现如下:

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
// 文件夹类
class Folder {
constructor(name) {
this.name = name
this.parent = null
this.components = []
}
/**
* 添加方法
*/
add(component) {
component.parent = this
this.components.push(component)
}

/**
* 扫描方法
*/
scan() {
console.log('开始扫描文件夹' + this.name)
this.components.forEach(component => {
component.scan()
})
}

/**
* 移除方法
*/
remove() {
if (!this.parent) return //根节点或者树外的游离节点
this.parent.components.forEach((component, index) => {
if (component === this) this.parent.components.split(index, 1)
})
}
}

// 文件类
class File {
constructor(name) {
this.name = name
this.parent = null
}
/**
* 添加方法
*/
add() {
throw new Error('不能添加在文件下面');
}

/**
* 扫描方法
*/
scan() {
console.log('开始扫描文件' + this.name)
}

/**
* 移除方法
*/
remove() {
if (!this.parent) return //根节点或者树外的游离节点
this.parent.components.forEach((component, index) => {
if (component === this) this.parent.components.split(index, 1)
})
}
}

// 创建文件夹
const myFolder = new Folder('我的学习资料')
const javaFolder = new Folder('Java')
const jsFolder = new Folder('JavaScript')
const designPatternFolder = new Folder('设计模式')
const javaWebFolder = new Folder('JavaWeb')
const reactFolder = new Folder('React')
const nodeFolder = new Folder('Node')

// 创建文件
const javaFile = new File('Java从入门到精通.pdf')
const springFile = new File('Spring从入门到精通.pdf')
const reactFile = new File('React学习笔记.md')
const jsFile = new File('JavaScript设计模式与开发实践.pdf')

// 向文件夹中添加文件夹以及文件
myFolder.add(javaFolder)
myFolder.add(jsFolder)
myFolder.add(designPatternFolder)

javaFolder.add(javaFile)
javaFolder.add(javaWebFolder)
javaWebFolder.add(springFile)

jsFolder.add(reactFile)

designPatternFolder.add(jsFile)

myFolder.scan()

输出

移除操作

1
2
3
// 移除
jsFolder.remove()
myFolder.scan()

输出

使用场景

组合模式如果运用得当,可以大大简化客户的代码。当有以下两种情况出现的时候我们就可以使用组合模式:

  • 表示对象的部分-整体层次结构 组合模式可以方便地构造一棵树来表示对象的部分整 体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。
  • 统一对待树种所有的对象 组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就 不用写一堆 if、else 语句来分别处理它们。

JavaScript设计模式之命令模式

命令模式对于我来说是一种较为陌生的设计模式,我们用一个生活中的一个例子来引出今天的主角命令模式。假设有一个快餐店,而我是该餐厅的点餐服务员,每天当客人点餐或者打来订餐电话后,我会把他的需求都写在清上,然后交给厨房,客人不用关心是哪些厨师帮他炒菜。这些记录着订餐信息的清单,便是命令模式的命令对象。

定义

有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此的耦合关系。其UML图如下所示:

  • Command

    定义命令的接口,声明执行的方法。

  • Receiver

    接收者,真正执行命令的对象,任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。

  • ConcreteCommand

    命令接口实现对象,通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。

  • Invoker

    要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。

  • Client

    注意不是常规意义上的客户端,其主要作用是创建具体的命令对象,并且设置命令对象的接收者。

JavaScript中命令模式的实现思路

  • 命令模式的接受者Receiver被当成命令对象Command的属性保存起来
  • 约定执行命令的操作默认调用execute方法

实现

基础命令

首先我们按照命令模式的实现思路来实现一个最简单的命令模式

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
// 定义接受者类
class Receive {
constructor(name) {
this.name = name
}
/**
* 保存方法
*/
save() {
console.log(`我是${this.name}的保存操作`)
}

/**
* 删除方法
*/
delete() {
console.log(`我是${this.name}的删除操作`)
}
}

/**
* 定义保存命令
*/
class SaveCommand {
constructor(receiver) {
this.receiver = receiver
}

/**
* 默认的命令执行方法
*/
execute() {
this.receiver.save()
}
}

/**
* 定义删除命令
*/
class DeleteCommand {
constructor(receiver) {
this.receiver = receiver
}

/**
* 默认的命令执行方法
*/
execute() {
this.receiver.delete()
}
}

// 创建接受者对象
const receiver = new Receive('哈哈哈')
// 创建保存命令对象
const saveCommand = new SaveCommand(receiver)
// 创建删除命令对象
const deleteCommand = new SaveCommand(receiver)
// 执行保存命令
saveCommand.execute() //输出:我是哈哈哈的保存操作
// 执行删除命令
deleteCommand.execute() //输出:我是哈哈哈的删除操作

宏命令

宏命令是一组命令的集合,通过执行宏命令的方式可以批量执行命令。例如我们在保存一个人员信息(包含有该人员的联系人信息)的时候,该人员与其相关联系人是一对多的关系,因此这两种信息我们要分别进行保存,示例代码如下:

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
// 人员信息类
class Person {
constructor(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
/**
* 保存方法
*/
save() {
// 具体保存方法省略
console.log(`${this.name}的信息保存成功`)
}
}

// 人员联系人信息类
class PersonContact {
constructor(person) {
this.person = person
}
/**
* 保存方法
*/
save() {
// 具体保存方法省略
console.log(`${this.person.name}的联系人信息保存成功`)
}
}

/**
* 定义保存命令
*/
class SaveCommand {
constructor(receiver) {
this.receiver = receiver
}

/**
* 默认的命令执行方法
*/
execute() {
this.receiver.save()
}
}

/**
* 定义宏命令
*/
class MacroCommand {
constructor() {
this.commandsList = []
}

addCommand(command) {
this.commandsList.push(command)
}

execute() {
this.commandsList.forEach(command => {
command.execute()
})
}
}

// 创建人员信息对象
const person = new Person('张三', 21, '男')
// 创建人员联系人信息对象
const pContact = new PersonContact(person)
// 创建保存命令对象
const saveCommandPerson = new SaveCommand(person)
const saveCommandContact = new SaveCommand(pContact)
// 创建宏命令对象
const macroCommand = new MacroCommand()
macroCommand.addCommand(saveCommandPerson)
macroCommand.addCommand(saveCommandContact)
// 执行宏命令
macroCommand.execute() //输出:张三的信息保存成功 张三的联系人信息保存成功

上面的例子我们还可以在命令中加入撤销函数,从而在保存方法有异常错误的时候执行撤销函数就可以实现事务回滚,借助命令模式,可以简单地实现一个具有原子事务的行为。当一个事务失败时,往往需要回退到执行前的状态,可以借助命令对象保存这种状态,简单地处理回退操作。

JavaScript设计模式之发布订阅模式

发布订阅模式无论是在现实生活中还是在程序的世界中应用都非常之广泛。举个简单的例子,微博是大部分年轻人都会接触使用的一种社交软件,假设在微博中我们关注了一个大V(通常把“粉丝”在50万以上的称为网络大V),当这个大V发布了一条动态后,就会向关注他的所有粉丝推送这条动态,而关注他的粉丝登录微博后就会收到这条动态的推送,这就是一种发布订阅模式。

定义

发布订阅模式又称观察者模式,它定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。其UML图如下所示:

  • Subject

作为发布订阅模式中的发布者,其维护了一个所有订阅它的引用集合,并拥有添加、删除观察者以及通知所有观察者的方法

  • Observer

作为发布订阅模式中的订阅者,为所有具体订阅者定义一个接口,在接到发布者的通知时来更新自己

JavaScript发布订阅模式的实现步骤:

  • 首先要指定好谁充当发布者

  • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者

  • 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函

实现

首先我们来实现发布者的基类,所有继承该发布者基类的子类都会成为发布者。

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
// 定义发布者基类
class Subject {
// 构造函数
constructor() {
this.observers = []
}

/**
* 添加订阅者
*/
add(observer) {
this.observers.push(obj)
}

/**
* 移除订阅者
*/
remove(observer) {
this.observers.forEach((item, index) => {
if (item === observer) this.observers.splice(index, 1);
})
}

/**
* 通知消息
* @param {*} msg
*/
notify(msg) {
this.observers.forEach(item => {
item.receive(this, msg)
})
}
}

然后我们来定义一个大V类,继承自Subject

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
/**
* 定义大V类
*/
class BigV extends Subject {
constructor(name) {
super()
this.name = name
}

/**
* 获取姓名
*/
getName() {
return this.name
}

/**
* 添加粉丝
* @param {} fans
*/
addFans(fans) {
this.add(fans)
}

/**
* 向粉丝推送消息
* @param {*} msg
*/
pushMsg(msg) {
this.notify(msg)
}
}

接着我们来定义粉丝类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 定义粉丝类
*/
class Fans {
constructor(name) {
this.name = name
}
/**
*接受推送消息方法
*/
receive(bigV, msg) {
console.log(`我是粉丝${this.name}接收到了大V${bigV.getName}推送的消息:${msg}`)
}
}

let bigV = new BigV('Eason')
let fans1 = new Fans('Jack')
let fans2 = new Fans('Tom')
bigV.addFans(fans1)
bigV.addFans(fans2)
bigV.pushMsg('祝大家新年快乐!')// 输出: 我是粉丝Jack接收到了大VEason推送的消息:祝大家新年快乐!
我是粉丝Tom接收到了大VEason推送的消息:祝大家新年快乐!

优缺点

通过上面的代码我们可以看出发布订阅模式的优点是**解耦**,作为发布者来说不用关心订阅方接收到推送消息之后究竟要做什么,而订阅方则不用关心发布方会何时发布消息。缺点就是创建订阅者本身就会**消耗一部分时间和内存**,有可能订阅消息到最终都未发生,并且过度使用发布订阅模式会导致程序**难以跟踪和理解**。

总结

我们学习设计模式的最终目的不是会使用某种设计而已,而是能判断出在哪种情况下可以使用设计模式来进行**解耦**和**优化代码**,这将是我接下来学习设计模式的重点。

JavaScript设计模式之迭代器模式

迭代器是我们平时编码中最常用的一个功能,通常由编程语言给我们提供,因此我们自定义迭代器的情况却很少,因此对于迭代器模式的具体实现也不是清楚,下面我们就一起来学习一下在JavaScript中如何实现一个自定义迭代器。

定义

迭代器模式是指提供一种方法顺序的访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。在面向对象语言中UML图如下所示:

  • 聚集类:Aggregate(抽象类)和ConcreteAggregate(具体聚集类)表示聚集类,是用来存储迭代器的数据。在Aggregate(抽象类)中有一个CreateIterator方法,用来获取迭代器

  • 迭代器:迭代器用来为聚集类提供服务,提供了一系列访问聚集类对象元素的方法。

JavaScript中的迭代器

JavaScript中的迭代器可以分为两种,一种是**内部迭代器**,另一种是**外部迭代器**,单从字面上我们并不能区分出这两种迭代器有什么区别,下面我们从定义以及实现来让大家直观的看出这两种迭代器的区别以及其各自用途。
内部迭代器
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,下面我们来实现一个内部迭代器:
1
2
3
4
5
6
7
8
9
10
11
// 定义内部迭代器
let forEach = (arry, callback) => {
// 迭代规则
for(let i = 0; i< arry.length; i++) {
callback(arry[i], i)
}
}
// 执行迭代器
forEach([a, b, c, d], (item, index) => {
console.log(item) // 输出:a b c d
})
如上代码所示,内部迭代器的迭代规则是提前设定好的,因此对于调用该迭代器的对象来说局限性也是很大的,这也正是内部迭代器的缺点。
外部迭代器
外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。外部迭代器的示例代码如下:
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
// 定义外部迭代器类
class Iterator {
// 构造函数
constructor(arry) {
this.currentIndex = 0;
this.arry = arry
}

// 当前数组下标后移一位,若还有下一位,则返回true 若没有下一位,则范围false
next() {
if (this.currentIndex < (this.arry.length - 1)) {
this.currentIndex++
return true
}
return false
}

// 获取当前下标的对象
getCurrentItem() {
return this.arry[this.currentIndex]
}
}

let iterator = new Iterator([1, 2, 3, 4, 5])
while(iterator.next()) {
console.log(iterator.getCurrentItem()) // 最终输出1 2 3 4 5
}

外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。

终止迭代器

终止迭代器就是提供一种可以跳出循环的方法,类似于for循环中的break,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 定义终止迭代器
let forEach = (arry, callback) => {
// 迭代规则
for(let i = 0; i< arry.length; i++) {
if (!callback(arry[i], i)) break;
}
}
// 执行迭代器
forEach([2, 4, 6, 7, 8], (item, index) => {
console.log(item) // 输出:2 4 6 7
return item%2 !== 0 // 当item不为偶数时终止
})

总结

迭代器模式是一种比较简单的设计模式,但是实际开发中使用频率也是非常高的,学会此模式有助于我们对迭代的更深层次的理解。

JavaScript设计模式之代理模式

又到了一周设计模式学习及分享的时间了,本周我要给大家分享的是设计模式中大名鼎鼎的代理模式。代理模式可能大家都听过,并且代理模式在我们日常生活中的例子也有很多,比如明星和经纪人…

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。

定义

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。其UML图如下图所示:

RealImage真实图片类和ProxyImage代理图片类实现了Image接口和该接口中的display()方法,ProxyImage对象中保留了RealImage对象的引用,并在display()方法中封装了对RealImage对象display()方法的调用,当ProxyPatternDemo类中的main方法调用ProxyImage类的display()方法时,也就是相当于间接的调用了RealImage类的display()方法,这就是我所理解代理模式。

职责划分

根据职责来进行划分,代理模式又分为以下8种:

1、远程代理

2、虚拟代理

3、写时复制Copy-on-Write 代理

4、保护(Protect or Access)代理

5、Cache代理

6、防火墙(Firewall)代理

7、同步化(Synchronization)代理

8、智能引用(Smart Reference)代理

JavaScript开发中最常用的是虚拟代理缓存代理

  • 虚拟代理
  • 缓存代理

虚拟代理

根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象,比如浏览器的渲染的时候先显示问题,而图片可以慢慢显示(就是通过虚拟代理代替了真实的图片,此时虚拟代理保存了真实图片的路径和尺寸。用虚拟代理实现图片预加载代码实例:
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
// 图片加载函数
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);

return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();

// 引入代理对象
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
// 图片加载完成,正式加载图片
myImage.setSrc( this.src );
};
return {
setSrc: function(src){
// 图片未被载入时,加载一张提示图片
myImage.setSrc("file://c:/loading.png");
img.src = src;
}
}
})();

// 调用代理对象加载图片
proxyImage.setSrc( "http://images/qq.jpg");
proxyImage 间接地访问MyImage。proxyImage 控制了客户对MyImage 的访问,并且在此过程中加入一些额外的操作(真正的图片加载好之前,先把img 节点的src 设置为一张本地的loading 图片)。看完这段代码之后我们会立马思考一个问题,不使用代理模式我们照样可以实现图片的预加载功能,无非就是在MyImage的setSrc方法中加上图片加载完成监听以及加载本地提示图片这两部分代码,为什么要使用代理模式反而把实现变得复杂了?

思考两个问题:

一、对于MyImage的setSrc函数来说职责过多,既要给img设置src,又要负责预加载图片,违反了面向对象设计原则中的**单一职责原则**;

二、如果后期我们考虑撤销图片预加载功能,就要去修改MyImage的setSrc方法,违反了**开闭原则**;

分析了上面两个问题后,代理模式恰好能规避掉这两个问题,此时就体现出了设计模式的优点。

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。使用缓存代理实现运算结果缓存代码如下:
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
var add = function(){
var sum = 0;
for(var i = 0, l = arguments.length; i < l; i++){
sum += arguments[i];
}
return sum;
};
var proxyAdd = (function(){
var cache = {}; //缓存运算结果的缓存对象
return function(){
var args = Array.prototype.join.call(arguments);//把参数用逗号组成一个字符串作为“键”
if(cache.hasOwnProperty(args)){//等价 args in cache
console.log('使用缓存结果');
return cache[args];//直接使用缓存对象的“值”
}
console.log('计算结果');
return cache[args] = add.apply(this,arguments);//使用本体函数计算结果并加入缓存
}
})();
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));

// 输出结果
计算结果
15
使用缓存结果
15
使用缓存结果
15
通过增加缓存代理的方式,add 函数可以继续专注于自身的职责——计算传入参数的和,缓存的功能是由代理对象实现的。

ES6中的代理Proxy

ES6原生提供了Proxy构造函数,主要作用是在目标对象之前架设一层**拦截**,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。其主要的思想还是设计模式,下面我们就来学习一下如何使用Proxy。

Proxy是一个构造函数,它可以接受两个参数:目标对象(target) 与句柄对象(handler) ,返回一个代理对象Proxy,主要用于从外部控制对对象内部的访问。
1
2
const target = {}, handler = {}
const proxy = new Proxy(target, handler)
  • Proxy、target、handler这三者之间有什么关系呢?

    Proxy的行为很简单:将Proxy的所有内部方法转发至target 。即调用Proxy的方法就会调用target上对应的方法。

  • handler是用来干嘛的?

    handler的方法可以覆写任意代理的内部方法。 外界每次通过Proxy访问target 对象的属性时,就会经过 handler 对象,因此,我们可以通过重写handler对象中的一些方法来做一些拦截的操作。

举个栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user = {
username: 'zhangsan',
password: '123456',
}

var userProxy = new Proxy(user,{
get: function(target, property, receiver){
console.log(`你访问了user的${property}属性`)
return target[prop]
}
});
console.log(userProxy.username)
// 你访问了user的username属性
// zhangsan
如上面的代码,我们访问并输出了username属性,但是运行结果确又额外的输出了我们在`handler`的get方法中预先输出的一句话,这就是拦截器的作用。
handler的内建方法
相信看了上面代码大家还有一个疑问,get方法是什么?get方法是handler对象的14个内建方法之一,我们可以通过重写这些内建方法来自定义拦截器的内容。handler对象拥有以下14个内建对象(我只举四个常用的,其他的请参考博客[深度揭秘ES6代理Proxy](https://blog.csdn.net/qq_28506819/article/details/71077788)):
  1. handler.get(target, property, receiver) 方法用于拦截对象的读取属性操作
    • target,目标对象
    • property,被获取的属性名
    • receiver,Proxy或者继承Proxy的对象
  2. handler.set(target, property, value, receiver) 方法用于拦截设置属性值的操作
    • target,目标对象
    • property,被设置的属性名
    • value,被设置的新值
    • receiver,最初被调用的对象。通常是proxy本身,但handler的set方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是proxy本身)
  3. handler.apply(target, thisArg, argumentsList) 方法用于拦截函数的调用
    • target,目标对象(函数)
    • thisArg,被调用时的上下文对象
    • argumentsList,被调用时的参数列表
  4. handler.construct(target, argumentsList, newTarget)用于来接new操作
    • target,目标对象
    • argumensList,构造器参数列表
    • newTarget,最初调用的构造函数

下面我们就来用ES6语法对虚拟代理例子进行重写

  • 虚拟代理
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
class MyImage {
constructor(document) {
this.imgNode = document.createElement('img')
document.body.appendChild(imgNode)
}

setSrc(src) {
imgNode.src = src
}
}

let myImg = new MyImage(document)

let myImageProxy = new Proxy(myImg.setSrc, {
apply(target, ctx, arguments) {
var img = new Image
img.onload = function(){
// 图片加载完成,正式加载图片
Reflect.apply(target, ctx, arguments)
}
// 图片未被载入时,加载一张提示图片
Reflect.apply(target, ctx, ['file://c:/loading.png'])
img.src = arguments[0]
}
})

// 调用
myImageProxy('http://images/qq.jpg')
  • 缓存代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Add {
constructor() {
var sum = 0;
for(var i = 0, l = arguments.length; i < l; i++){
sum += arguments[i]
}
return {value: sum}
}
}

let AddProxy = new Proxy(Add, {
construct(target, arguments, newTarget) { // newTarget最初的构造函数
let cache = target.cache // 从Add类中取出静态属性cache(缓存运算结果的缓存对象)
var args = Array.prototype.join.call(arguments);//把参数用逗号组成一个字符串作为“键”
if(cache.hasOwnProperty(args)){//等价 args in cache
console.log('使用缓存结果');
return cache[args];//直接使用缓存对象的“值”
}
console.log('计算结果');
return cache[args] = Reflect.construct(target, arguments);//使用本体函数计算结果并加入缓存
}
})

结束语

学习了代理模式之后,我的第一个想法就是赶快将我之前写的带有缓存的请求函数进行一个彻底的改造。

JavaScript设计模式之策略模式

​ 策略模式对于我来说是一种比较陌生的设计模式,单从字面的意思我感觉这种设计模式应该是提供一些策略(解决方案或者方法)。哈哈,可能我理解的有些偏差,那么下面我们就来探究下什么是策略模式,以及在JavaScript中策略模式是什么样子的,优缺点等。

定义

定义一系列的算法,把它们封装起来,并且使它们可以相互替换。其UML图如下图所示:

由上面的UML图我们看出,策略模式中有两个重要的部分:

  • Strategy

    策略部分,定义所有支持的算法的公共接口,ConcreteStrategy类实现该接口,实现具体算法。

  • Context

    上下文部分,主要用来维护一个Strategy对象的引用,根据业务逻辑来决定调用哪个实现了Strategy接口的策略对象(例如:ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC)

JavaScript中的策略模式

在JavaScrip中没有接口这一概念,因此在JavaScript中实现策略模式也是与上面传统的策略模式略有不同,但是大体上是一样的。JavaScrip实现策略模式程序也是由两部分组成,分别是策略类Strategy和环境类Context。策略类封装了具体的算法,并负责具体的计算过程;环境类接受用户的请求,随后把请求委托给某一个策略类。

实现

下面我以一个简单的表单验证来展示一下在JavaScript中策略模式长什么样子。首先我们来实现表单验证中的策略类,代码如下:

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
// 表单验证策略类
class Strategy {
/**
* 是否为空校验
* @param {*} value 校验对象
* @param {*} errorMsg 错误信息
*/
isNonEmpty (value, errorMsg) {
if (value === '') return errorMsg
}

/**
* 最小长度校验
* @param {*} value 校验对象
* @param {*} length 最小长度
* @param {*} errorMsg 错误信息
*/
minLength (value, length, errorMsg) {
if (value.length < length) return errorMsg
}

/**
* 手机号码格式校验
* @param {*} value 校验对象
* @param {*} errorMsg 错误信息
*/
isMobile (value, errorMsg){ // 手机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) return errorMsg
}
}

​ 在表单校验策略类中提供了三种校验方法的实现。在策略模式中策略类Strategy只关注于每种策略函数的实现过程,而不关注于该策略函数何时被调用;而环境类Context则恰恰相反,其关注于根据用户请求来调用某种策略,而不关心该种策略是如何实现的,这正好满足了设计模式中的开闭原则。那么下面我们就来看下表单验证中环境类Context的实现,代码如下:

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
// 表单验证环境类
class Validator {
constructor () {
this.cache = [] // 保存校验规则
this.strategies = new Strategy() //维护一个Strategy对象的引用
}

/**
* 添加校验规则
* @param {*} value 校验对象
* @param {*} rule 以冒号隔开的字符串。冒号前面的代表客户挑选的strategy对象,冒号后面表示在校验过 程中所必需的一些参数
* @param {*} errorMsg 当校验未通过时返回的错误信息
*/
add (value, rule, errorMsg) {
var param = rule.split(':') // 把 strategy 和参数分开
this.cache.push(() => { // 把校验的步骤用空函数包装起来,并且放入cache
var strategy = param.shift() // 用户挑选的 strategy
param.push(errorMsg) // 将errorMsg添加进参数列表
return this.strategies[strategy].apply(value, param)
})
}

/**
* 检验
*/
validate () {
let errorMsgs = []
for (let validatorFunc of this.cache) { // 遍历检验规则
let msg = validatorFunc() // 调用校验方法
if (msg) errorMsgs.push(msg) // 将错误信息存入到errorMsgs
}
if (errorMsgs.length > 0) return errorMsgs // 如果错误信息数组长度大于0则表明没有校验通过
}
}

调用

1
2
3
4
5
6
7
let validator = new Validator() // 创建一个 validator 对象
/***************添加一些校验规则****************/
validator.add('张三' , 'isNonEmpty', '用户名不能为空' )
validator.add('123', 'minLength:6', '密码长度不能少于6位' )
validator.add('135034', 'isMobile', '手机号码格式不正确' )
let errorMsg = validator.validate() // 获得校验结果
errorMsg.forEach(msg => { console.error(msg) }) // 输出:密码长度不能少于6位 手机号码格式不正确

优缺点

  • 优点
    • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句
    • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换、易于理解、易于扩展
    • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作
  • 缺点
    • 会在程序中增加许多的策略类和策略对象
    • 用户必须了解各个Strategy之间的不同点,这样才能选择一个合适的Strategy,因此Strategy要想用户暴露所有的实现,但这实际上是违反了最少知识原则

总结

​ 学习掌握了策略模式之后,我们代码中的if或switch判断会减少一部分,并且书写的代码的可维护度都在增高,也在逐渐满足开闭原则,可谓是优化代码的一个神器。

JavaScript设计模式之单例模式

单例模式算是设计模式中我们最常接触的一种设计模式。在实际开发中,系统中有些对象我们只需要一个,例如线程池,数据库操作对象,全局缓存等,这时候我就要用到单例模式。

定义

单例模式的定义就是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。其UML图如下所示:

​ 单例模式包含的角色只有一个,就是单例类——Singleton。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

实现

ES5写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Singleton = function(name) {
this.name = name
//一个标记,用来判断是否已将创建了该类的实例
this.instance = null
}
// 提供了一个静态方法,用户可以直接在类上调用
Singleton.getInstance = function(name) {
// 没有实例化的时候创建一个该类的实例
if(!this.instance) {
this.instance = new Singleton(name)
}
// 已经实例化了,返回第一次实例化对象的引用
return this.instance
}

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')

console.log(a === b) // 输出true
console.log(a) // 输出Singleton {name: "sven1", instance: null}
console.log(b) // 输出Singleton {name: "sven1", instance: null}

ES6写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton {
constructor(name) {
this.name = name
this.instance = null
}
// 构造一个广为人知的接口,供用户对该类进行实例化
static getInstance(name) {
if(!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
}

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')

console.log(a === b) // 输出true
console.log(a) // 输出Singleton {name: "sven1", instance: null}
console.log(b) // 输出Singleton {name: "sven1", instance: null}

​ 我们在调用两次静态公有方法getInstance之后返回的对象都是同一个,因此达到了我们的目的(保证一个类仅有一个实例)。

​ 相信大家看完这段代码之后和我一样都有一个疑问,那就是在输出a对象和b对象的时候name属性为sven1这在预料之内,但instance为null,就有些出乎意料了。明明在getInstance静态方法中都已经将new Singleton(name)赋值给了this.instance,为什么输出的时候还是null呢?这时候我们不妨将构造函数中的this与静态方法getInstance中的this进行输出

1
2
3
4
5
6
7
8
9
10
// constructor中输出
Singleton {name: "sven1", instance: null}

// getInstance中输出
function(name) {
this.name = name;
//一个标记,用来判断是否已将创建了该类的实例
this.instance = null;
console.log(`constructor:${Singleton}`)
}

​ 原来两个地方的this并不一样,构造函数中的this指向当前类的对象,而静态方法getInstance中的this则指向该静态方法。

漏洞

​ 虽然我们通过该类的静态工厂方法获取到的对象都是同一个对象,但是还记得我们一开始就介绍的单例类的必要条件之一就是私有的构造函数,为什么要有这样一个条件呢?试想一下,当用户拿到一个类之后的第一反应就是new,那么我们所设计的静态工厂方法getInstance此时并没有起到任何作用,下面我们来验证一下:

1
2
3
4
5
6
7
8
9
10

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')
var c = new Singleton('sven3')

console.log(a === b) // 输出true
console.log(a) // 输出Singleton {name: "sven1", instance: null}
console.log(b) // 输出Singleton {name: "sven1", instance: null}
console.log(a === c) // 输出false
console.log(c) // 输出Singleton  {name: "sven3", instance: null}

​ 通过new出来的对象c与通过静态工厂方法getInstance得到的对象(a, b)并不是同一个对象,因此这种单例模式是不是“单“的不够彻底呢。

完善

​ 在JavaScript中我暂时还没有查到如何将构造方法变为私有的,因此我们需要对构造函数做个手脚来堵住这个”漏洞“。

constructor 方法是类的默认方法,通过 new 命令生成对象实例时会自动调用这个方法,类必须有 constructor 方法,如果一个类没有显式定义构造函数,那么一个空的 constructor 方法会自动添加到类中

ES5写法

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
var Singleton = function(name) {
if (!Singleton.instance) {
this.name = name
//一个标记,用来判断是否已将创建了该类的实例
Singleton.instance = this
}
return Singleton.instance
}
// 提供了一个静态方法,用户可以直接在类上调用
Singleton.getInstance = function(name) {
// 没有实例化的时候创建一个该类的实例
if(!this.instance) {
this.instance = new Singleton(name)
}
// 已经实例化了,返回第一次实例化对象的引用
return this.instance
}

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')
var c = new Singleton('sven3')

console.log(a === b) // 输出true
console.log(a) // 输出Singleton {name: "sven1"}
console.log(b) // 输出Singleton {name: "sven1"}
console.log(a === c) // 输出true
console.log(c) // 输出Singleton  {name: "sven1"}

ES6写法

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
class Singleton {
constructor(name) {
if (!Singleton.instance) {
this.name = name
//一个标记,用来判断是否已将创建了该类的实例
Singleton.instance = this
}
return Singleton.instance
}
// 构造一个广为人知的接口,供用户对该类进行实例化
static getInstance(name) {
if(!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
}

var a = Singleton.getInstance('sven1')
var b = Singleton.getInstance('sven2')
var c = new Singleton('sven3')

console.log(a === b) // 输出true
console.log(a) // 输出Singleton {name: "sven1"}
console.log(b) // 输出Singleton {name: "sven1"}
console.log(a === c) // 输出true
console.log(c) // 输出Singleton  {name: "sven1"}

通过在构造函数中增加一个类似的判断之后,这个”漏洞“就被堵上了。

JavaScript设计模式之装饰者模式(ReactNative表单验证)

在任何系统的开发工作中,表单的验证都是一项必不可少的工作,在ReactNative的开发过程中也不过如此。

现状

​ 由于ReactNative原生并没有提供表单验证功能,因此只能求助于第三方的一些插件,目前比较常用的ReactNative表单验证插件有以下几种:

  1. react-native-gifted-form

    react-native-gifted-form是一款非常棒的ReactNative表单验证插件,页面效果非常酷炫,上手也不是很难;但是该项目作者已经很长时间没有维护,并且表单控件只能使用该插件提供的一些控件。

  2. tcomb-form-native

    tcomb-form-native是一款非常容易上手的ReactNative表单验证插件,star也达到了将近3k;同样它的缺点也是表单控件只能使用该插件提供的一些控件,并且表单控件的效果与我们的需求差异很大。

  3. rc-form

    rc-form是antd所推荐的一款表单验证插件,它支持ReactNative,可以使用自己封装的表单控件;但是看完官方给出的demo之后,完全懵逼,瞬间感觉遇到了一块硬骨头。

取舍

​ 以上三种表单验证插件都是非常棒的,各有优缺点,关于要使用哪一种完全取决于自己的业务需求,由于目前的业务需求,我选择了rc-form

rc-from for ReactNative

先来看下官方给出的demo,代码如下:

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
import React from 'react'
import PropTypes from 'prop-types'
import {
StyleSheet,
Button,
Dimensions,
TextInput,
Text,
View,
Alert,
} from 'react-native'

import { createForm } from 'rc-form'

const { width } = Dimensions.get('window')
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
padding: 50,
justifyContent: 'center',
},
inputView: {
width: width - 100,
paddingLeft: 10,
},
input: {
height: 42,
fontSize: 16,
},
errorinfo: {
marginTop: 10,
},
errorinfoText: {
color: 'red',
},
})

class FromItem extends React.PureComponent {
getError = error => {
if (error) {
return error.map(info => {
return (
<Text style={styles.errorinfoText} key={info}>
{info}
</Text>
)
})
}
}
render() {
const { label, onChange, value, error } = this.props
return (
<View style={styles.inputView}>
<TextInput
style={styles.input}
value={value || ''}
label={`${label}:`}
duration={150}
onChangeText={onChange}
highlightColor="#40a9ff"
underlineColorAndroid="#40a9ff"
/>
<View style={styles.errorinfo}>{this.getError(error)}</View>
</View>
)
}
}

class App extends React.Component {
static propTypes = {
form: PropTypes.object.isRequired,
}

checkUserNameOne = (value, callback) => {
setTimeout(() => {
if (value === '15188888888') {
callback('手机号已经被注册')
} else {
callback()
}
}, 2000)
}
submit = () => {
this.props.form.validateFields((error) => {
if (error) return
Alert('通过了所有验证') // eslint-disable-line new-cap
})
}
render() {
const { getFieldDecorator, getFieldError } = this.props.form
return (
<View style={styles.container}>
<Text>简单的手机号验证</Text>
{getFieldDecorator('username', {
validateFirst: true,
rules: [
{ required: true, message: '请输入手机号!' },
{
pattern: /^1\d{10}$/,
message: '请输入正确的手机号!',
},
{
validator: (rule, value, callback) => {
this.checkUserNameOne(value, callback);
},
message: '手机号已经被注册!',
},
],
})(
<FromItem
autoFocus
placeholder="手机号"
error={getFieldError('username')}
/>
)}
<Button color="#40a9ff" onPress={this.submit} title="登陆" />
</View>
)
}
}

export default createForm()(App)

​ 看完这段代码,相信大部分人跟我的感觉是一样的(不就是一个表单验证吗,怎么使用起来这么复杂,让我瞬间怀念起使用Vue的那段日子)。之前做过Vue相关的开发,elementUI和iView中的表单组件使用起来是多么的简洁易懂,怎么到了ReactNative中就变的这么的复杂难懂。先不吐槽了~,来梳理一下这个demo中的思路吧,毕竟思路清晰以后我们还是可以将其再次封装的(毕竟再好用的插件也都是人家进行多次封装之后供我们使用的,这也是一次技术提升的好机会)。

​ 我先来讲一下大致的思路,在代码的最后一句,导出了createForm()(App),createForm是一个React高阶函数,它接收一个React组件(App),在函数内部对该组件进行加工改造,最后返回加工改造之后的新组件。在createForm函数中其将form对象传递给App组件,因此通过this.props.form就可以获取到这个form对象。

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
const { getFieldDecorator, getFieldError } = this.props.form
....
{getFieldDecorator('username', {
validateFirst: true,
rules: [
{ required: true, message: '请输入手机号!' },
{
pattern: /^1\d{10}$/,
message: '请输入正确的手机号!',
},
{
validator: (rule, value, callback) => {
this.checkUserNameOne(value, callback);
},
message: '手机号已经被注册!',
},
],
})(
<FromItem
autoFocus
placeholder="手机号"
error={getFieldError('username')}
/>
)}
...

getFieldDecorator()函数

form提供了getFieldDecorator函数。其函数定义形式为

1
getFieldDecorator(fieldName, fieldOption) => (Component) => ComponentWithExtraProps

getFieldDecorator函数接收两个参数,分别是

  • fieldName:字段名称
  • fieldOption:字段操作对象

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 
* {getFieldDecorator(name,fieldOption)(<FormItem {...props}/>)}将表单项包装为高阶组件后返回
* 实现功能同getFieldProps方法,内部也调用getFieldProps方法
* 与getFieldProps方法不同的是,被封装表单项的props作为this.fieldMeta[name]的originalProps属性
* originalProps属性的主要目的存储被封装表单项的onChange事件,fieldOption下无同类事件时,执行该事件
* 不推荐将value、defaultValue作为表单项组件如FormItem的props属性
*/
getFieldDecorator: function getFieldDecorator(name, fieldOption) {
var _this = this;
// 获取需要传递给被修饰元素的属性。包括onChange,value等
// 同时在该props中设定用于收集元素值得监听事件(onChange),以便后续做双向数据。
var props = this.getFieldProps(name, fieldOption);
return function (fieldElem) {
// 此处fieldStore存储字段数据信息以及元数据信息。
// 数据信息包括value,errors,dirty等
// 元数据信息包括initValue,defaultValue,校验规则等。
var fieldMeta = _this.getFieldMeta(name);
var originalProps = fieldElem.props;
...
fieldMeta.originalProps = originalProps;
fieldMeta.ref = fieldElem.ref;
return _react2["default"].cloneElement(fieldElem, (0, _extends3["default"])({}, props, _this.getFieldValuePropValue(fieldMeta)));
};
};

其返回值是一个React高阶函数,接收参数是FormItem自定义组件,在该高阶函数中将onChange以及value传递给FormItem,然后在FormItem组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
const { label, onChange, value, error } = this.props
return (
<View style={styles.inputView}>
<TextInput
style={styles.input}
value={value || ''}
label={`${label}:`}
duration={150}
onChangeText={onChange}
highlightColor="#40a9ff"
underlineColorAndroid="#40a9ff"
/>
<View style={styles.errorinfo}>{this.getError(error)}</View>
</View>
)
...

获取到onChange以及value后,将其绑定在TextInput组件的onChangeText和value上。(TextInput可以替换为其他的组件,但是替换的组件一定要有value和onChange属性)

getFieldError()函数

form同样还提供了getFieldError函数。其函数定义形式为

1
getFieldError(fieldName) => errors数组

getFieldError函数接收一个参数:

  • fieldName:字段名称(要与getFieldDecorator函数的第一个参数fieldName保持一致)

源码

1
2
3
4
5
6
7
8
9
// 获取this.fields[name]["errors"]错误数据,并剔除没有error.message的错误数据
getFieldError: function getFieldError(name) {
return (0, _utils.getErrorStrs)(this.getFieldMember(name, 'errors'));
},
// 获取this.fields[name][member]属性数据
getFieldMember: function getFieldMember(name, member) {
var field = this.getField(name);
return field && field[member];
},

其返回值是一个数组,数组中存储的是错误信息,拿到错误信息之后,将其传递给FormItem的error属性

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
...
<FromItem
...
error={getFieldError('username')}
/>
...
// 在FromItem.js中
...
const { label, onChange, value, error } = this.props
return (
<View style={styles.inputView}>
...
<View style={styles.errorinfo}>{this.getError(error)}</View>
</View>
)
...
// 返回error信息组件函数
getError = error => {
if (error) {
return error.map(info => {
return (
<Text style={styles.errorinfoText} key={info}>
{info}
</Text>
)
})
}
}

​ 在FromItem组件中,获取到error数组对象之后,调用error信息展示函数,该函数返回错误信息展示组件,从而将错误信息展示给用户。

表单组件的封装

​ 要想对render函数进行瘦身,我们首先要从自定义表单项组件入手。上一节我们讲到getFieldDecorator函数接收fieldName字段名称和fieldOption字段操作对象两个参数,并返回一个React高阶函数(该高阶函数的参数就是自定义表单组件),在该高阶函数中将onChange以及value传递给自定义表单项组件并返回一个加工后的组件对象。因此,我们可以将getFieldDecorator函数放到自定义表单组件中去,代码如下所示:

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
// FormItem.js

import BaseFormItem from './base-form-item' // 表单项组件基类

class FromItem extends BaseFormItem {
render() {
const { label, onChange, value, prop, rules, form } = this.props;
const { getFieldDecorator, getFieldError } = form;
return (
<View style={styles.inputView}>
{getFieldDecorator(prop, {rules})(
<TextInput
style={styles.input}
value={value || ''}
label={`${label}:`}
duration={150}
onChangeText={onChange}
highlightColor="#40a9ff"
underlineColorAndroid="#40a9ff"
/>
<View style={styles.errorinfo}>{this.getError(getFieldError(prop))}</View>
)}
</View>
);
}
}

​ 我们可以看到,上面的代码将getFieldDecorator放到了FormItem组件中去,在页面组件中将form对象,prop和rules传递给FormItem组件,然后在FormItem组件中从页面组件传递过来的form对象中取出getFieldDecoratorgetFieldError函数,getFieldDecorator函数接收的两个参数fieldName(页面组件传递过来的prop)、fieldOption(页面组件传递过来的rules)。

1
2
3
4
5
6
7
8
9
10
11
/**
* 表单项基类
*/
class BaseFormItem extends Component {
getError = (error) => {
if (error) {
return error.map(info => <Text style={{color: 'red'}} key={info}>{info}</Text>)
}
return null
}
}

​ 每一个表单子项都会有显示错误信息的方法,为了避免写重复代码,我们将显示错误信息的方法抽离出来,并创建一个基类,将该方法放到基类中,让所有表单项来继承这个基类。

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
class App extends React.Component {
constructor(props) {
super(props)

this.state = {
rules: { // 定义校验规则
username: [
{ required: true, message: '请输入手机号!' },
{
pattern: /^1\d{10}$/,
message: '请输入正确的手机号!',
},
{
validator: (rule, value, callback) => {
this.checkUserNameOne(value, callback);
},
message: '手机号已经被注册!',
},
],
},
}
}
...
render() {
const { form } = this.props
return (
<View>
<FromItem
form={form}
prop="username"
rules={this.state.rules.username}
autoFocus
placeholder="手机号"
/>
<Button color="#40a9ff" onPress={this.submit} title="登陆" />
</View>
)
}
}

​ 经过改造之后,页面的render函数代码是不是瞬间减少了。但是这个时候我们要考虑另外一个问题,页面中render函数确实被瘦身了,但是表单组件中的render函数却又开始变的臃肿了。如果我们需要来封装一个选择器表单组件,那FormItem中的那段臃肿的代码是不是还要再被重写一遍呢;如果我们后续开发的过程中rc-form的实现形式改变了,或者是换了另外一个表单校验插件,那么我们之前封装的表单组件就全部要进行修改;我们封装的表单组件只能用于表单验证,如果想在非表单验证页面上使用的话也是不可以的。综上所述组件与表单验证是强耦合的,我们能不能将组件表单验证这两个东西给分开呢?试想一下,组件如果不披表单验证这件外衣它就是一个单纯的组件,如果披了这件外衣,它就是带有表单验证功能的组件,表单验证实现的改变不会影响到组件。怎么才能达到这种理想状态呢?

装饰者模式

​ 绕了一大圈子,终于把本文的重点装饰者模式引了出来。

什么是装饰者模式?

​ 装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。通常情况下要给对象扩展功能使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@startuml
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB

Component <|-- Decorator
Component <|-- ConcreteComponent

Decorator o-- Component

Component: +Operation()
ConcreteComponent: +Operation()
Decorator: +Operation()
class ConcreteDecoratorA {
{field} addedState
{method} Operation()
}
ConcreteDecoratorB: +Operation()
ConcreteDecoratorB: AddedBehavior()

@enduml
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
/**
* 表单项装饰函数(详情可参考React高阶组件)
* @param {*} WrappedComponent 传入React组件对象
* @returns 经过加工后新的React组件
*/
const formItemDecorator = function formItemDecorator(WrappedComponent) {
return class extends Component {
componentWillMount() {
const { form, prop, rules } = this.props
const { getFieldDecorator } = form
this.fieldDecorator = getFieldDecorator(prop, {rules: [...rules]})
}

render() {
const {form, prop} = this.props
const {getFieldError} = form
return (
<View>
{this.fieldDecorator(<WrappedComponent {...this.props} error={getFieldError(prop)} />)}
</View>
)
}
}
}


//InputItem.js中使用
export default formItemDecorator(InputItem)