背景

项目需要表格支持拉伸每列的宽度,查看了文档,官方建议是用 vue-draggable-resizable 插件结合 components 属性,给表头 header 增加一个可拖拽的功能。其实下面的前三个问题都很好解决,网上也有很多解决方案,但因为考虑到项目中表格很多,而且每个表格都要支持拉伸列宽度,所以不可能按官方文档那样,每个表格的 vue 组件都写重复的代码,所以就封装了一个方法,专门获取可拖拽的 header 的components。

问题

1、使用 ResizeableTitle 可能会出现命名冲突
报错: Module parse failed: Argument name clash

解决方式:
把 ResizeableTitle 改成 resizeableTitle 即可,有的时候会出现这个问题,有的时候没有。

2、添加表格选择框后报错
其实主要是官方文档这处代码(如下图)的问题

那是因为在 antd 的文档中,colums 是没有选择框的,那函数在遍历执行每列时,遇到选择框那一列就找不到对应的 col,col 会为 undefined ,所以必须得对选择框做一个判断处理,可以直接返回 th,不用执行后面的操作。
解决:

3、表头的一些按钮样式消失,像下图中的滑过样式和点击样式消失,是因为新的th没有继承原来的th的类名。

如下图官方文档的代码,我们可以看到返回的 th ,要么是没有类名,要么是只有 resize-table-th ,这就很敷衍。

解决:
其实原有元素的类名,我们可以从函数返回的参数中获取

打印的形参如下图:

4、表头里的排序功能失效,无法点击进行排序功能

上诉背景已说过,我已经把这个获取拖拽的方法封装成一个全局的方法。而出现这个问题的直接原因也正因如此,我把 resizeableTitle 封装成一个全局的 getTableDragHeader 方法,并放在一个 js 文件里面,而不是像官方文档写在调用表格组件的 vue 页面里面。因为不封装的话,那所有需要拖拽的表格都要重复写这些代码,这就很不方便,所以我就把它写成了一个全局的方法,供所有带有表格的页面使用。

除此之外我还发现了一个问题,在我封装的全局拖拽方法里,如果我把 resizeableTitle 改成其它名字,那么调用的方式就会不一样,原本 resizeableTitle 命名的函数只传一个参数过来,如下图:

打印 a 形参:

只写一个 a 形参就能拿到 props、children、data。

但改成其它任意名字的函数名,就传三个参数过来,官方文档的写法就是传三个参数的。
打印如下图,第一个参数是个function,第二个参数是props,第三个参数是children:

这就很莫名其妙,只是函数名字不一样就有这么大的区别。
原本使用 resizeableTitle 封装成全局的方法去调用获取可拖拽headers 的colums 是不会报错,只是排序功能无法点击。但当我改 resizeableTitle 的名字为 resizeable 后,就是变成和官方文档有三个参数的情况时,控制台就报错:Error in render: “ReferenceError: h is not defined”,如下图:

百度了下这个错误,其他人遇到这个问题,它们的解释是表格 colums 没有放在 data 函数里面,获取不到上下文,这是什么鬼模糊的解释。。。
这和我现在的情况关联起来,的确也和上下文有关,毕竟我封装成全局的 js 方法,也就是方法不是特属某一个 vue 实例里的,而是全局的 js 方法。

所以我还是先把方法改成局部的,再对比看看到底所谓的上下文到底是什么?
按 antd 文档那种形式,我把方法写回在 vue 页面里面,getTableDragHeader 就是我封装成一个能根据 colums 返回拖拽header的方法,代码如下图:

然后去看,结果是不会出现排序功能失效的问题。那也就是说不是因为表头被 components 返回的 th 模板替换导致的事件失效,vue-draggable-resizable + components 的拖拽方法是没问题的。

所以问题可能出现在方法的调用和声明上了。

经过很多次的尝试,考虑过this的指向、引入的vue实例对象方式,但似乎都没关系,重点好像是方法必须在组件里声明,必须得要在调用的组件实例下。那这样还怎么封装成全局的方法供所有组件使用?不是只能在一个一个组件里面写了吗?

我回过头,重新点开报错地方,直接查看他的源码:

