前端开发用到的精妙设计模式

一、创建型:单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。

1.1、如何才能保证一个类仅有一个实例

当我们创建了一个类(构造函数)后,可以通过new关键字调用,从而生成任意多的实例对象。

javascript 复制代码
class Toast {
    show() {
        console.log('我是一个单例对象')
    }
}
const t1 = new Toast()
const t2 = new Toast()

t1 === t2// false

我们先 new 了一个t1,又 new 了一个 t2,很明显 t1和t2是相互独立的对象,各占一块内存空间,没有关系。而单例模式需要做到的是,不管我们创建多少个,它都只给你返回第一次所创建的、唯一的一个实例。

上述代码改成使用构造函数实现的单例模式,我们需要确保每次调用构造函数时都返回同一个实例。这通常通过在构造函数外部维护一个静态属性来存储已创建的实例,并在每次实例化时检查这个属性是否已经存在实例。:

javascript 复制代码
class Toast {
    show() {
        console.log('我是一个单例对象');
    }
  
    static getInstance() {
        if (!Toast.instance) {
            Toast.instance = new Toast(); // 仅在第一次调用时创建实例
        }
        return Toast.instance;
    }
}

const t1 = Toast.getInstance();
const t2 = Toast.getInstance();

// 输出 true,因为 t1 和 t2 引用的是同一个实例
console.log(t1 === t2);  // true

1.2、实践:使用单例模式实现Vant的toast组件

1.2.1. 引入和注册

Toast 组件是通过插件的方式引入和注册的,

ini 复制代码
const Toast = require('vant/lib/toast').default

Vue.prototype.$TM={}
Vue.prototype.$TM.Toast = Toast

module.exports.install = function(_components){
    _components.forEach(function (_component) {
        if(_component.name){         
            Vue.component(_component.name.replace('van','tm'), _component);
        } 
        if (_component.install) Vue.use(_component);
    });
}

toC框架中代码如下:

这样,Toast 组件就被注册到了 Vue 的全局实例上,可以通过 this.$TM.Toast 函数来调用。

1.2.2. Toast 函数的实现

Toast 组件的实现核心在于其提供的函数式接口。当你调用 this.$TM.Toast.loading({}) 等方法时,实际上是在调用一个已经封装好的函数。这个函数负责创建 Toast 实例、更新其状态,并将其显示在页面上。

在 Vant 的源码中,Toast 函数的大致实现如下:

scss 复制代码
function Toast(options = {}) {
  // 创建一个 Toast 实例
  const toast = createInstance();
  
  // 如果之前的 Toast 还在显示,则更新其 z-index,以确保它能够正确地显示在页面上其他内容之上
  if (toast.value) {
    toast.updateZIndex();
  }
  
  // 解析并合并传入的选项
  options = parseOptions(options);
  
  // 扩展 Toast 实例的属性
  _extends(toast, transformOptions(options));
  
  // 如果设置了持续时间,则在时间到达后自动关闭 Toast
  if (options.duration > 0) {
    toast.timer = setTimeout(() => {
      toast.clear();
    }, options.duration);
  }
  
  // 返回 Toast 实例
  return toast;
}

toC框架中代码如下:

createInstance 负责创建并返回一个 Toast 实例,是一个 Vue 组件的实例,它继承了 Toast 组件的所有属性和方法。

1.2.3. createInstance 函数的实现

createInstance 函数的大致实现如下:

arduino 复制代码
function createInstance() {
  // 如果当前是在服务器端渲染环境下,则直接返回一个空对象
  if (_utils.isServer) {
    return {};
  }

  // 对队列进行过滤,保留当前需要被关注或可能在未来显示的 toast 实例
  queue = queue.filter(function (item) {
    return !item.$el.parentNode || isInDocument(item.$el);
  });

  // 如果队列为空或允许创建多个实例,则创建一个新的 Toast 实例
  if (!queue.length || multiple) {
    var toast = new (_vue.default.extend(_Toast.default))({
      el: document.createElement('div')
    });
    // 将新创建的 Toast 实例添加到队列中
    queue.push(toast);
  }

  // 返回队列中的最后一个 Toast 实例,允许调用者获取最新的 toast 实例
  return queue[queue.length - 1];
}

toC框架中代码如下:

在这个函数中,创建的实例被添加到一个全局队列 queue 中,以便管理。

二、行为型:策略模式

策略模式是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以互换。

重构小能手,拆分"胖逻辑":将层级相同的逻辑封装成可以组合和替换的策略方法,减少 if...else 代码,方便扩展后续功能。

