单例模式
1.定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
2. 核心
确保只有一个实例,并提供全局访问
弹窗是前端开发中一个比较常规的需求,下面定义了一个简易的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 跳过了后面的判断;如果产品希望直接展示每个字段的错误,则改动的工作量可不谓不少。
不过目前在antd
、ELementUI
等框架盛行的年代,在 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
,为了避免在业务代码里面散落各种getItem
和setItem
,我们可以做一下最简单的封装
封装 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
暴露的get
、set
接口,我们无需再维护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.com/624.html [复制] (转载时请注明本文出处及文章链接)
发表评论