前端框架arco table源码遇到的问题解析

目录
  • 前言
  • 离谱的filter代码
  • 疑问1:
  • 打不全补丁
  • 更大的问题
  • 错误的继续
  • 显而易见的问题
  • 结尾
    • 如何改进,有兴趣的同学可以去提pr

前言

先不说别的,上两个arco design table的bug。本来是写react table组件,然后看源码学习思路,结果看的我真的很想吐槽。(其他组件我在学习源码上受益匪浅,尤其是工程化arco-cli那部分,我自己尝试写的轮子也是受到很多启发,这个吐槽并不是真的有恶意,我对arco和腾讯的tdeisgn是有期待的,因为ant一家独大太久了,很期待新鲜的血液)

如果arco deisgn的团队看到这篇文章,请一定让写table的同学看一下!!!把多级表头的筛选 + 排序 + 固定逻辑好好梳理一下,目前的写法隐患太多了,我后面会写为什么目前的写法隐患很多,非常容易出bug!

1、这是在线bug demo codesandbox.io/s/jovial-ka…

bug显示

2、继续看,我筛选userInfo上,工资大于2000的行,根本没效果

在线bug 的demo codesandbox.io/s/competent…

说实话,我随便送给大家几个table的bug,都可以去给官方提pr了。(这个写table的人一定要好好的批评一下!!!!)

离谱的filter代码

filter是啥呢,我们看下图

这个表头的筛选我们简称为filter

首先官方把columns上所有的受控和非受控的filter收集起来,代码如下:

  const { currentFilters, currentSorter } = getDefaultFiltersAndSorter(columns);

columns我们假设长成这样:

const columns = [
  {
    title: "Name",
    dataIndex: "name",
    width: 140,
  },
  {
    title: "User Info",
    filters: [
      {
        text: "> 20000",
        value: "20000",
      },
      {
        text: "> 30000",
        value: "30000",
      },
    ],
    onFilter: (value, row) => row.salary > value,
  },
  {
    title: "Information",
    children: [
      {
        title: "Email",
        dataIndex: "email",
      },
      {
        title: "Phone",
        dataIndex: "phone",
      },
    ],
  },
]

getDefaultFiltersAndSorter的代码如下,不想看细节的,我就说下结论,这个函数是把filters受控属性,filteredValue和非受控属性defaultFilters放到currentFilters对象里,然后导出,其中key可以简单认为是每个columns上的dataIndex,也就是每一列的唯一标识符。

currentSorter我们暂时不看,也是为排序的bug埋下隐患,我们这篇文章先不谈排序的bug。

  function getDefaultFiltersAndSorter(columns) {
    const currentFilters = {} as Partial<Record<keyof T, string[]>>;
    const currentSorter = {} as SorterResult;
    function travel(columns) {
      if (columns && columns.length > 0) {
        columns.forEach((column, index) => {
          const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex;
          if (!column[childrenColumnName]) {
            if (column.defaultFilters) {
              currentFilters[innerDataIndex] = column.defaultFilters;
            }
            if (column.filteredValue) {
              currentFilters[innerDataIndex] = column.filteredValue;
            }
            if (column.defaultSortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.defaultSortOrder;
            }
            if (column.sortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.sortOrder;
            }
          } else {
            travel(column[childrenColumnName]);
          }
        });
      }
    }
    travel(columns);
    return { currentFilters, currentSorter };
  }

这里的已经为出bug埋下隐患了,大家看啊,它是递归收集所有columns上的filter相关的受控和非受控的属性,而且受控的属性会覆盖非受控。

这里没有单独区分受控的filter属性和非受控的属性就很奇怪。后面分析,因为arco deisgn有个专门处理受控和非受控的hooks,因为他现在不区分,还用错这个hooks,造成我看起来它的代码奇怪的要命!!

接着看!

然后,他用上面的currentFilters去

  const [filters, setFilters] = useState<FilterType<T>>(currentFilters);

接着看一下useColunms,这个跟filters后面息息相关,所以我们必须要看下useColumns的实现

  const [groupColumns, flattenColumns] = useColumns<T>(props);

简单描述一下useColumns的返回值 groupColumns, flattenColumns分别代表什么:

  • groupColumns,它将columns按行存储到数组里面,啥是按行呢,看下图

  • name、user info、Information、salary是第一行
  • Birthday、address是第二行,Email,phone也是第二行
  • city、road、no是第三行

flattenColumns是啥意思呢?就是columns叶子节点组成的数组,叶子节点是指所有columns中没有children属性的节点。以下是具体代码,有兴趣的可以看看,我们接着看,马上很奇怪的代码就要来了!

function useColumns<T>(props: TableProps<T>): [InternalColumnProps[][], InternalColumnProps[]] {
  const {
    components, // 覆盖原生表格标签
    rowSelection, // 设置表格行是否可选,选中事件等
    expandedRowRender, // 点击展开额外的行,渲染函数。返回值为 null 时,不会渲染展开按钮
    expandProps = {}, // 展开参数
    columns = [], // 外界传入的columns
    childrenColumnName, // 默认是children
  } = props;
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59dbcdab3b154494b751f61eeebe2432~tplv-k3u1fbpfcp-watermark.image?)
  // 下面有getFlattenColumns方法
  // getFlattenColumns平铺columns,因为可能有多级表头,所以需要平铺
  // getFlattenColumns,注意这个平铺只会搜集叶子节点!!!!
  const rows: InternalColumnProps[] = useMemo(
    () => getFlattenColumns(columns, childrenColumnName),
    [columns, childrenColumnName]
  );
  // 是否是checkbox
  const isCheckbox =
    (rowSelection && rowSelection.type === 'checkbox') ||
    (rowSelection && !('type' in rowSelection));
  // 是否是radio
  const isRadio = rowSelection && rowSelection.type === 'radio';
  // 展开按钮列的宽度
  const { width: expandColWidth } = expandProps;
  // 是否有expand—row
  const shouldRenderExpandCol = !!expandedRowRender;
  const shouldRenderSelectionCol = isCheckbox || isRadio;
  // 获取到自定义的操作栏,默认是selectNode和expandNode
  const { getHeaderComponentOperations, getBodyComponentOperations } = useComponent(components);
  const headerOperations = useMemo(
    () =>
      getHeaderComponentOperations({
        selectionNode: shouldRenderSelectionCol ? 'holder_node' : '',
        expandNode: shouldRenderExpandCol ? 'holder_node' : '',
      }),
    [shouldRenderSelectionCol, shouldRenderExpandCol, getHeaderComponentOperations]
  );
  const bodyOperations = useMemo(
    () =>
      getBodyComponentOperations({
        selectionNode: shouldRenderSelectionCol ? 'holder_node' : '',
        expandNode: shouldRenderExpandCol ? 'holder_node' : '',
      }),
    [shouldRenderSelectionCol, shouldRenderExpandCol, getBodyComponentOperations]
  );
  // rowSelection.fixed 表示checkbox是否固定在左边
  const selectionFixedLeft = rowSelection && rowSelection.fixed;
  // 选择列的宽度
  const selectionColumnWidth = rowSelection && rowSelection.columnWidth;
  const getInternalColumns = useCallback(
    (rows, operations, index?: number) => {
      const operationFixedProps: { fixed?: 'left' | 'right' } = {};
      const _rows: InternalColumnProps[] = [];
      rows.forEach((r, i) => {
        const _r = { ...r };
        if (!('key' in r)) {
          _r.key = _r.dataIndex || i;
        }
        if (i === 0) {
          _r.$$isFirstColumn = true;
          if (_r.fixed === 'left') {
            operationFixedProps.fixed = _r.fixed;
          }
        } else {
          _r.$$isFirstColumn = false;
        }
        _rows.push(_r);
      });
      const expandColumn = shouldRenderExpandCol && {
        key: INTERNAL_EXPAND_KEY,
        title: INTERNAL_EXPAND_KEY,
        width: expandColWidth,
        $$isOperation: true,
      };
      const selectionColumn = shouldRenderSelectionCol && {
        key: INTERNAL_SELECTION_KEY,
        title: INTERNAL_SELECTION_KEY,
        width: selectionColumnWidth,
        $$isOperation: true,
      };
      if (selectionFixedLeft) {
        operationFixedProps.fixed = 'left';
      }
      if (typeof index !== 'number' || index === 0) {
        [...operations].reverse().forEach((operation) => {
          if (operation.node) {
            if (operation.name === 'expandNode') {
              _rows.unshift({ ...expandColumn, ...operationFixedProps });
            } else if (operation.name === 'selectionNode') {
              _rows.unshift({ ...selectionColumn, ...operationFixedProps });
            } else {
              _rows.unshift({
                ...operation,
                ...operationFixedProps,
                title: operation.name,
                key: operation.name,
                $$isOperation: true,
                width: operation.width || 40,
              });
            }
          }
        });
      }
      return _rows;
    },
    [
      expandColWidth,
      shouldRenderExpandCol,
      shouldRenderSelectionCol,
      selectionColumnWidth,
      selectionFixedLeft,
    ]
  );
  const flattenColumns = useMemo(
    () => getInternalColumns(rows, bodyOperations),
    [rows, getInternalColumns, bodyOperations]
  );
  // 把表头分组的 columns 分成 n 行,并且加上 colSpan 和 rowSpan,没有表头分组的话是 1 行。
  // 获取column的深度
  const rowCount = useMemo(
    () => getAllHeaderRowsCount(columns, childrenColumnName),
    [columns, childrenColumnName]
  );
  // 分行之后的rows
  const groupColumns = useMemo(() => {
    if (rowCount === 1) {
      return [getInternalColumns(columns, headerOperations, 0)];
    }
    const rows: InternalColumnProps[][] = [];
    const travel = (columns, current = 0) => {
      rows[current] = rows[current] || [];
      columns.forEach((col) => {
        const column: InternalColumnProps = { ...col };
        if (column[childrenColumnName]) {
          // 求出叶子结点的个数就是colSpan
          column.colSpan = getFlattenColumns(col[childrenColumnName], childrenColumnName).length;
          column.rowSpan = 1;
          rows[current].push(column);
          travel(column[childrenColumnName], current + 1);
        } else {
          column.colSpan = 1;
          // 这是
          column.rowSpan = rowCount - current;
          rows[current].push(column);
        }
      });
      rows[current] = getInternalColumns(rows[current], headerOperations, current);
    };
    travel(columns);
    return rows;
  }, [columns, childrenColumnName, rowCount, getInternalColumns, headerOperations]);
  return [groupColumns, flattenColumns];
}
export default useColumns;
function getFlattenColumns(columns: InternalColumnProps[], childrenColumnName: string) {
  const rows: InternalColumnProps[] = [];
  function travel(columns) {
    if (columns && columns.length > 0) {
      columns.forEach((column) => {
        if (!column[childrenColumnName]) {
          rows.push({ ...column, key: column.key || column.dataIndex });
        } else {
          travel(column[childrenColumnName]);
        }
      });
    }
  }
  travel(columns);
  return rows;
}

接下来这个函数求的是受控的filters的集合!

疑问1:

为啥你受控的集合不在上面我们提到的getDefaultFiltersAndSorter里面就求出来,非要自己单独再求一遍?

  const controlledFilter = useMemo(() => {
    // 允许 filteredValue 设置为 undefined 表示不筛选
    const flattenFilteredValueColumns = flattenColumns.filter(
      (column) => 'filteredValue' in column
    );
    const newFilters = {};
    // 受控的筛选,当columns中的筛选发生改变时,更新state
    if (flattenFilteredValueColumns.length) {
      flattenFilteredValueColumns.forEach((column, index) => {
        const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex;
        if (innerDataIndex !== undefined) {
          newFilters[innerDataIndex] = column.filteredValue;
        }
      });
    }
    return newFilters;
  }, [flattenColumns]);

结果我们一看,flattenColumns里去拿受控的columns属性的值,而flattenColumns是拿的叶子节点,这么说来,这个controlledFilter还是跟之前的getDefaultFiltersAndSorter里的currentFilters是有区别的,一个是叶子节点,一个是全部的columns。

但是!问题来了,你只求叶子节点的受控属性,那非叶子节点的受控属性万一用户给你赋值了,岂不是没有作用了!!!

这就是我们最开始提到的第二个bug的根本原因,你自己最开始求得是所有columns中的filters的集合,现在用的是叶子节点的filters的属性,这不是牛头不对马嘴吗???

打不全补丁

接着看,上面的离谱逻辑导致后面的代码想去打补丁,结果就是打不全补丁!

  const innerFilters = useMemo&lt;FilterType&lt;T&gt;&gt;(() =&gt; {
    return Object.keys(controlledFilter).length ? controlledFilter : filters;
  }, [filters, controlledFilter]);

你看,他去得到一个innerFilters,咋求的呢?如果controlledFilter有值,也就是叶子节点有filter的受控属性,那么就用叶子节点的受控属性作为我们要使用的filters,但是!!!!

如果没有叶子节点的受控属性的filters,他居然用的是filters,filters是咋求出来的,不就是最上面的getDefaultFiltersAndSorter吗,这个函数求的是所有columns里filters的集合。

这个函数就非常非常离谱,为啥逻辑不对啊,一个针对叶子节点,一个针对全部节点!!!

更大的问题

// stateCurrentFilter 标记了下拉框中选中的 filter 项目,在受控模式下它与 currentFilter 可以不同
  const [currentFilter, setCurrentFilter, stateCurrentFilter] = useMergeValue<string[]>([], {
    value: currentFilters[innerDataIndex] || [],
  });

注意,这里有个useMergeValue的hooks,这个hooks 在arco deisgn中起着举足轻重的作用,我们必须好好说一下这个hooks,再看看写这个组件的同学为什么用错了!

我们简单解释一下这个hooks的目的,我们在用组件的时候一般会有两种模式,受控组件和非受控,这个hooks就是完美解决这个问题,你只要把value传入受控组件的属性,defaultValue传入非受控属性,这个hooks就自动接管了这两种状态的变化,非常棒的hooks,写的人真的很不错!

import React, { useState, useEffect, useRef } from 'react';
import { isUndefined } from '../is';
export default function useMergeValue<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T;
    value?: T;
  }
): [T, React.Dispatch<React.SetStateAction<T>>, T] {
  const { defaultValue, value } = props || {};
  const firstRenderRef = useRef(true);
  const [stateValue, setStateValue] = useState<T>(
    !isUndefined(value) ? value : !isUndefined(defaultValue) ? defaultValue : defaultStateValue
  );
  useEffect(() => {
    // 第一次渲染时候,props.value 已经在useState里赋值给stateValue了,不需要再次赋值。
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    // 外部value等于undefined,也就是一开始有值,后来变成了undefined(
    // 可能是移除了value属性,或者直接传入的undefined),那么就更新下内部的值。
    // 如果value有值,在下一步逻辑中直接返回了value,不需要同步到stateValue
    if (value === undefined) {
      setStateValue(value);
    }
  }, [value]);
  const mergedValue = isUndefined(value) ? stateValue : value;
  return [mergedValue, setStateValue, stateValue];
}

从这个hooks导出的[mergedValue, setStateValue, stateValue],我们简单分析下怎么用,mergedValue是以受控为准的,也就是外部发现如果用了受控属性,取这个值就行了,而且因为useEffect监听了value的变化,你就不用管受控属性的变化了,自动处理,多好啊!

然后setStateValue主要是手动去更新stateValue的,主要是在非受控的条件下去更新值,所以stateValue一般也是外部判断,如果是非受控条件,就取这个值!

我们接着看arco deisgn中这个人咋用的,完全浪费了这么一个好hook。

// stateCurrentFilter 标记了下拉框中选中的 filter 项目,在受控模式下它与 currentFilter 可以不同
  const [currentFilter, setCurrentFilter, stateCurrentFilter] = useMergeValue<string[]>([], {
    value: currentFilters[innerDataIndex] || [],
  });

value传了一个currentFilters[innerDataIndex] ,currentFilters是指所有columns里有可能是filters受控的属性集合,有可能是非受控filters属性的集合,innerDataIndex值的当前列的dataindex,也就是唯一标识符key。

那么问题来了value明明人家建议你传的是受控!受控!受控属性的值啊,因为你currentFilters目前既可能是受控,也可能是非受控,所以你传给value是没有办法的办法,因为你传给defaultValue也不对!

错误的继续

  useEffect(() => {
    setCurrentFilter(currentFilters[innerDataIndex] || []);
  }, [currentFilters, innerDataIndex]);
  useEffect(() => {
    if (currentFilter && currentFilter !== stateCurrentFilter) {
      setCurrentFilter(currentFilter);
    }
  }, [filterVisible]);

第一个useEffect是赋值了一个不知道是受控还是非受控的filters,然后第二个假设currentFilter存在,就是说如果受控的filters存在就赋值给优先级更高的受控属性!

显而易见的问题

上面造成两个useEffect的原因,不就是最开始在收集filters的时候,没有区分受控和非受控filters,然后后面代码再求一遍吗,而且求的逻辑让人不好看懂,对不起,我想说这代码写的,太容易出bug了,写的这个人真的是一己之力把table组件毁了!!!

然后我们看filters在确定筛选时的函数!

 /** ----------- Filters ----------- */
  function onHandleFilter(column, filter: string[]) {
    const newFilters = {
      ...innerFilters,
      [column.dataIndex]: filter,
    };
    const mergedFilters = {
      ...newFilters,
      ...controlledFilter,
    };
    if (isArray(filter) && filter.length) {
      setFilters(mergedFilters);
      const newProcessedData = getProcessedData(innerSorter, newFilters);
      const currentData = getPageData(newProcessedData);
      onChange &&
        onChange(getPaginationProps(newProcessedData), innerSorter, newFilters, {
          currentData,
          action: 'filter',
        });
    } else if (isArray(filter) && !filter.length) {
      onHandleFilterReset(column);
    }
  }

搞笑操作再次上演,innerFilters本来就是个奇葩,然后用

[column.dataIndex]: filter

去覆盖innerFilters里的dataIndex里的filter,这里的filter本来就是非受控的属性,你完全不区分受控非受控就上去一顿合并,万一是受控的属性呢??????

然后在mergedFilters里居然用controlledFilter再次去亡羊补牢,想用假如说有受控的filters,那么就优先用受控的值去覆盖innerFilters。

结尾

最开始不区分受控和非受控filters,后面全是一顿补丁!你开始区分不就代码逻辑就很清晰了吗,造成这么多次的遍历columns还有很多多余的更新react组件,让我忍不住想吐槽一下!!!

如何改进,有兴趣的同学可以去提pr

我简单写一下如何解决最开始写的第二个bug。

首先,getDefaultFiltersAndSorter要区分受控和非受控的情况,这是给后面的useMergeProps传递给受控和非受控属性做铺垫,题外话!大家写组件库的话可以copy一份useMergeProps这个hook,真的好东西!改进如下:

// currentFilteredValues代表非受控的filters的全部收集器
// currentDefaultFilters代表受控的filters的全部收集器
  const { currentFilteredValues, currentDefaultFilters, currentSorter } =
    getDefaultFiltersAndSorter(columns);
function getDefaultFiltersAndSorter(columns) {
    const currentFilteredValues = {} as Partial<Record<keyof T, string[]>>;
    const currentDefaultFilters = {} as Partial<Record<keyof T, string[]>>;
    const currentSorter = {} as SorterResult;
    function travel(columns) {
      if (columns && columns.length > 0) {
        columns.forEach((column, index) => {
          const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex;
          if (!column[childrenColumnName]) {
            // 筛选的非受控写法
            if (column.defaultFilters) {
              currentDefaultFilters[innerDataIndex] = column.defaultFilters;
            }
            // 筛选的受控属性,值为筛选的value数组 string[]
            if (column.filteredValue) {
              currentFilteredValues[innerDataIndex] = column.filteredValue;
            }
            // 默认排序方式	'ascend' | 'descend'
            if (column.defaultSortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.defaultSortOrder;
            }
            // 排序的受控属性,可以控制列的排序,可设置为 ascend descend
            if (column.sortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.sortOrder;
            }
          } else {
            // 筛选的非受控写法
            if (column.defaultFilters) {
              currentDefaultFilters[innerDataIndex] = column.defaultFilters;
            }
            // 筛选的受控属性,值为筛选的value数组 string[]
            if (column.filteredValue) {
              currentFilteredValues[innerDataIndex] = column.filteredValue;
            }
            // 默认排序方式	'ascend' | 'descend'
            if (column.defaultSortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.defaultSortOrder;
            }
            // 排序的受控属性,可以控制列的排序,可设置为 ascend descend
            if (column.sortOrder) {
              currentSorter.field = innerDataIndex;
              currentSorter.direction = column.sortOrder;
            }
            travel(column[childrenColumnName]);
          }
        });
      }
    }
    travel(columns);
    return { currentFilteredValues, currentDefaultFilters, currentSorter };
  }

然后初始化filters的时候,就要简单判断一下,我这里写的很烂,因为抽出一个函数的,主要是自己当初为了跑通代码,随便写了下,意思大家懂就行

  const [filters, setFilters] = useState<FilterType<T>>(
    Object.keys(currentDefaultFilters).length
      ? currentDefaultFilters
      : Object.keys(currentFilteredValues).length
      ? currentFilteredValues
      : undefined
  );

然后在 columns文件里,useMergeValue做受控属性和非受控属性的收口,因为之前我们区分了受控和非受控让后面的代码逻辑清晰很多。

const innerDataIndex = dataIndex === undefined ? index : dataIndex;
// stateCurrentFilter 标记了下拉框中选中的 filter 项目,在受控模式下它与 currentFilter 可以不同
  // currentFilter是受控value, setCurrentFilter主要是给非受控value的, stateCurrentFilter 内部value
  const [currentFilter, setCurrentFilter] = useMergeValue<string[]>([], {
    value: currentFilteredValues[innerDataIndex],
    defaultValue: currentDefaultFilters[innerDataIndex],
  });

然后点击filters的时候如何排序呢,这里filters就是受控和非受控的合并体,再用 [column.dataIndex]: filter更新当前最新的filter,后面更新数据就很自然了,getProcessedData是计算filters后的列,这个函数也需要改一下,把只计算叶子节点的改为计算所有的columns

  function onHandleFilter(column, filter: string[]) {
    const mergedFilters = {
      ...filters,
      [column.dataIndex]: filter, // 筛选项
    };
    if (isArray(filter) && filter.length) {
      setFilters(mergedFilters);
      const newProcessedData = getProcessedData(innerSorter, mergedFilters);
      const currentData = getPageData(newProcessedData);
      onChange &&
        onChange(getPaginationProps(newProcessedData), innerSorter, mergedFilters, {
          currentData: getOriginData(currentData),
          action: 'filter',
        });
    } else if (isArray(filter) && !filter.length) {
      onHandleFilterReset(column);
    }
  }

以上就是前端框架arco table源码遇到的问题解析的详细内容,更多关于前端框架arco table解析的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Javascript 基于长连接的服务框架问题

    目录 背景 Webscoket 封装 FakeHttpServer Context Middleware 请求处理 Quick Start 小结 背景 经常使用 Node 进行服务端开发的同学,想必都知道 Koa 框架.Koa 是一种 http 服务框架,其基于洋葱模型作为基本架构,能够让用户方便快捷地添加中间件,进行业务开发.而 websocket 是一种长连接的服务通信协议,需要自定义通讯 api 进行数据通讯.一般情况下,基于 websocket 的通讯 api 也是遵循一问一答的交互模式

  • JS前端可扩展的低代码UI框架Sunmao使用详解

    目录 引言 设计原则 1. 明确不同角色的职责 2. 发挥代码的威力,而不是限制 3. 各个层面的可扩展性 4. 专注而不是发散 Sunmao 的工作原理 响应最新的状态 组件间交互 布局与样式 类型安全 在组件间复用代码 可扩展的可视化编辑器 保持开放 引言 尽管现在越来越多的人开始对低代码开发感兴趣,但已有低代码方案的一些局限性仍然让大家有所保留.其中最常见的担忧莫过于低代码缺乏灵活性以及容易被厂商锁定. 显然这样的担忧是合理的,因为大家都不希望在实现特定功能的时候才发现低代码平台无法支持,

  • vue中生成条形码(jsbarcode)和二维码(qrcodejs2)的简单示例

    目录 1.条形码插件jsbarcode 2.二维码插件 总结 1.条形码插件jsbarcode 安装:npm install jsbarcode --save 引入:在需要生成条形码的页面引入即可 import JsBarcode from 'jsbarcode' 需要显示条形码的页面里 <img id="barcode1"> 调用构造函数生成条形码 let barCode1 = this.info.marIdCode; let barheight = this.imgHe

  • JavaScript与JQuery框架基础入门教程

    目录 一,JS对象 二,DOM –1,作用 –2,测试 三,Jquery –1,概述 –2,使用步骤 –3,入门案例 –4,jQuery的文档就绪 四,JQuery的语法 –1,选择器 –2,常用函数 –3,常用事件 –4,练习 总结 一,JS对象 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>测试 js的创建对象</title> <s

  • JavaScript框架设计模式详解

    目录 mvc mvp mvvm vue的来源 spa mpa createElement class 总结 mvc Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO.它也可以带有逻辑,在数据变化时更新控制器. View(视图) - 视图代表模型包含的数据的可视化. Controller(控制器) - 控制器作用于模型和视图上.它控制数据流向模型对象,并在数据变化时更新视图.它使视图与模型分离开. 是单向的 mvp mvp的核心在于presenter层,该层的核心是对于do

  • 前端框架arco table源码遇到的问题解析

    目录 前言 离谱的filter代码 疑问1: 打不全补丁 更大的问题 错误的继续 显而易见的问题 结尾 如何改进,有兴趣的同学可以去提pr 前言 先不说别的,上两个arco design table的bug.本来是写react table组件,然后看源码学习思路,结果看的我真的很想吐槽.(其他组件我在学习源码上受益匪浅,尤其是工程化arco-cli那部分,我自己尝试写的轮子也是受到很多启发,这个吐槽并不是真的有恶意,我对arco和腾讯的tdeisgn是有期待的,因为ant一家独大太久了,很期待新

  • layui前端框架之table表数据的刷新方法

    最简单的方法就是: //当前页的刷新 $(".layui-laypage-btn")[0].click(); 以上这篇layui前端框架之table表数据的刷新方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • Thinkphp 框架基础之源码获取、环境要求与目录结构分析

    本文实例讲述了Thinkphp 框架基础之源码获取.环境要求与目录结构.分享给大家供大家参考,具体如下: 获取ThinkPHP 获取ThinkPHP的方式很多,官方网站(http://thinkphp.cn)是最好的下载和文档获取来源. 官网提供了稳定版本的下载:http://thinkphp.cn/down/framework.html 如果你希望保持最新的更新,可以通过github获取当前最新的版本(完整版). Git获取地址列表(你可以选择一个最快的地址): Github: https:/

  • 解析Android框架之OkHttp3源码

    OkHttp流程图 OkHttp基本使用 gradle依赖 implementation 'com.squareup.okhttp3:okhttp:3.11.0' implementation 'com.squareup.okio:okio:1.15.0' /** *这里拿get请求来 * 异步的get请求 */ public void okhttpAsyn() { //设置超时的时间 OkHttpClient.Builder builder = new OkHttpClient.Builder

  • 解析Android框架之Volley源码

    Volley简单使用 我这里是以依赖架包的形式 ,大家也可以以gradle的形式进行依赖. 好了,接下来上代码了..... //获取volley的请求对象 RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext()); StringRequest stringRequest = new StringRequest(StringRequest.Method.GET, "http://www.baidu.com

  • 半小时实现Java手撸网络爬虫框架(附完整源码)

    最近在做一个搜索相关的项目,需要爬取网络上的一些链接存储到索引库中,虽然有很多开源的强大的爬虫框架,但本着学习的态度,自己写了一个简单的网络爬虫,以便了解其中的原理.今天,就为小伙伴们分享下这个简单的爬虫程序!! 首先介绍每个类的功能: DownloadPage.java的功能是下载此超链接的页面源代码. FunctionUtils.java 的功能是提供不同的静态方法,包括:页面链接正则表达式匹配,获取URL链接的元素,判断是否创建文件,获取页面的Url并将其转换为规范的Url,截取网页网页源

  • react源码合成事件深入解析

    目录 引言 导火线 事件委托 合成事件特点 React 事件系统 事件注册 enqueuePutListener() listenTo() trapCapturedEvent 与 trapBubbledEvent 事件存储 事件分发 事件执行 构造合成事件 批处理 引言 温馨提示: 下边是对React合成事件的源码阅读,全文有点长,但是!如果你真的想知道这不为人知的背后内幕,那一定要耐心看下去! 最近在做一个功能,然后不小心踩到了 React 合成事件 的坑,好奇心的驱使,去看了 React 官

  • Go Excelize API源码阅读SetSheetViewOptions示例解析

    目录 一.Go-Excelize简介 二. SetSheetViewOptions 一.Go-Excelize简介 Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准. 可以使用它来读取.写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档. 支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式.图片(表).透视表.切片器

  • .NET Core 源码编译的问题解析

    引言: .NET Core 源码编译 https://github.com/dotnet git clone https://github.com/dotnet/runtime.git 一:Windows 编译 VS 2019 16.6(不要安装预览版) Win 10 专业版,最新版本 (1903/2004) 长路径支持:组策略(gpedit.msc) > 计算机配置 > 管理模板 > 系统 > 文件系统 > 启用 Win32 长路径 Git长路径:git config --

  • 微前端qiankun沙箱实现源码解读

    目录 前言 LegacySandbox单实例沙箱 ProxySandbox多实例沙箱 SapshotSandbox 快照沙箱 结束语 前言 上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下JS沙箱这篇内容,扫盲一下.接下来我们通过源 码详细分析下qiankun沙箱实现,我们clone下qiankun代码,代码主要在sandbox文件夹下,目录结构为 ├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙

随机推荐