2.1、改造if-else侠

kotlin 复制代码
ConfirmOK(){
        var _this=this;
        if(this.ConfirmFlag=="1"){
          this.checkConfirm2();

        }else if(this.ConfirmFlag=="2"){
          this.checkConfirm3();

        }else if(this.ConfirmFlag=="3"){
          this.checkConfirm4();

        }else if(this.ConfirmFlag=="4"){
          this.checkConfirm5();

        }else if(this.ConfirmFlag=="5"){
          this.checkConfirm6();

        }else if(this.ConfirmFlag=="6"){
          this.checkConfirm7();

        }else if(this.ConfirmFlag=="7"){
          this.checkConfirm8();

        }else if(this.ConfirmFlag=="8"){
          this.checkConfirm9();

        }else if(this.ConfirmFlag=="9"){
          this.checkConfirm10();

        }else if(this.ConfirmFlag=="10"){
          this.checkConfirm11();

        }else if(this.ConfirmFlag=="11"){
          this.checkConfirm12();

        }else if(this.ConfirmFlag=="12"){
          $("#ConfirmModal").modal("hide");
          _this.ChangeStaData(_this.activeName);
          _this.activeName= _this.CheckFlag;
          $("#UserNO").val(_this.OnlyDataList.RecognitionInfo.YHBH);
          _this.getBillInfo(_this.OnlyDataList.StaShareList[_this.activeName]);
        }
      },

ConfirmOK 方法中,每次当 ConfirmFlag 的值增加或变化时,都需要修改这个方法以添加新的条件分支。这就违反了开放-封闭原则(对扩展开放,对修改封闭),因为方法对于修改(添加新的条件分支)是开放的,而不是封闭的。

我们可以怎么优化呢?

javascript 复制代码
const confirmStrategies = {
  "1": function() { this.checkConfirm2(); },
  "2": function() { this.checkConfirm3(); },
  "3": function() { this.checkConfirm4(); },
  "4": function() { this.checkConfirm5(); },
  "5": function() { this.checkConfirm6(); },
  "6": function() { this.checkConfirm7(); },
  "7": function() { this.checkConfirm8(); },
  "8": function() { this.checkConfirm9(); },
  "9": function() { this.checkConfirm10(); },
  "10": function() { this.checkConfirm11(); },
  "11": function() { this.checkConfirm12(); },
  "12": function() { this.checkConfirm13(); }
};
ConfirmOK() {
  var _this = this;
  const strategy = confirmStrategies[this.ConfirmFlag];
  if (strategy) {
    strategy.call(_this);
  } else {
    console.log(`没有发现ConfirmFlag: ${this.ConfirmFlag}`);
  }
}

首先,我们定义一个策略对象,包含每个 ConfirmFlag 对应的函数,然后在 ConfirmOK 方法中,使用这个策略对象来调用相应的函数。这样,我们就将原始的 if-else 结构替换为了一个更加灵活和可扩展的策略对象。如果将来需要添加新的 ConfirmFlag 处理逻辑,只需在 confirmStrategies 对象中添加新的键值对即可,无需修改 ConfirmOK 方法本身。

2.2、改造表单校验

以下代码中,将所有字段的校验规则都堆叠在一起,如果想查看某个字段的校验规则,则需要将所有的判断都看一遍

我们该怎么优化一下呢?

javascript 复制代码
class Schema {
  constructor(descriptor) {
    this.descriptor = descriptor;
  }
  handleRule(val, rule) {
    const { key, params, message } = rule;
    const ruleMap = {
      required: () => !val,
      max: () => (typeof val === 'number' && typeof params === 'number') && val > params,
      validator: () => (typeof params === 'function') && params(val),
    };
    const handler = ruleMap[key];
    if (handler && handler()) {
      throw new Error(message); // 使用Error对象来存储错误信息
    }
  }
  validate(data) {
    return new Promise((resolve, reject) => {
      const keys = Object.keys(data);
      for (const key of keys) {
        const ruleList = this.descriptor[key];
        if (!Array.isArray(ruleList) || !ruleList.length) continue;
        const val = data[key];
        for (const rule of ruleList) {
          try {
            this.handleRule(val, rule);
          } catch (e) {
            // 立即拒绝Promise并返回错误信息
            reject(e.message);
            return;
          }
        }
      }
      // 如果没有错误发生,则解析Promise
      resolve();
    });
  }
}
export default Schema;

上面文件中Schema主要暴露了构造参数和validate两个接口,是一个通用的工具类,而params是数据源,主要的校验逻辑是在descriptor中声明的。

