前端开发中应用过的策略模式

402 阅读1分钟

一系列算法封装起来,并使它们相互之间可以替换。被封装起来的算法具有独立性,外部不可改变其特性

数据字典

个人理解中数据字典天然符合策略模式的概念 所以将其归类于策略模式的应用

const dates = ['日','一','二','三','四','五','六']
console.log(`今天是星期${dates[new Date().getDay()]}`)

const statusArr = ['保存','编辑','审核','审核通过','审核不通过','关闭']
statusArr[0] //=>保存
statusArr[3] //=>审核通过
statusArr[6] //=>关闭
// 以上是一个简单的枚举状态回显例子
const statusObj = {
    'new':'新增',
    'edit':'编辑',
    'pass':'审核',
    'close':'关闭'
}
let code = 'close'
statusObj[code]
// 非枚举状态下的回显 实际业务可能存在非枚举的的类型 依旧可以通过这种方式回显

//以下是模拟i18n场景的数据字典
const DEFAULT = 'zh-CN' // 默认读取
const dictionary = { // 国际化字典表
  'zh-CN':{
    hello:'你好',
    message:'消息',
    home:'首页'
  },
  'en':{
    hello:'hello',
    message:'message',
    home:'home'
  }
}

// 安装新的数据字典方法
function installNew(params,info){
  Object.entries(info).forEach(([key,text])=>{
      // 转换传入的对象为键值对数组
      // params 为需要按照的字段
      // 这里的key 对应语言
      // 这里的text 对应实际文本
      dictionary[key][params] = text
  })
}

// 翻译方法
function i18n(language,params){
  const info = dictionary[language] || dictionary[DEFAULT]
    // 根据传入的语言提取字典的对应语言对象 如果没有读取到那么读取默认语言对象
  return info[params] // 返回语言对象的对应参数
}


installNew('user',{
    // 需要安装user这个字段
    'zh-CN':'用户',
    en:'user'
})

i18n('zh-CN','user') // => 用户
i18n('en','user') // => user

i18n('zh-CN','home') // => 首页
i18n('en','home') // => home
i18n('jp','home') // => 首页

自动分发

🌰 汽车总站会有不同线路的公交 他们之间线路形式可能部分重叠,目的站不一定相同
那么我们从选择一条路线的公交上车必定是按照既有的路线行驶
策略模式的行为与齐差不多

// 这里我们假设一个业务场景并且用策略模式实现
// 1. 按下 a键 输出描述 '你好'
// 2. 按下 b键 弹出一个时间戳
// 3. 按下s 执行 a键逻辑 然后执行b键逻辑 但是不增加策略执行次数
// 4. 按下esc 输出执行不同策略的次数 要求自动添加在新增新增策略是不需要添加额外代码处理次数记录
const bus = {
    run(e){ // 自动分发路口 当成例子中的汽车总站
        const {key} = e // 从传入的键盘事件提取按键的key
        const fn = this.methods[key] // 从自身的方法
        if(fn){
            this.nums[key] ? this.nums[key]++ : this.nums[key] = 1 // 判断是否执行过这个策略如果没有那么直接设置为1 如果有就增长1
            fn.call(this,e) // 这里修改方法的this指向使其可以读取整个策略对象
        }
    },
    nums:{},// => 储存执行次数数据
    methods:{
        a(){
            console.log('你好')
        },
        b(){
            alert(+new Date())
        },
        s(){
            this.methods.a()
            this.methods.b()
        },
        Escape(){
            console.table(this.nums)
        }
    }
}
window.addEventListener('keydown',e=>bus.run(e))

使用策略模式开发一个表格类的数据单向更新 以及方向键移动

  1. 支持快捷键上下左右移动焦点
  2. 表格数据可以新增/删除/编辑并且实时同步到数据源
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <style>
        table {
            border-collapse: collapse;

        }

        th,
        td {
            border: 1px solid #9c9c9c;
            width: 200px;
        }

        input[type="text"] {
            padding: 0px;
            margin: 0px;
            outline: 1px;
            width: 100%;
            border: 0px;
            height: 30px;
        }

        input[type="text"]:focus {
            box-shadow: 0px 0px 1px 2px #9f9f9f;

        }
    </style>
</head>

<body>
<input value="新增一行" type="button" e-click="addRow" />
<table>
    <tbody></tbody>
