1. 首页
  2. 学习室

JS设计模式

单例模式

1.定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

2. 核心

确保只有一个实例,并提供全局访问

3.实现 :全局弹窗

弹窗是前端开发中一个比较常规的需求,下面定义了一个简易的MessageBox,用于实例化各种弹窗

class MessageBox {
    show() {
        console.log("show");
    }
    hide() {}
}
let box1 = new MessageBox();
let box2 = new MessageBox();
console.log(box1 === box2); // false

在常规情况下,一般同一时间只会存在一个全局弹窗,我们可以实现单例模式,保证每次实例化时返回的实际上是同一个方法

class MessageBox {
    show() {
        console.log("show");
    }
    hide() {}

    static getInstance() {
        if (!MessageBox.instance) {
            MessageBox.instance = new MessageBox();
        }
        return MessageBox.instance;
    }
}

let box3 = MessageBox.getInstance();
let box4 = MessageBox.getInstance();

console.log(box3 === box4); // true

上面这种是比较常见的单例模式实现,这种方式存在一些弊端

  • 需要让调用方了解到通过Message.getInstance来获取单例

  • 假设需求变更,可以通过存在二次弹窗,则需要改动不少地方,因为 MessageBox 除了实现常规的弹窗逻辑之外,还需要负责维护单例的逻辑

因此,可以将初始化单例的逻辑单独维护,换言之,我们需要实现一个通用的、返回某个类对应单例的方法,通过闭包可以很轻松的解决这个问题

function getSingleton(ClassName) {
    let instance;
    return () => {
        if (!instance) {
            instance = new ClassName();
        }
        return instance;
    };
}

const createMessageBox = getSingleton(MessageBox);
let box5 = createMessageBox();
let box6 = createMessageBox();
console.log(box5 === box6);

这样,通过createMessageBox返回的始终是同一个实例。

如果在某些场景下需要生成另外的实例,则可以重新生成一个createMessageBox方法,或者直接调用new MessageBox(),这样就对之前的逻辑不会有任何影响。

策略模式

1. 定义

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

2. 核心

将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用

策略模式的主要作用是:将层级相同的逻辑封装成可以组合和替换的策略方法,减少 if...else 代码,方便扩展后续功能。

前端以前的表单验证

function onFormSubmit(params) {
    if (!params.nickname) {
        return showError("请填写昵称");
    }
    if (params.nickname.length > 6) {
        return showError("昵称最多6位字符");
    }
    if (!/^1\d{10}$/.test(params.phone))
        return showError("请填写正确的手机号");
    }
    // ...
    sendSubmit(params)
}

关于 if..else 代码的罪过想必大家都比较熟悉了,这种写法还有一些额外的问题

  • 将所有字段的校验规则都堆叠在一起,如果想查看某个字段的校验规则,则需要将所有的判断都看一遍(避免某个同事将同一个字段的两种判断放在了不同的位置)

  • 在遇见错误时,直接通过 return 跳过了后面的判断;如果产品希望直接展示每个字段的错误,则改动的工作量可不谓不少。

不过目前在antdELementUI等框架盛行的年代,在 Form 组件中已经很少看见这种代码了,这要归功于async-validator。

下面我们来实现一个建议的validator

class Schema {
    constructor(descriptor) {
        //校验规则
        this.descriptor = descriptor;
    }

    validate(data) {
        return new Promise((resolve, reject) => {
            let keys = Object.keys(data);
            let errors = [];
            for (let key of keys) {
                //获取规则
                const config = this.descriptor[key];
                if (!config) continue;

                const { validator } = config;
                try {
                    //校验
                    validator(data[key]);
                } catch (e) {
                    errors.push(e.toString());
                }
            }
            if (errors.length) {
                reject(errors);
            } else {
                resolve();
            }
        });
    }
}

声明每个字段的校验规则

// 首先声明每个字段的校验规则
const descriptor = {
    nickname: {
        validator(val) {
            if (!val) {
                throw "请填写昵称";
            }
            if (val.length < 6) {
                throw "昵称最多6位字符";
            }
        },
    },
    phone: {
        validator(val) {
            if (!val) {
                throw "请填写电话号码";
            }
            if (!/^1\d{10}$/.test(val)) {
                throw "请填写正确的手机号";
            }
        },
    },
};

最后校验数据源

// 开始校验
const validator = new Schema(descriptor);
//校验的规则跟需要校验的字段,其变量名必须一致
const params = { nickname: "", phone: "123000" }; 
validator
    .validate(params)
    .then(() => {
        console.log("success");
    })
    .catch((e) => {
        console.log(e);
    });

可以看见,Schema主要暴露了构造参数和validate两个接口,是一个通用的工具类,而params是表单提交的数据源,因此主要的校验逻辑实际上是在descriptor中声明的。

在上面的实现中,我们按照字段的维度,为每个字段实现了一个validator方法,用于处理校验该字段需要的逻辑。

实际上我们可以拆分出一些更通用的规则,比如required(必填)、len(长度)、min/max(最值)等,这样,当多个字段存在一些类似的校验逻辑时,可以尽可能地复用。

修改一下 descriptor,将每一个字段的校验规则类型修改为列表,列表中每个元素的 key 表示这条规则的名称,validator作为自定义规则

const descriptor = {
    nickname: [
        { key: "required", message: "请填写昵称" },
        { key: "max", params: 6, message: "昵称最多6位字符" },
    ],
    phone: [
        { key: "required", message: "请填写电话号码" },
        {
            key: "validator",
            params(val) {
                return !/^1\d{10}$/.test(val);
            },
            message: "请填写正确的电话号码",
        },
    ],
};