javascript 复制代码
let descriptor = {};
let params = this.invoiceInf;

// 定义正则表达式
let mySpecialReg = /[α&"'<>^\]/;
let regTaxCode = /^[0-9A-Z]*$/;
let regMobile = /^[0-9-+()]*$/;
let regEmail = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$/;

// 抬头中客户名称或企业名称必输
descriptor.InvoiceTitle = [
  {
    key: "required",
    message: `请填写${this.invoiceInf.InvoiceClass == '2' ? '企业名称' : '客户名称'}`
  },
  {
    key: "validator",
    params: (val) => { return mySpecialReg.test(val); },
    message: `${this.invoiceInf.InvoiceClass == '2' ? '企业名称' : '客户名称'}中包含特殊字符,请修正`
  }
];

// 校验电子邮箱
if (this.invoiceInf.IsPostEmail == '1' || this.sumMoney.EFlag > 0) {
  params.emailIpt = $("#emailIpt").val();
  descriptor.emailIpt = [
    {
      key: "required",
      message: `${this.sumMoney.EFlag > 0 ? '请输入电子邮件地址以获取电子发票' : '请输入电子邮件地址以获取充电记录详情'}`
    },
    {
      key: "validator",
      params: (val) => { return !regEmail.test(val); },
      message: "电子邮件地址格式错误,请修正"
    }
  ];
}

// 企业需判断纳税人识别号
if (this.invoiceInf.InvoiceClass == 2) {
  descriptor.TaxCode = [
    {
      key: "required",
      message: "请填写纳税人识别号"
    },
    {
      key: "validator",
      params: (val) => { return !regTaxCode.test(val); },
      message: "纳税人识别号格式错误,请修正"
    }
  ];
}

// 增值税专票需另外判断
if (this.invoiceInf.InvoiceType == 2) {
  descriptor.RegisterTel = [{ key: "required", message: "请填写注册电话" }];
  descriptor.RegisterAddress = [{ key: "required", message: "请填写注册地址" }];
  descriptor.BankName = [{ key: "required", message: "请填写开户银行" }];
  descriptor.BankAccount = [{ key: "required", message: "请填写银行账户" }];
}

// 判断收货地址
if (this.sumMoney.PFlag > 0 || this.NewHLHTInvoiceFlag == 1) {
  descriptor.Taker = [
    {
      key: "required",
      message: "请填写收件人"
    },
    {
      key: "validator",
      params: (val) => { return mySpecialReg.test(val); },
      message: "收件人有特殊字符,请修正"
    }
  ];
  descriptor.TakerTel = [
    {
      key: "required",
      message: "请填写联系电话"
    },
    {
      key: "validator",
      params: (val) => { return !regMobile.test(val); },
      message: "收件人联系电话格式错误,请修正"
    }
  ];
  descriptor.TakerAddress = [{ key: "required", message: "请填写收件地址" }];
}

// 开始校验
const validator = new Schema(descriptor);
validator.validate(params)
  .then(() => {
    console.log("success");
  })
  .catch((e) => {
    Toast.fail(e);
    console.log(e);
  });

在上面的实现中,我们为需要校验的字段实现了一些通用的规则,通过策略模式,我们可以灵活地添加新的验证规则并组合不同的规则来实现复杂的表单验证逻辑,同时保持代码的可维护性和可扩展性。

相关推荐
tester Jeffky1 小时前
ECMAScrip 与 ES2015(ES6):JavaScript 现代化编程的里程碑
javascript·正则表达式·es6
海上彼尚1 小时前
前端自己也能开启HTTPS
前端·vue.js·https·vue
陪你去流浪_2 小时前
Vue iframe嵌套的页面实现路由缓存 实现keep-alive效果
前端·vue.js·缓存
Jing_jing_X2 小时前
心情追忆- SEO优化提升用户发现率
前端·后端·产品经理·个人开发·流量运营
牵牛老人2 小时前
Qt控件的盒子模型,了解边距边线和内容区
开发语言·javascript·qt
ekskef_sef2 小时前
2024年前端真实面试题集合(Vue篇02)
前端·javascript·vue.js
zhaocarbon3 小时前
vue canvas 绘制选定区域 矩形框
前端·javascript·vue.js
萧鼎3 小时前
探索 Robyn 框架 —— 下一代高性能 Web 框架
前端·robyn
violet_evergarden.3 小时前
【前端开发】HTML+CSS网页,可以拿来当作业(免费开源)
前端·css·html
13 iug^3 小时前
Vue路由进阶攻略
前端·javascript·vue.js