likes
comments
collection
share

如何给ant design贡献代码

作者站长头像
站长
· 阅读数 12

前言

各位小伙伴, 好久不见, 有段时间没写文章了, 去年12月羊了, 今年开年一直很忙, 03月份抽空在给ant designpr, 终于这个pr在昨天被merge了, 于是想写篇文章记录一下

记得我第一次用ant design是在2018年的时候, 当时给我的印象就很好: 界面简洁, 示例完善, api也很多, 而且解释得也很清楚, 使用起来也很顺手, 好用, 后来发现好多国外的开发者也在使用, 不禁感叹了一句卧槽, 咳...咳...当然, 更多的是对参与开发维护这个项目的大佬们的仰慕和钦佩

一晃5年过去了, 期间我也给另一个前端UI组件库提过pr, 不过那个就是更新了一下文档而已, 当时弄一个东西, 结果文档上没明确写到, 我花了3个多小时搞, 搞完了才发现如果文档上写了, 那就可以少花那3个多小时, 于是我就想我踩过的坑, 就不让其他人也踩了, 就更新了一下文档, 提了个pr

再后来, 去年12月的时候开发项目, 要弄一个拖拽排序的功能, 然后我想起了antd上有一个这样的demo, 就拿过来用了用, 还是熟悉的配方, 熟悉的味道, 一个字: 好用得一笔, 后来2023年来了, 其他项目又需要做拖拽排序, 我就又打开了antd, 说再看看那个demo, 参考借鉴一下, 结果发现那个demo不见了, emmmm, 居然不见了怎么, 后来反应过来: 2022年12月那个项目里有, 那我为何不不给antd提个pr呢, 我想这个需求很多人都会有吧, 于是我就把之前项目里的代码拿过来改了改, 打算给antd提一个pr

这不提不知道, 一提发现门道还不少, 于是就有了这篇文章

前期准备工作

这个私以为主要还是代码方面的, 无论是给开原仓库修改bug还是新增功能, 代码能力都是一个前提, 不过话又说回来, 能点开这篇文章, 有这个需求的小伙伴这个问题自然不会是一个问题啦

贡献指南

每个仓库都有自己的一套贡献规范指南, ant design的在这: 贡献指南, 这个是非常重要的, 直接决定了我们的代码能否通过review

具体的步骤里面都有提到, 跟着这些步骤一步一步来就行, 这里主要和大家聊聊相对不是太熟悉的两个点, 一个是上游仓库, 另一个是单元测试, 以及很可能会遇到的一个问题

安装Chromium的时候报错

这个问题我个人遇到了, 在我克隆完fork的仓库之后运行npm install之后报错了: Failed to set up Chromium xxx! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download, 而我找了很多方法均没解决这个问题, ERROR: Failed to set up Chromium r800071! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.我也试了, 但也不行, 最后是我换了网络, 删除了.npmrc里面的配置之后装成功的

上游仓库

关于上游仓库(或者说上游分支)的概念, 以及它的作用, 这里给大家贴一下github的官方文档: Configuring a remote repository for a fork, Syncing a fork

简单来说上游仓库就是我们需要fork的仓库, 这里就是ant designgithub仓库, 设置上游仓库或者说上游仓库的目的是为了同步代码, 因为我们的pr可能会经历比较长的时间, 这期间ant design仓库中可能会有新的提交, 那么这个时候我们就需要将这些提交同步过来, 以本文来举例, 具体步骤如下:

  1. fork ant design的仓库, 取消勾选Copy the master branch only, 这样才能把所有分支都fork过来, 因为根据贡献指南, 我这pr应该基于feature分支做修改而非master分支
  2. 切到feature分支, 新建一个draggable-tag分支, 然后在draggable-tag分支上修改
  3. 上游分支的设置以及代码同步可以直接在github中操作, 确切的说是同步操作可以直接在github中操作, 因为上游分支已经确定了, 在fork的时候就已经确定了, 同步的时候记得上游仓库的分支和fork过来的仓库的分支都要选成feature, 然后在github上点Sync fork按钮即可, 这个操作可查看Syncing a fork, 这样就可以将代码从ant design仓库的feature分支同步到我们fork过来的feature分支了
  4. 同步完毕之后需要将代码拉取到本地, 因为第3步只是把代码同步到了我们fork过来的仓库的远端, 还没有到我们本地, 于是需要再次切到feature分支, 然后拉取(这步可能有点慢, 需要多试几次), 注意这个时候拉取已经和上游分支没关系了, 因为上游分支的变更在第3步中已经同步到了我们fork过来的仓库了, 我们这个时候拉取是从fork过来的仓库拉取的
  5. 拉取完毕, 记得要将feature分支合并到draggable-tag, 这样才能保持最新
  6. 继续在draggable-tag分支上开发

