ZooMze's World

vuePress-theme-reco ZooMze    2018 - 2021
ZooMze's World ZooMze's World

Choose mode

  • dark
  • auto
  • light
主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
author-avatar

ZooMze

35

Article

23

Tag

主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
  • 2021-Q1 技术分享与探讨

    • 数据标红功能的实现
      • 前置准备
      • 初步校验
      • 寻找位置
      • 拆分vnode
      • 替换color属性
      • 重组vnode
      • 完整代码
    • data的定义规范
      • data 通用命名思路
        • 文件的统一上传

        2021-Q1 技术分享与探讨

        vuePress-theme-reco ZooMze    2018 - 2021

        2021-Q1 技术分享与探讨


        ZooMze 2021-01-15 自定义指令

        # 数据标红功能的实现

        在数据资源汇聚平台中, 有一个标红显示字段的功能, 这个功能的需求是, 在表格内每一cell都进行一次文本过滤, 将匹配的字符串(精确匹配)的进行 标红 / 标红并显示星号 **

        这个功能其实是对标签的二次渲染, 因为初次渲染是直接由数据驱动, 数据本身是不具备标红的信息的, 二次渲染比较好的方法就是通过自定义指令来实现

        # 前置准备

        在了解如何实现之前, 如果还不太熟悉自定义指令, 可以看看这里: 自定义指令, 也可以去vue官网查看更详细的指令教程

        前置知识也准备好了, 回到正题. 正如上述所说, 数据本身是不具备标红信息的, 它只是纯粹的数据, 这时需要额外的 数据来源 来确定需要标红的字符串数组(兼容多个词语的情况), 然后我们再来创建一些用于标红的源数据, 大致就是这样:

        data() {
          return {
            redTags: ['这是', '标红', '词语'],
            sourceData: [
              {
                name: '这是源数据',
                info: '看看标红不'
              },
              {
                name: '这是不是词语',
                info: '看看省略不'
              },
              {
                name: '这是短语',
                info: '看看省略不'
              }
            ]
          }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19

        然后再注册好指令:

        directives: {
          redTag: {
            bind(el, binding, vNode) {
              return binding.value // 这里先直接返回, 我们下一步再来完善逻辑
            }
          }
        }
        
        1
        2
        3
        4
        5
        6
        7

        对比数据准备好之后再来看看使用指令时的参数传递, 指令是建立在页面标签的基础之上的, 所以在template中需要传递足够的信息来让指令判断是否需要标红

         

        <p v-for="item in sourceData" style="color: blue;" :key="item.name" v-redTag="item.name"></p>
        
        1

        让我们先把这些数据渲染出来吧:

        # 初步校验

        在本部分要校验两个东西: 类型, 数据是否为空, 还有一个是数据否出现在标红列表中的校验, 我们在下一小节来讨论

        这两样有一样不满足时, 直接返回原值就行了

        校验的方法可以访问这里[判断数据类型]](./OriginalJavaScript.md#对象), 下面直接贴代码了

        bind(el, binding, vNode) {
          let getType = Object.prototype.toString
        
          // 类型校验
          if(getType.call(binding.value) != '[object Number]' && getType.call(binding.value) != '[object String]') {
            el.innerHTML = binding.value
            return false
          }
        
          // 判断非空
          const redTagsLength = vnode.context.redTags.length
          if(!binding.value || && redTagsLength == 0) {
            // 这里同时处理了一下 binding.value === 0 的情况 因为 (0 || '') >> 输出 >> ('')
            el.innerHTML = `${binding.value == 0 ? '0' : (binding.value || '')}` || ''
            return binding.value
          }
        
          el.innerHTML = binding.value + ' 初步校验成功的'
          // ... to be continue
        
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21

        为什么数字类型也要校验

        因为数字类型(Number)也将会进行标红逻辑, 其显示在HTML中与String无异, 所以其也应进入校验的逻辑

        其余数据类型其实也不必太过担心显示到页面上会有多可怕, 因为只要数据通过JSON传递自动转换成String/Number/null/Object类型, 如果无法解析时, 会被JSON自动舍弃, 如果觉得不放心时可以执行一次JSON.parse(JSON.stringify())

        JSON.parse(JSON.stringify({
          a: NaN,
          b: (a) => {return 'a'},
          c: new Date(),
          d: undefined,
          e: null,
          f: new Object(),
          g: new Array()
        }))
        // >> 输出 >> {a: null, c: '2021-03-17T06:58:48.831Z', e: null, f: {}, g: []}
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10

        经过上一步之后, 现在仍在执行的函数内肯定是一个非空字符串啦

        实现标红效果的最基本逻辑就是对比字符串, 如果有匹配项则进行标红处理, 当不存在任何标红信息时直接跳过所有逻辑

        # 寻找位置

        遍历标红词语redTags, 找出其在源数据字符串中出现的位置和其本身的长度, 这里用while()累加查找

        // 记录原始字符串, 避免数字类型导致对比出错
        let valueString = `${binding.value}`
        
        let redMatched = []
        vnode.context.redTags.forEach(word => {
          // 轮训查找字符串 直到字符串结束
          let index = 0; //开始的位置
          while ((index = valueString.indexOf(word, index)) != -1) {
            // 如果是-1情况,说明找完了
            redMatched.push({
              index: index,
              length: word.length
            })
            index += word.length;
          }
        })
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16

        此时的redMatched的三次输出分别为

        [{index: 0, length: 2}] // '这是'
        [{index: 0, length: 2}, {index: 2, length: 2}] // '这是', '词语'
        [{index: 0, length: 2}] // '这是'
        
        1
        2
        3

        # 拆分vnode

        有了上一步找出的标红具体的顺序要想实现标红就好办了, 当然纯粹的#text节点当然无法表现出红色的信息, 所以我们借助HTML的特性, 给其添加color属性, 在需要的节点改变color的值即可

        先将源数据binding.value逐个拆分成由单个字符标签代码片段

        let valueArray = []
        if(valueString.length > 0) {
          valueArray = valueString.split('').map(value => {
            return `<span style="color: blue;">${value}</span>`
          })
        } else {
          valueArray = [valueString]
        }
        
        1
        2
        3
        4
        5
        6
        7
        8

        注意

        单个字符也需要将其处理成数组哦!

        '这是源数据' 拆完之后的valueArray变成这样:

        [
          '<span style="color: blue;">这</span>',
          '<span style="color: blue;">是</span>',
          '<span style="color: blue;">源</span>',
          '<span style="color: blue;">数</span>',
          '<span style="color: blue;">据</span>'
        ]
        
        1
        2
        3
        4
        5
        6
        7

        # 替换color属性

        所有的准备工作已就绪, 然后就是根据redMatched的信息将拆分好的数组进行重组

        resultArray = valueArray.map((value, valueIndex) => {
          let flag = false
          flag = redMatched.some(matchInfo => {
            return (valueIndex >= matchInfo.index
            && valueIndex < matchInfo.index + matchInfo.length)
          })
          return flag ? value.replace('blue' , 'red') : value // 替换颜色
        })
        
        1
        2
        3
        4
        5
        6
        7
        8

        此时resultArray为:

        [
          '<span style="color: red;">这</span>',
          '<span style="color: red;">是</span>',
          '<span style="color: blue;">源</span>',
          '<span style="color: blue;">数</span>',
          '<span style="color: blue;">据</span>'
        ]
        
        1
        2
        3
        4
        5
        6
        7

        # 重组vnode

        最后一步啦, 现在数组resultArray组合起来即可啦, 按照此种方法处理后无须担心数据重复处理, 毕竟已经被标红的内容再次执行replace()也会是无事发生

        el.innerHTML = resultArray.join('')
        
        1

        然后来看看执行的结果吧(可以打开控制台检查每个字符)~

        # 完整代码

        其中参数部分本例中暂未使用, 写出来是作为以后可以参考写法

        提示

        在使用动态参数v-directive:[arg]时, 如果需要自己组合数据当做参数时, 注意不要使用空格:

        <!-- 错误写法-->
        <div v-directive:[{ a: scope.row, b: scope }]="scope.row.data"></div>
        
        <!-- 正确写法-->
        <div v-directive:[{a:scope.row,b:scope}]="scope.row.data"></div>
        
        1
        2
        3
        4
        5

        同时arg中的值全部为表达式, 传入字符串是不行的哦

        <template>
          <div>
            <p v-for="item in sourceData" style="color: blue;" :key="item.name" v-redTag:[{param:item.info,params:item.name}]="item.name"></p>
          </div>
        </template>
        
        <script>
          export default {
            directives: {
              redTag: {
                bind: function(el, binding, vnode) {
                  let getType = Object.prototype.toString
        
                  // 类型校验
                  if(getType.call(binding.value) != '[object Number]' && getType.call(binding.value) != '[object String]') {
                    el.innerHTML = binding.value
                    return false
                  }
        
                  // 判断非空
                  const redTagsLength = vnode.context.redTags.length
                  if(!binding.value || redTagsLength == 0) {
                    // 这里同时处理了一下 binding.value === 0 的情况 因为 (0 || '') >> 输出 >> ('')
                    el.innerHTML = `${binding.value === 0 ? '0' : (binding.value || '')}` || ''
                    return binding.value
                  }
        
                  // 记录原始字符串, 避免数字类型导致对比出错
                  let valueString = `${binding.value}`
        
        
                  let valueArray = []
                  if(valueString.length > 0) {
                    valueArray = valueString.split('').map(value => {
                      return `<span style="color: blue;">${value}</span>`
                    })
                  } else {
                    valueArray = [valueString]
                  }
        
                  // 开始记录当前字符串内是否有标红词语出现, 出现则记录其开始index位置以及该标红词的长度length
                  let redMatched = []
                  vnode.context.redTags.forEach(word => {
                    // 轮训查找字符串 直到字符串结束
                    let index = 0; //开始的位置
                    while ((index = valueString.indexOf(word, index)) != -1) {
                      // 如果是-1情况,说明找完了
                      redMatched.push({
                        index: index,
                        length: word.length
                      })
                      index += word.length;
                    }
                  })
        
                  let resultArray = []
                  resultArray = valueArray.map((value, valueIndex) => {
                    let flag = false
                    flag = redMatched.some(matchInfo => {
                      return (valueIndex >= matchInfo.index && valueIndex < matchInfo.index + matchInfo.length)
                    })
                    return flag ? value.replace('blue' , 'red') : value // 替换颜色
                  })
        
                  el.innerHTML = resultArray.join('')
                }
              }
            },
            data() {
              return {
                redTags: ['这是', '标红', '词语'],
                sourceData: [
                  {
                    name: '这是源数据',
                    info: '看看标红不'
                  },
                  {
                    name: '这是不是词语',
                    info: '看看省略不'
                  },
                  {
                    name: '这是短语',
                    info: '看看省略不'
                  }
                ]
              }
            }
          }
        </script>
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89

        # data的定义规范

        每个页面中通常都需要定义大量的data数据, 页面越复杂data自然越臃肿:

        data() {
          return {
            name: '',
            gender: 1,
            age: 1
            // 通常是同区块的数据放置在一起 不同的区块用换行隔开
        
            dialogOneVisible: false,
            dialoOneTitle: '标题1',
            dialoOneContent: '内容1',
        
            dialogTwoVisible: false,
            dialogTwoTitle: '标题2',
            dialogTwoContent: '内容2',
        
            form: {
              keyword: '',
              type: 1,
            },
            // ...
          }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22

        为了更直观的定义data, 可以将不同区域的数据直接放置于不同的对象下, 方便统一维护, 同时也避免了在同一根对象下命名太冗长的问题:

        data() {
          return {
            userInfo: {
              name: '',
              gender: 1,
              age: 1
            },
            form: { ... }, // 置于上方
            dialogOneInfo: {
              visible: false,
              title: '标题1',
              content: '内容1',
            }
            dialogTwoInfo: {
              visible: false,
              title: '标题2',
              content: '内容2',
            }
            // ...
          }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21

        参照流式文档的模式, data 数据在定义时可以遵循页面的文档流顺序, 方便阅读与对应

        例如 <dialog> 组件一般直接放置于文档流最末尾, 表格所对应的查询表单<form> 通常在文档流靠前的位置等. 实际情况当然遵循页面实际的文档流顺序来调整

        除此之外, 当出现复杂对话框内容却因业务特殊(无处复用)而无需拆分组件时, 同样可以按照上述的规则进行嵌套

        data() {
          return {
            userInfo: { ... },
            form: { ... },
            dialogInfo: {
              visible: false,
              title: '新建对话框'
            },
            complexDialogInfo: {
              visible: false,
              title: '复杂对话框',
              searchForm: { ... }, // 对话框-表格 对应的筛选条件form
              tableData: [], // 对话框-表格 数据
              pageInfo: { ... } // 对话框-表格 分页信息
            }
            // ...
          }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18

        # data 通用命名思路

        经过上面的改造, data() 内目前算是规整了许多, 但你可能也经常遇到如下这种情况: 新的A页面跟已写好的B页面长得几乎一样, 功能也非常类似就是换了一些文本名称, 然后你就开始直接Ctrl+C + Ctrl+V 复制A页面

        但是复制过去的页面有非常多琐碎的问题: 最恶心的莫过于跟A页面内与其功能指代高度耦合的字段名, 为了代码可读性这些肯定是要改的, 所以为了尽量避免这个问题, data()(甚至methods)都可以采用尽量通用的方式来命名, 这样复用代码也会更快乐, 那么来试试下面的命名规则:

        data() {
          return {
            searchForm: { // -- 泛指页面内的主要查询表单
              name: '',
              // ...表单里面的字段无法做到通用化...
            }
            tableData: [], // -- 泛指页面内的主要table数据
            pageInfo: { // -- 泛指页面内的主要table对应的分页信息
              pageNo: 1,
              pageSize: 10,
              total: 999 // 这三个字段可以做到通用
            }
          }
        },
        created() {
          this.findPreData() // 也可以放在mounted
        },
        mounted() {
          this.findTableList()
        },
        methods: {
          /**
           * 查询页面所需前置数据(仅限在页面初始就进行查询的前置数据)
           */
          findPreData() { ... },
          /**
           * 查询主要列表
           */
          findTableList() { ... },
          /**
           * 查询列表项详情
           */
          findDetail(id) { ... },
          /**
           * 提交主要表单
           */
          submitForm() { ... },
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38

        上述例子中主要都围绕着, 代码命名空间解耦页面功能来展开, 越低的耦合度意味着更高的复用度~

        # 文件的统一上传

        通常的代码中, 我们通过<el-upload>组件上传文件时, 总是通过action属性直接上传, 这样在上传数量较多的内容时会多次反复调用上传接口, 而且还会导致在填写表单时, 还未进行保存就已经上传, 导致上传了很多无用数据

        于是我们来自定义一下这个上传, 通过http-request属性来重写





















         




         























        <template>
          <el-upload
            action="/api/v1/fs/store"
            multiple
            :limit="1000"
            :on-change="handleChanged"
            :http-request="handleUpload">
            <el-button>点击上传</el-button>
          </el-upload>
        
          <el-button @click="handleSubmit">点击提交</el-button>
        </template>
        
        <script>
        // ...
          methods: {
            handleChanged(file, fileList) {
              this.fileList = fileList
            },
            handleUpload(data) {
              // 用此方法阻止默认的action提交
              console.log(this.fileList)
            },
            // 在需要的时候统一再提交
            handleSubmit(data) {
              // 校验文件是否过大, 注意fileList中的文件是在raw字段内
              const isLt100M = this.fileList.every((file) => file.raw.size / 1024 / 1024 < 100);
              if (!isLt100M) {
                this.$message.error('请检查,上传文件大小不能超过100MB!');
              } else {
                let formData = new FormData()
                this.fileList.forEach(item => {
                  // 参数名以后台需求为准
                  formData.append('files', item.raw)
                })
                this.$http.uploadStoreImgs(formData).then((res) => {
                  if (res.status == 200) {
                    this.$message.success('上传成功');
                  }
                })
                .finally(() => {
                  this.loading = false;
                });
              }
            },
          }
        // ...
        </script>
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48