好像是 TableHeaderRow 是提供 render 给 vue 来渲染函数 return template 的方式,其中的 h 就是创建元素的方法。
综上所述,按我的理解,其实就是 return 返回的模板,如下图:

必须得要在 vue 实例里面写,而不是单纯的写在一个 js 文件,然后才在 vue 里面调用。百度时,网友说这种报错的解决方式是 colums 写在 data 里面,也无非是 它们的 colums 带有 customRender 这种需要渲染模板的选项而已。所以类似我封装的 getTableDragHeader 方法,这种带有 template 模板返回的方法,是不能在 vue 实例外声明的,必须得在某一个 vue 实例中声明。所以官方文档中在 vue 实例外声明,如下图,也是有问题的。

官方控制台不报错,是因为函数名是 ResizeableTitle ,改成其它函数名会立即报错,而且还是会出现上述的 h is not defined 报错,因为返回模板的方法没有写在 vue 实例里面。
解决:
既然知道必须得将方法写在任意 vue 实例里面,但是我又不想一个一个声明,所以我得要有一个全局的 vue 实例可供所有 vue 页面调用。这就想到了 $root 根组件 和 $Bus 。因为不想让 $root 变得复杂和冗乱,所以就创建了一个 $Bus 公共 vue 实例,用于提供给全部组件调用和通讯。
代码如下:

import Vue from 'vue';
import VueDraggableResizable from 'vue-draggable-resizable';
Vue.component('vue-draggable-resizable', VueDraggableResizable);
const Bus = new Vue({
    methods:{
        getTableDragHeader(columns) {
            const draggingMap = {};
            columns.forEach((col) => {
              draggingMap[col.key] = col.width;
            });
            const draggingState = Vue.observable(draggingMap);
            const resizeable  = (a,b,c) => {
              let thDom = null;
              const props = b;
              const children = c;
              const { key, ...restProps } = props;
              let col = null;
              if (key === "selection-column") {
                // 当前column为全选的时候,要返回全选的 th ,否则无法出现全选
                return <th {...restProps} class={props.class}>{children}</th>
              } else {
                col = columns.find((item) => {
                  const k = item.dataIndex || item.key;
                  return k === key;
                });
              }
          
              if (!col.width) {
                return <th {...restProps} class={props.class}>{children}</th>;
              }
              const onDrag = (x) => {
                draggingState[key] = 0;
                const maxWidth = thDom.parentNode.offsetWidth/2 || 100; // 拖拽最长长度不能超过表格的一半
                col.width = Math.min(Math.max(x, 50), maxWidth);
              };
          
              const onDragstop = () => {
                draggingState[key] = thDom.getBoundingClientRect().width;
              };
              return (
                <th {...restProps} v-ant-ref={(r) => (thDom = r)} width={col.width} class={props.class+" resize-table-th"}>
                  {children}
                  <vue-draggable-resizable
                    key={col.key}
                    class="table-draggable-handle"
                    w={10}
                    x={draggingState[key] || col.width}
                    z={1}
                    axis="x"
                    draggable={true}
                    resizable={false}
                    onDragging={onDrag}
                    onDragstop={onDragstop}
                  ></vue-draggable-resizable>
                </th>
              );
            };
            return {
              header: {
                cell: resizeable ,
              },
            }
          }
    }
})

export default Bus;

我将 getTableDragHeader 方法写在了 Bus 这个 vue 实例下面。
最后我在 main.js 中,引入上诉导出的 Bus 并挂载在 Vue 实例下成为 $Bus,其实 $Bus.getTableDragHeader 就可以调用获取可拖拽的 header colums,但因为之前封装的方法 $YGetTableDragHeader,代码很多地方已经用 $YGetTableDragHeader 的形式调用获取了,所以只能把 $Bus.getTableDragHeader 这个新方法再重新赋值给它,那其它地方的代码就不用改了。

终言

antd 表格的 components 文档可查资料很少,表格列之间的伸缩文档也只是给了一个小例子,而且这个例子的问题很多,在我看来本身就是有问题的。希望这篇文章可以提供给大家作一个参考并解决遇到的问题,若使用 vue-draggable-resizable 有其它问题,可给我留言,我看到也会帮忙解决的。

更多推荐

Ant Design of Vue 表格使用 vue-draggable-resizable 封装表头问题汇总