接手一个五年老项目,用三种设计模式去优化改造一下,纯实践无理论~

4,486 阅读4分钟

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。

最近接手了一个老项目,我一看提交,天啊足足有五年的历史了,里面的东西都挺原始的,老大要求我在做这个项目需求的过程中,看能不能尽量地去顺便优化一下~好吧,那就优化吧~于是乎就先从一些公用的东西先优化,主要是用了一些设计模式的思想

虽然简单,但是还是挺有意义的~

策略模式 -> 表单校验更方便

背景

我发现项目中很多地方在提交表单时,做校验的方式非常地直接了当,例如

const verifyForm = (formData) => {
  if (formData.userName == '') {
    console.log('用户名不能为空');
    return false
  };
  if (formData.password.length < 6) {
    console.log('密码长度不能小于6位');
    return false;
  }
  if ((!/(^1[3|5|8][0-9]{9}$)/.test(formData.phone)) {
    console.log('手机格式错误');
    return false
  }
}

而且这么写的地方非常多,这导致很多逻辑都重复了,所以我想使用策略模式再加上一些必要的封装,去让代码复用性更强,更好维护

改造的设想

以下是伪代码,具体得自己去实现

我的改造设想是这样的

const validator = new Validator(formData)
const validators = [
  {
    name: '用户名',
    field: 'userName',
    validator: 'isNonEmpty'
  },
  {
    name: '密码',
    field: 'password',
    validator: 'length:6'
  },
  {
    name: '手机',
    field: 'phone',
    validator: 'phone'
  }
]
for (let v of validators) {
  validator.add(v)
}
validator.start().then(() => {
  // 校验通过
}).catch((err) => {
  // 校验不通过
})

其实就是维护一个 Validator 的类,专门用来进行表单的校验,肯定会有人反驳说,你看这代码不是更多了吗?但是这样做的好处是很大的

  • 可维护性强: 有一天需要改校验规则,只需要改一处,不需要每个页面都去改
  • 复用性强: 比较统一,防止你自己一个页面一个页面去敲的时候敲错了

代码实现

那接下来就开始代码实现吧,核心就是实现 Validator 这个类,利用策略模式的思想,让校验更加地方便快捷高质量~

// 维护一个规则的map
const ruleMap = {
  isNonEmpty: (name, v) => {
    if (v === '') {
      return `${name}不能为空`
    }
  },
  length: (name, v, len) => {
    if (v.length < len) {
      return `${name}不能少于${len}位`
    }
  },
  phone: (name, v) => {
    if (!/(^1[3|5|8][0-9]{9}$)/.test(v)) {
      return `${name}不是一个手机号`
    }
  }
}

class Validator {

  // 记录校验结果
  result = [];

  constructor(formData) {
    this.formData = formData;
  }

  add({
    name,
    field,
    validator
  }) {
    const formItem = this.formData[field];
    // 区分length与其他规则
    if (validator.includes('length')) {
      const va = validator.split(':')
      result.push(ruleMap['length'](name, formItem, va[1]))
    } else {
      result.push(ruleMap[validator](name, formItem))
    }
  }

  start() {
    return new Promise((resolve, reject) => {
      const fails = this.result.filter((v) => v)
      if (fails.length) {
        // 失败走catch
        reject(fails)
      } else {
        // 成功走then
        resolve(true)
      }
    })
  }
}

适配器模式 -> 适配多种数据格式

背景

项目中有很多页面,页面中包括了:

  • 列表
  • 下拉框
  • 多选框

这看似是三种数据格式,但是其实,这三个数据,是通过同一个接口返回的数据来生成的。。。

所以很多地方都是这么写:

list = [];
selectOptions = [];
checkedOptions = [];
http().then((res) => {
  // 处理成列表格式
  this.list = xxxxx;
  // 处理成下拉框格式
  this.selectOptions = sssss;
  // 处理成多选框格式;
  this.checkedOptions = ccccc;
})

上面省略了许多代码,实际上很多页面的逻辑都是一样的,如果这样写的话导致很多重复的代码,非常冗余

改造的设想

我设想的是实现一个适配器的类,类上拥有一些方法, 可以将数据源转换成所需要的格式

class Adpater {
  dataSouce = [];

  constructor(dataSouce) {
    this.dataSouce = dataSouce;
  }

  transformToList() {
    // 转换成列表数据格式
  }

  transformToSelectOptions() {
    // 转换成下拉框数据格式
  }

  transformToCheckedOptions() {
    // 转换成多选框数据格式
  }
}

代码实现

其实代码实现也挺简单的,实现不难,能让代码更加精简,复用性更强

class Adpater {
  dataSouce = [];

  constructor(dataSouce) {
    this.dataSouce = dataSouce;
  }

  transformToList(columns) {
    // 转换成列表数据格式
    return this.dataSouce.map((item) => {
      const obj = {}
      for (let c of columns) {
        const field = c.field
        obj[field] = item[field];
      }
      return obj
    })
  }

  transformToSelectOptions(options) {
    // 转换成下拉框数据格式
    const { labelField, valueField } = options
    return this.dataSouce.map((item) => ({
      label: item[labelField],
      value: item[valueField]
    }))
  }

  transformToCheckedOptions(options) {
    // 转换成多选框数据格式
    const { valueField } = options
    return this.dataSouce.map((item) => ({
      checked: false,
      value: item[valueField]
    }))
  }
}

使用的时候只需要构建一个 Adapter 即可,使用里面的方法去进行转换

list = [];
selectOptions = [];
checkedOptions = [];
http().then((data) => {
  const adapter = new Adapter(data)
  // 处理成列表格式
  this.list = adapter.transformToList(columns);
  // 处理成下拉框格式
  this.selectOptions = adapter.transformToSelectOptions({
  labelField: 'name',
  valueField: 'id'
  });
  // 处理成多选框格式;
  this.checkedOptions = adapter.transformToCheckedOptions({
  valueField: 'id'
  });;
})

发布订阅模式 -> 两页面间的通信

背景

这个老项目有一个 BUG,当然这个 BUG 不是我写的,而是老 BUG,是这样的有两个页面

  • 页面 A:有同步代码,有异步代码
  • 页面 B:全是同步代码

注意:此项目是老项目,没有全局状态管理工具!!!

// 页面A
console.log(1)
console.log(2)
http.get(url).then(res => {
  console.log(3)
  localStorage.setItem(key, res)
})

// 页面B
console.log(
  localStorage.getItem(key)
)

然后这两个页面是先后加载的,那么我们可以得出输出顺序是

1 // 页面A
2 // 页面A
undefined // 页面B
console.log(3) // 页面A

因为请求是异步的,导致页面B那边拿不到 localStorage 里面的东西,而无法完成很多操作,导致了出现 BUG。所以得想想怎么去解决这个 BUG。

定时器

最简单的就是利用定时器去解决

// 页面B
setTimeout(() => {
  console.log(
  localStorage.getItem(key)
  )
})

但是这样是不对的,不好维护,滥用定时器会导致以后可能会有新的 BUG 出现!!!

发布订阅模式

所以还是使用发布订阅,首先实现一个发布订阅中心

以下是简单实现

type Callback<T> = (data: T) => void;

class PubSub<T> {
  private subscribers: Callback<T>[] = [];

  subscribe(callback: Callback<T>): void {
    this.subscribers.push(callback);
  }

  unsubscribe(callback: Callback<T>): void {
    this.subscribers = this.subscribers.filter(fn => fn !== callback);
  }

  publish(data: T): void {
    this.subscribers.forEach(fn => fn(data));
  }
}

export const ps = new PubSub();

接着就可以用它来解决我们那个 BUG 了!!

// 页面A
console.log(1)
console.log(2)
http.get(url).then(res => {
  console.log(3)
  localStorage.setItem(key, res)
  ps.publish(res)
})

// 页面B
// 订阅
ps.subscribe((res) => {
  console.log(res)
  console.log(
    localStorage.getItem(key)
  )
})

现在的输出顺序就是

1 // 页面A
2 // 页面A
console.log(3) // 页面A
res // 页面B
res // 页面B

结语 & 加学习群 & 摸鱼群

我是林三心

  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;
  • 一个偏前端的全干工程师;
  • 一个不正经的掘金作者;
  • 一个逗比的B站up主;
  • 一个不帅的小红书博主;
  • 一个喜欢打铁的篮球菜鸟;
  • 一个喜欢历史的乏味少年;
  • 一个喜欢rap的五音不全弱鸡

如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点

image.png