</table>
</body>
<script>
    function on(type, lister) { //全局监听封装简化代码量
        return window.addEventListener(type, lister)
    }
    function require(msg) {// 必传参数校验
        throw new Error(msg)
    }

    class MyTable {
        _config = { // 默认配置
            el: 'table',
            cols: [],
            data: []
        }
        dom = null // 操作的实例
        constructor(config) {
            Object.assign(this._config, config)//拷贝传入的配置到自身
            const { el } = this._config // 提取参数
            this.dom = document.querySelector(el) || require('不存在的dom') // 获取dom没有获取到自动报错结束实例化
            this.renderHeader() //渲染表头
            this.load() // 渲染内容
            this.addLiser() // 监听表格
        }
        addLiser() {
            on('click', e => {
            // 元素被点击 => 提取来源dom => 尝试提取dom上配置的e-click属性 => 如果存在主动调用累自身方法,这里只做demo就不写兼容代码了
                // 监听点击事件 来源dom如果存在 e-click 属性 自动读取自身对应的方法名去执行
                const { target } = e
                const eName = target.attributes['e-click']
                if (eName) {
                    this[eName.value](e)
                }
            })
            on('input', e => {
            // 元素出发input事件 => 尝试提取e-input的值 => 如果实例自身存在对应方法 => 主动调用方法传入事件 => 提取事件来源input框自定义的index field => 根据index提取当前行对应的数据对象通过field动态设置对应属性的值
                // 监听点击事件 来源dom如果存在 e-input 属性 自动读取自身对应的方法名去执行
                const { target } = e
                const eName = target.attributes['e-input']
                if (eName) {
                    this[eName.value](e)
                }
            })
            on('keydown', e => {
                // 监听方向键 自动分发到实例内部对应的切换焦点方法
                const { key, target } = e
                if (target.attributes.focus && typeof this[key] === 'function') {
                    const { index, field } = target.dataset
                    this[key](target, index * 1, field)
                }
            })
        }
        getNext(index, field) {
            // 获取传入的 索引行 以及 字段的焦点内容
            return this.dom.querySelector(`[data-index="${index}"][data-field="${field}"][focus]`)
        }
        getFocusList(index) {
            // 获取当前行所有可聚焦的dom
            return this.dom.querySelectorAll(`[data-index="${index}"][focus]`)
        }
        ArrowUp(target, index, field) {
            //获取上一行同字段的输入框 没有就使用自身
            const el = this.getNext(index - 1, field) || target
            el.focus()
            el.select()
        }
        ArrowDown(target, index, field) {
            const el = this.getNext(index + 1, field) || target
            el.focus()
            el.select()
        }
        ArrowLeft(target, index, field) {
            // 获取当前行所有可聚焦的输入框然后根据自身索引找到上一个
            const els = [...this.dom.querySelectorAll(`[data-index="${index}"][focus]`)]
            const i = els.indexOf(target)
            const prv = els[i - 1] || target
            prv.focus()
            prv.select()

        }
        ArrowRight(target, index, field) {
            const els = [...this.dom.querySelectorAll(`[data-index="${index}"][focus]`)]
            const i = els.indexOf(target)
            const next = els[i + 1] || target
            next.focus()
            next.select()

        }

        renderHeader() {
            const vm = document.createElement('table')
            const ths = this._config.cols.reduce((prv, next) => `${prv}<th>${next.title || ''}</th>`, '')
            vm.innerHTML = `<thead><tr>${ths}<th>操作</th></tr></thead>`
            this.dom.append(...vm.children)
        }
        load() {
            const { cols, data } = this._config
            const body = this.dom.querySelector('tbody')
            // 渲染所有行内容的字符串
            const trs = data.reduce((prv, next, index) => {
                return prv + MyTable.renderTr(cols, next, index)
            }, '')
            body.innerHTML = trs // 替换表格体内容
            console.table(this._config.data)
        }

        static renderTr(cols, row, index) {
            // 批量渲染行数据
            return `<tr>
                ${cols.reduce((prv, next) => {
                return prv + MyTable.renderTD(row, index, next.field)
            }, '')}
                <td>
                    <input value="删除" type="button" e-click="removeRow" data-index="${index}"/>
                </td>
            </tr>`

        }
        static renderTD(row, index, field) {
            // 渲染单个数据内容是 td标签内包含input
            // focus标记 => 可聚焦内容
            // e-input => 输入内容后触发input事件
            // data-index => 数据对象索引
            // data-field => 数据字段
            return `<td><input focus type="text" e-input="update" data-index="${index}" data-field="${field}" value="${row[field] || ''}" /></td>`
        }




        addRow() {
            // 推入空数据
            this._config.data.push({})
            // 重新画表格
            this.load()
        }
        removeRow(e) {
            const { target } = e
            const { index } = target.dataset
            // 提取索引
            this._config.data.splice(index, 1)
            // 删除数组中的对应索引内容
            this.load()
            // 重新画表格
        }
        update(e) {
            const { target } = e
            const { index, field, value } = target.dataset
            // 提取索引 和 数据字段
            // 根据索引提取对象 根据字段更新值
            this._config.data[index][field] = target.value
            console.table(this._config.data)
        }
    }
    new MyTable({
        cols: [
            {
                field: 'msg',
                title: '消息'
            },
            {
                field: 'user',
                title: '用户',
            },
            {
                field: 'remark',
                title: '备注'
            }
        ],
        data: [{}]
    })
</script>

</html>