然后修改Schema的实现,增加handleRule方法

class Schema {
    constructor(descriptor) {
        this.descriptor = descriptor;
    }

    handleRule(val, rule) {
        const { key, params, message } = rule;
        let ruleMap = {
            required() {
                return !val;
            },
            max() {
                return val > params;
            },
            validator() {
                return params(val);
            },
        };

        let handler = ruleMap[key];
        if (handler && handler()) {
            throw message;
        }
    }

    validate(data) {
        return new Promise((resolve, reject) => {
            let keys = Object.keys(data);
            let errors = [];
            for (let key of keys) {
                const ruleList = this.descriptor[key];
                if (!Array.isArray(ruleList) || !ruleList.length) continue;

                const val = data[key];
                for (let rule of ruleList) {
                    try {
                        this.handleRule(val, rule);
                    } catch (e) {
                        errors.push(e.toString());
                    }
                }
            }
            if (errors.length) {
                reject(errors);
            } else {
                resolve();
            }
        });
    }
}

这样,就可以将常见的校验规则都放在ruleMap中,并暴露给使用者自己组合各种校验规则,比之前各种不可复用的 if..else 判断会更容易维护和迭代。

const descriptor = {
            nickname: [{
                    key: "required",
                    message: "请填写昵称"
                },
                {
                    key: "max",
                    params: 6,
                    message: "昵称最多6位字符"
                },
            ],
            phone: [{
                    key: "required",
                    message: "请填写电话号码"
                },
                {
                    key: "validator",
                    params(val) {
                        return !/^1\d{10}$/.test(val);
                    },
                    message: "请填写正确的电话号码",
                },
            ],
        };

        class Schema {
            constructor(descriptor) {
                this.descriptor = descriptor;
            }

            handleRule(val, rule) {
                // key,params,message 获取规则中的key、方法、提示信息
                const {
                    key,
                    params,
                    message
                } = rule;
                
                let ruleMap = {
                    required() {
                        return !val;
                    },
                    max() {
                        return val > params;
                    },
                    validator() {
                        return params(val);
                    },
                };
                //校验数据是否匹配,不匹配输出message
                let handler = ruleMap[key];
                if (handler && handler()) {
                    throw message;
                }
            }

            validate(data) {
                return new Promise((resolve, reject) => {
                    let keys = Object.keys(data);
                    let errors = [];
                    for (let key of keys) {
                        // ruleList 获取规则的key,value
                        const ruleList = this.descriptor[key];
                        if (!Array.isArray(ruleList) || !ruleList.length) continue;
                        // val 获取输入的数据的value
                        const val = data[key];
                        for (let rule of ruleList) {
                            try {
                                this.handleRule(val, rule);
                            } catch (e) {
                                errors.push(e.toString());
                            }
                        }
                    }
                    if (errors.length) {
                        reject(errors);
                    } else {
                        resolve();
                    }
                });
            }
        }
        // 开始校验
        const validator = new Schema(descriptor);
        //校验的规则跟需要校验的字段,其变量名必须一致
        const params = {
            nickname: "啦啦啦",
            phone: "12345678901"
        };
        validator
            .validate(params)
            .then(() => {
                console.log("success");
            })
            .catch((e) => {
                console.log(e);
            });

工厂模式

工厂模式提供了一种创建对象的方法,对使用方隐藏了对象的具体实现细节,并使用一个公共的接口来创建对象。

前端本地存储目前最常见的方案就是使用localStorage,为了避免在业务代码里面散落各种getItemsetItem,我们可以做一下最简单的封装

封装 storage

let themeModel = {
    name: "local_theme",
    get() {
        let val = localStorage.getItem(this.name);
        return val && JSON.parse(val);
    },
    set(val) {
        localStorage.setItem(this.name, JSON.stringify(val));
    },
    remove() {
        localStorage.removeItem(this.name);
    },
};
themeModel.get();
themeModel.set({ darkMode: true });

这样,通过themeModel暴露的getset接口,我们无需再维护local_theme;但上面的封装也存在一些可见的问题,新增 10 个 name,则上面的模板代码需要重新写 10 遍?

为了解决这个问题,我们可以将创建 Model 对象的逻辑进行封装

const storageMap = new Map()
function createStorageModel(key, storage = localStorage) {
    // 相同key返回单例
    if (storageMap.has(key)) {
        return storageMap.get(key);
    }

    const model = {
        key,
        set(val) {
            storage.setItem(this.key, JSON.stringify(val););
        },
        get() {
            let val = storage.getItem(this.key);
            return val && JSON.parse(val);
        },
        remove() {
            storage.removeItem(this.key);
        },
    };
    storageMap.set(key, model);
    return model;
}

const themeModel =  createStorageModel('local_theme', localStorage)
const utmSourceModel = createStorageModel('utm_source', sessionStorage)

这样,我们就可以通过createStorageModel创建各种不同本地存储接口对象,而无需关注创建对象的具体细节。

 
[打赏一下]
  • 版权声明:本文基于《知识共享署名-相同方式共享 3.0 中国大陆许可协议》发布,转载请遵循本协议
  • 文章链接:https://www.imiowo.cn/624.html [复制] (转载时请注明本文出处及文章链接)
上一篇:
:下一篇
今天又是荒废的一天。

作者:月亽

月亽
介绍:大西瓜

文章:48篇

最后更新:20-12-25

发表评论

gravatar

当前页面评论被关闭,原因如下: