• 基于装饰器对通用表格的封装


    在Vue中如何将通用的方法抽象出来进行封装,mixin是一种方式,但是mixin的缺点也很明显,关于mixin的缺点这里不做过多介绍,也可以通过这篇文章介绍的通过装饰器对通用列表的分装也是一种比较好的处理方式。

    DataContext

    先定义一个基础的数据上下文,作为对数据的操作逻辑的抽象,以达到对逻辑的复用。

    1. import { getEvents } from './attachEvent'
    2. class DataContext {
    3. @getEvents
    4. events
    5. }
    6. export { DataContext }

    getEvents

    1. export const getEvents = (target, key) => {
    2. return {
    3. get () {
    4. console.log('访问了key ===> ' + key)
    5. const cacheKey = `__${key}_events__`
    6. return this[cacheKey] ?? (this[cacheKey] = {})
    7. },
    8. set () {}
    9. }
    10. }

    对于装饰器不了解的这里不做过多的介绍,上述代码的意义是给 events 属性添加 get set 方法便于访问时会触发,此处的 target 指向的是实例的 __proto__

    1. const data = new DataContext()
    2. console.log('data.events ===> ', data.events)
    3. console.log('data ===> ', data)

    再定义一个表格通用的类主要目的是为了将通用列表的基础通用逻辑进行封装

    TableDataContext

    1. import { DataContext } from './dataContext'
    2. import { getEvents } from './attachEvent'
    3. import { bind } from './bind'
    4. class TableDataContext extends DataContext {
    5. @getEvents
    6. tableEvents
    7. constructor() {
    8. super()
    9. }
    10. @bind
    11. handleFieldWidthChanged() {
    12. console.log('change')
    13. }
    14. }
    15. export { TableDataContext }

    bind

    给类的成员方法绑定this。

    1. const bind = (target, key, descriptor) => {
    2. return {
    3. configurable: true,
    4. get() {
    5. console.log('key ===> ', key)
    6. console.log('target ===> ', target)
    7. const boundFn = descriptor.value.bind(this)
    8. Object.defineProperty(this, key, {
    9. configurable: true,
    10. get() {
    11. return boundFn
    12. }
    13. })
    14. return boundFn
    15. }
    16. }
    17. }

    调用  TableDataContext 查看 this上的属性和方法

    1. const table = new TableDataContext()
    2. console.log('table ===> ', table)
    3. console.log(table.handleFieldWidthChanged())

    attachTableEvent

    在 Vue 中基本上是通过 $emit 方法触发自定义事件的,但是在 .vue 文件中如何触发 .js 文件中定义的这些方法呢,这里对 TableDataContext 里面的 handleFieldWidthChanged 的方法进行改动,为方法绑定一个装饰器

    1. import { attachTableEvent } from './attachEvent'
    2. class TableDataContext extends DataContext {
    3. @bind
    4. @attachTableEvent('resize-change')
    5. handleFieldWidthChanged() {
    6. console.log('change')
    7. }
    8. }
    9. export { TableDataContext }

    buildAttachEvent

    给 tableEvents 用的预定义的 attachEvent

    1. const attachTableEvent = buildAttachEvent('tableEvents')
    2. const buildAttachEvent = propName => {
    3. console.log('propName ===> ', propName)
    4. return eventName => {
    5. console.log('eventName ===> ', eventName)
    6. return (target, key, descriptor) => {
    7. console.log('target ===> ', target)
    8. console.log('key ===> ', key)
    9. console.log('descriptor ===> ', descriptor)
    10. const privatePropName = `__${propName}__`
    11. if (!Object.hasOwnProperty.call(target, privatePropName)) {
    12. target[privatePropName] = { ...target[privatePropName] }
    13. }
    14. target[privatePropName][eventName] = key
    15. return descriptor
    16. }
    17. }
    18. }

     buildEventsObject

    如何在访问 tableEvents 属性时拿到绑定在 __tableEvents__ 这个自定义属性上的方法呢,添加一个 buildEventsObject 装饰器,并对 getEvents 进行如下改造

    1. export const getEvents = (target, key) => {
    2. return {
    3. get () {
    4. console.log('访问了key ===> ' + key)
    5. const cacheKey = `__${key}_events__`
    6. return this[cacheKey] ?? (this[cacheKey] = buildEventsObject(this, key))
    7. },
    8. set () {}
    9. }
    10. }
    1. const buildEventsObject = (instance, propName) => {
    2. console.log('instance ===> ', instance)
    3. console.log('propName ===> ', propName)
    4. const privatePropName = `__${propName}__`
    5. const eventsMap = instance[privatePropName] ?? {}
    6. console.log('eventsMap ==> ', eventsMap)
    7. return Object.keys(eventsMap).reduce((acc, key) => {
    8. acc[key] = instance[eventsMap[key]]
    9. return acc
    10. }, {})
    11. }

    1. const table = new TableDataContext()
    2. console.log('table ===> ', table)
    3. console.log('tableEvents ===> ', table.tableEvents)

    这里可以看到对同一个方法绑定多个装饰器执行的先后顺序。

    1. 通过 getEvents 方法对基础类里面的属性 tableEvents 绑定 get 方法。
    2. 通过 buildAttachEvent('tableEvents') 在为属性 tableEvents 预定义执行方法。
    3. 访问属性 tableEvents 时会触发 get 方法,通过 buildEventsObject 获取绑定到属性          tableEvents 上的方法。
    4. 通过 bing 装饰器修正调用时的 this 的指向。

    接下来是对于基础列表的封装让我们定义的这些被装饰器修饰的方法可以在 .vue 文件里面被使用

    ListTable

    1. <script>
    2. import TableContextAdapter from './TableContextAdapter.vue'
    3. import { TableDataContext } from '@/components/table/tableDataContext'
    4. export default {
    5. components: {
    6. TableContextAdapter
    7. },
    8. data() {
    9. return {
    10. /**
    11. * 表格数据与操作上下文。
    12. */
    13. context: new TableDataContext()
    14. }
    15. }
    16. }
    17. script>

    TableContextAdapter

    1. <script>
    2. import BaseTable from './BaseTable.vue'
    3. import { TableDataContext } from '@/components/table/tableDataContext'
    4. export default {
    5. components: {
    6. BaseTable
    7. },
    8. props: {
    9. context: {
    10. type: TableDataContext
    11. }
    12. },
    13. /**
    14. * 将 context 上的事件与该组件上的事件合并后传入 BaseTable
    15. */
    16. computed: {
    17. combinedEvents() {
    18. // 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
    19. const eventsObjList = [this.context.tableEvents, this.context.events, this.$listeners]
    20. // [{ resize-change: ƒ, issue-change: ƒ }, { issue-modify: ƒ, issue-delete: ƒ }]
    21. const eventNameList = [...new Set(eventsObjList.map(o => Object.keys(o)).flat())]
    22. // ['resize-change', 'issue-change', 'issue-modify', 'issue-delete']
    23. return eventNameList.reduce(
    24. (map, name) =>
    25. Object.assign(map, {
    26. [name]: eventsObjList.map(eo => eo[name]).filter(f => f)
    27. }),
    28. {}
    29. )
    30. }
    31. }
    32. }
    33. script>
    34. <style>style>

    BaseTable

    1. <script>
    2. export default {
    3. created() {},
    4. methods: {
    5. handleResizableChange() {
    6. this.$emit('resize-change')
    7. },
    8. handleIssueChange() {
    9. this.$emit('issue-change')
    10. },
    11. handleIssueDelete() {
    12. this.$emit('issue-delete')
    13. },
    14. handleIssueModify() {
    15. this.$emit('issue-modify')
    16. }
    17. }
    18. }
    19. script>
    20. <style>style>

    如果多个方法 emit 需要触发同一个方法只需要进行如下改写即可

    1. import { DataContext } from './dataContext'
    2. import { attachTableEvent, getEvents, attachEvent } from './attachEvent'
    3. import { bind } from './bind'
    4. class TableDataContext extends DataContext {
    5. /**
    6. * 直接监听到 VapdTable 上的事件。
    7. */
    8. @getEvents
    9. tableEvents
    10. constructor() {
    11. super()
    12. }
    13. @bind
    14. @attachTableEvent('resize-change')
    15. handleFieldWidthChanged() {
    16. console.log('resize-change')
    17. }
    18. @bind
    19. @attachEvent('issue-delete')
    20. @attachEvent('issue-modify')
    21. @attachTableEvent('issue-change')
    22. handleIssueListChanged() {
    23. console.log('issue-change')
    24. }
    25. }
    26. export { TableDataContext }

  • 相关阅读:
    bp神经网络解决什么问题,bp神经网络的改进方法
    机械臂速成小指南(二十):机械臂的位姿重复性实验
    Linux--进程概念
    域内路由选择协议——RIP
    Linux音频-基本概念
    【mia】wa:webrtcagent组件
    项目管理必备的22个公式
    深入了解Linux中的scp命令及高级用法
    “互联网寒冬”来袭,软件测试人员该如何度过这次危机?
    文件共享方法
  • 原文地址:https://blog.csdn.net/AXBNMD/article/details/126131572