另外代码同步的话, 理论上可以从ant designfeature分支直接同步到我们forkdraggable-tag分支, 但我没试过, 以及考虑到后续可能还会基于featurepr, 于是我就在feature分支上做同步, 然后再合到draggable-tag分支

单元测试

单元测试对于前端的同学来说可能不是太熟悉, 我本人也不是太熟悉, 在这回的pr中第一次写, 参考了其他已有的单元测试之后自己编写一个单元测试, 单元测试个人理解就是用代码去对一个功能/模块进行描述, 然后运行单元测试的时候检查那个功能/模块的执行结果是否和描述一致, 测试框架用的是jest, 以及react的测试库react testing library

以本文中的pr举例, 所有组件的单元测试文件均放在__test__目录下, 参考其他单元测试代码之后我新增了和我要加的demo同名的单元测试文件: draggable.test.tsx, 而在这之前, 我们先来看下demo的代码, 因为我的单元测试代码是依照demo代码, 也就是业务代码来写的:

/components/tag/demo/draggable.tsx:

import React, { useState } from 'react';
import { Tag } from 'antd';
import { DndContext, PointerSensor, useSensor, useSensors, closestCenter } from '@dnd-kit/core';
import {
  arrayMove,
  useSortable,
  SortableContext,
  horizontalListSortingStrategy,
} from '@dnd-kit/sortable';
import type { FC } from 'react';
import type { DragEndEvent } from '@dnd-kit/core/dist/types/index';

type Item = {
  id: number;
  text: string;
};

type DraggableTagProps = {
  tag: Item;
};

const DraggableTag: FC<DraggableTagProps> = (props) => {
  const { tag } = props;
  const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: tag.id });

  const commonStyle = {
    cursor: 'move',
    transition: 'unset', // 防止拖拽完毕之后元素抖动
  };

  const style = transform
    ? {
        ...commonStyle,
        transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
        transition: isDragging ? 'unset' : transition, // 处理拖拽中的元素不跟手的问题
      }
    : commonStyle;

  return (
    <Tag style={style} ref={setNodeRef} {...listeners}>
      {tag.text}
    </Tag>
  );
};

const App = () => {
  const [items, setItems] = useState<Item[]>([
    {
      id: 1,
      text: 'Tag 1',
    },
    {
      id: 2,
      text: 'Tag 2',
    },
    {
      id: 3,
      text: 'Tag 3',
    },
  ]);

  const sensors = useSensors(useSensor(PointerSensor));

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    if (!over) return;

    if (active.id !== over.id) {
      setItems((data) => {
        const oldIndex = data.findIndex((item) => item.id === active.id);
        const newIndex = data.findIndex((item) => item.id === over.id);

        return arrayMove(data, oldIndex, newIndex);
      });
    }
  };

  return (
    <DndContext sensors={sensors} onDragEnd={handleDragEnd} collisionDetection={closestCenter}>
      <SortableContext items={items} strategy={horizontalListSortingStrategy}>
        {items.map((item) => (
          <DraggableTag tag={item} key={item.id} />
        ))}
      </SortableContext>
    </DndContext>
  );
};

export default App;

然后是单元测试文件, /components/tag/__tests__/draggable.test.tsx:

import React from 'react';
import mountTest from '../../../tests/shared/mountTest';
import { render, fireEvent } from '../../../tests/utils';
import DraggableTag from '../demo/draggable';

(global as any).isVisible = true;

jest.mock('rc-util/lib/Dom/isVisible', () => {
  const mockFn = () => (global as any).isVisible;
  return mockFn;
});

describe('DraggableTag', () => {
  mountTest(DraggableTag);

  // tags渲染正确
  it('renders tags correctly', () => {
    const { getByText } = render(<DraggableTag />);

    expect(getByText('Tag 1')).toBeInTheDocument();
    expect(getByText('Tag 2')).toBeInTheDocument();
    expect(getByText('Tag 3')).toBeInTheDocument();
  });

  // 点击New Tag的时候渲染一个input
  it('render a input when the "New Tag" element is clicked', () => {
    const { container, getByText } = render(<DraggableTag />);

    const addTagButton = getByText('New Tag');

    fireEvent.click(addTagButton);

    expect(container.getElementsByTagName('input').length).toBe(1);
  });

  // input中敲击回车键的时候新增一个tag
  it('should add a new tag when the input press Enter is clicked', () => {
    const { container, getByText } = render(<DraggableTag />);

    const addTagButton = getByText('New Tag');

    fireEvent.click(addTagButton);

    const input = container.getElementsByTagName('input')[0];

    fireEvent.change(input, { target: { value: 'Tag xxx' } });
    fireEvent.keyDown(input, { key: 'Enter', code: 13, charCode: 13 });

    expect(input).not.toBeInTheDocument();
    expect(getByText('Tag xxx')).toBeInTheDocument();
  });

  // 敲回车键的时候保存编辑过的tag
  it('should save the edited tag when the "Enter" key is pressed', () => {
    const { container, getByText } = render(<DraggableTag />);
    const tag1 = getByText('Tag 1');

    fireEvent.doubleClick(tag1);

    const input = container.getElementsByTagName('input')[0];

    fireEvent.change(input, { target: { value: 'Tag xxx' } });
    fireEvent.keyDown(input, { key: 'Enter', code: 13, charCode: 13 });

    expect(input).not.toBeInTheDocument();
    expect(getByText('Tag xxx')).toBeInTheDocument();
  });

  // X按钮被点击的时候移除tag
  it('removes a tag when the "X" button is clicked', () => {
    const { getByText } = render(<DraggableTag />);
    const tag = getByText('Tag 1');
    const delTagBtn = tag.nextElementSibling as Element;

    fireEvent.click(delTagBtn);

    expect(tag).not.toBeInTheDocument();
  });

  // 双击tag的时候渲染一个input
  it('should render a input when the tag is double clicked', () => {
    const { container, getByText } = render(<DraggableTag />);
    const tag = getByText('Tag 1');

    fireEvent.doubleClick(tag);

    const inputs = container.getElementsByTagName('input');

    expect(inputs.length).toBe(1);
    expect(inputs[0]).toHaveAttribute('value', 'Tag 1');
  });
});

业务代码修改过, 一开始是包括Tagcrud操作的, 最后改成现在的样子, 而单元测试文件则是按照一开始包含crud逻辑的业务代码写的, 这里只是做一个展示, 展示一下单元测试文件和业务代码之间的关联, 本次pr最终的单元测试文件只保留了'renders tags correctly', 也就是这个:

import React from 'react';
import mountTest from '../../../tests/shared/mountTest';
import { render } from '../../../tests/utils';
import DraggableTag from '../demo/draggable';

(global as any).isVisible = true;

jest.mock('rc-util/lib/Dom/isVisible', () => {
  const mockFn = () => (global as any).isVisible;
  return mockFn;
});

describe('DraggableTag', () => {
  mountTest(DraggableTag);

  // tags渲染正确
  it('renders tags correctly', () => {
    const { getByText } = render(<DraggableTag />);

    expect(getByText('Tag 1')).toBeInTheDocument();
    expect(getByText('Tag 2')).toBeInTheDocument();
    expect(getByText('Tag 3')).toBeInTheDocument();
  });
});

单元测试写完之后就是运行, 贡献指南中提到一点: 确认所有的测试都是通过的 npm run test。 小贴士:开发过程中可以用 npm test -- --watch TestName 来运行指定的测试。, 在本次pr中运行指定的测试就是:

$ npm test --watch draggable

test后面的--我个人是忽略了的, 并没有使用--, 而是直接--watch TestName

这个draggable是我们单元测试文件的文件名前段部分: draggable.test.tsx中的draggable, 运行完毕会在控制台看到运行结果, 会告知我们一共有几个测试, 有几个通过, 几个失败, 同时会在__test__目录下的__snapshots__下生成快照文件: draggable.test.tsx.snap, 关于快照的解释, jest官网Snapshot Testing with Jest是这么说的:

A similar approach can be taken when it comes to testing your React components. Instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your React tree.

翻译过来:

当涉及到测试你的React组件时,可以采取类似的方法。你可以使用测试渲染器为你的React树快速生成一个可序列化的值,而不是渲染图形用户界面,这需要构建整个应用程序。

简单来说就是运行单元测试, 会生成一个关于那个单元测试的运行结果, 那个被测模块/功能的运行结果

以及这个生成快照我们忽略即可, 主要是看运行这个测试用例的过程中有误失败的情况, 有就需要修改, 直到全部成功为止

当然了, ant design应该可以在本地运行起来, 就是本地运行起ant design的网站的, 因为贡献指南中也提到了这一点, 以及装的时候还装了Chromium, 但我本人没尝试, 而是在我自己的一个草稿仓库中安装了antd然后直接写代码调试的, 调完之后再将代码复制到fork的仓库中, 然后再写单元测试

接下来就是确认所有的 UI 改动通过 npm run test-image,可以运行 npm run test-image -- -u 更新 UI 快照并且把这些更新也提交上来(如果有的话),UI 测试基于 Docker,根据平台下载对应的安装程序。, 这一步执行起来比较的费时间, 也占内存, 我这里是没有UI快照的

以及最后一步: 运行 npm test -- -u 来更新 jest snapshot 并且把这些更新也提交上来(如果有的话)。, 这一步也是非常耗时和占内存, 运行完之后会有100+的变更, 当然不用全部提交上去, 我们只需要找到/components/tag/__tests__/__snapshots__/demo-extend.test.ts.snap中和/components/tag/__tests__/__snapshots__/demo.test.ts.snap中关于draggable.tsx的变更就行了, 保留这两个文件中关于draggable.tsx的变更, 其他的放弃即可, 同时需要留意这两个文件中斜杠的变化, 我这可能是因为我用的win的电脑, 里面的/全部变成\了, 这里使用win的小伙伴要留意一下, 如果发生了这种情况, 在提交之前记得把斜杠修改回/

接着将代码push到远端, 然后去github中发起一个pr, 这里附上github的官方文档以供参考: Creating a pull request from a fork, 接着填写一下模板, 然后会有一个检查, 检查完毕, 等待仓库维护者的审核, 这个过程可能会比较漫长, 也可能会重复修改, 而在我们每一次做修改之前记得从上游分支中同步一下代码, 然后再修改, 仓库维护者审核完毕之后就会合到相应的分支上啦

最后

我入行也好多年了, ant design陪伴我走过了所有这些年, 不止使用过, 之前公司内部的组件库中组件的开发也参考过ant design, 真的是非常了不起的一个开源仓库, 这回能有机会给它提pr, 算是了却了我的一个心愿了, 前有羊, 后有虎(ai), 前端开发能做多久我也不知道, 但想到我的代码能留在这么一个非常了不起的开源仓库中, 其他开发者或者说新人能看到, 用到, 我就觉得满足了, 如果能让一些人觉得自己也想贡献一份力量, 就像我的前辈们激励过我那样, 那真的是没有什么遗憾了. 这里让我想到了李小龙先生所说的手指指月:

"It is like a finger pointing a way to the moon, don't concentrate on the finger or you will miss all that heavenly glory"

"就像用手指指向月亮, 不要将注意力集中在手指上, 不然你将错过所有的美景"

他指向的不是月亮, 而是浩瀚无垠的武学世界, 这些伟大的开源作品也像一根根手指一样, 指引着我们前往绚丽的, 广袤的代码的海洋, 后来者站在前者的肩膀上, 去向那更广阔的世界, 寻找更美的景色

好的, 这就是这篇文章的全部内容了, 欢迎大家在评论区和我一起交流探讨, 最后, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需, 想看更多知识干货欢迎关注哦~