Como entrevistador, ¿por qué recomiendo la biblioteca de componentes como lo más destacado de la entrevista inicial?

Folleto de algoritmos gráficos

Compilé recientemente un folleto de algoritmos, los estudiantes interesados ​​​​pueden agregarme a WeChat linwu-hipara obtenerlo

imagen.png

prefacio

En el artículo anterior , como entrevistador, ¿por qué recomiendo micro-frontends como lo más destacado de las entrevistas de front-end? Los comentarios son buenos, publicaré el segundo artículo 组件库专题, 主要是我选择的方向,前端同学都可以很轻易尝试por lo que se agregarán muchos aspectos destacados al proyecto.

Durante la entrevista en Dachang, también vi que muchos candidatos escribieron xx组件的封装, pero rara vez se ven 二次组件库的封装o 维护开源组件库, de hecho, estos son los aspectos más destacados del proyecto. Generalmente, si el entrevistador lo ve, lo investigará en detalle.

Este artículo tomará antd Element vant等等la biblioteca de componentes como ejemplo para analizar y comparar

¿Por qué necesita una biblioteca de componentes de paquetes secundarios?

En el trabajo real, necesitamos personalizar 主题色, 更改按钮样式, 自定义图标, personalizar, table组件etc. en el proyecto Estos pueden volver a encapsularse en función de la biblioteca de componentes antd para reducir la duplicación de trabajo y mejorar la eficiencia del desarrollo.

Entonces, solo debemos pensar de acuerdo con los siguientes cuatro principios al encapsular. Además, encapsular la biblioteca de componentes en sí no tiene ningún riesgo para el proyecto, porque al principio llevaremos a cabo directamente la función de agregar negocios internamente, PropsTypepara 转发lograr completamente解耦

  • Estilo unificado : en un proyecto grande o en varios proyectos relacionados, es muy importante mantener un estilo de interfaz y un método de interacción coherentes. A través de la encapsulación secundaria, podemos definir estilos y comportamientos uniformes y reducir las inconsistencias.

  • Reduzca los costos de mantenimiento : cuando se actualiza la biblioteca de componentes subyacente, es posible que debamos realizar cambios en varios lugares del proyecto. Pero si tenemos nuestro propio paquete, solo necesitamos actualizarlo a nivel de paquete, lo que reduce mucho los costos de mantenimiento.

  • Agregar funciones personalizadas : a veces, necesitamos agregar algunas funciones específicas sobre la base de la biblioteca de componentes original, como verificación específica, manejo de errores, etc. El embalaje secundario ofrece esa posibilidad.

  • Mejore la eficiencia del desarrollo : para algunas funciones de uso común (como verificación de formularios, avisos globales, etc.), el empaquetado secundario puede proporcionar API más convenientes y mejorar la eficiencia del desarrollo.

Combine el proceso de diseño de una biblioteca de componentes para hablar sobre la idea de la ingeniería de front-end

Cuando hablamos de la idea de ingeniería front-end en combinación con un proceso de diseño de biblioteca de componentes, debemos aclarar estos puntos:

1. Use Lerna para la administración de paquetes múltiples: use Lerna para administrar paquetes múltiples (componentes) y realice funciones como desacoplamiento a nivel de componente, control de versión independiente y carga a pedido.

# 安装 Lerna
npm install -g lerna

# 初始化一个 Lerna 仓库
lerna init

# 创建 "Button" 组件包
lerna create button --yes

2. Envío estandarizado: el uso de información de envío estandarizado puede mejorar la legibilidad de los registros de Git y puede generar automáticamente CHANGELOG a través de confirmaciones convencionales. Puede usar commitizen, commitlint y otras herramientas para configurar.

# 安装相关工具
npm install commitizen cz-conventional-changelog --save-dev
// package.json
{
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
}

3. Estandarización del código: implemente la estandarización y el formato del código a través de ESLint, Prettier y otras herramientas, y empaquételos como sus propios ajustes preestablecidos estándar.

# 安装相关工具
npm install eslint prettier eslint-plugin-prettier eslint-config-prettier --save-dev
// .eslintrc.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:prettier/recommended'],
};

// .prettierrc.js
module.exports = {
  singleQuote: true,
  trailingComma: 'es5',
};

4. Desarrollo y depuración de componentes: deben tenerse en cuenta cuestiones como la compilación de actualizaciones activas y la referencia de enlaces suaves para facilitar la depuración de componentes durante el proceso de desarrollo.

// packages/button/src/Button.js
import React from 'react';

const Button = ({ type = 'primary', onClick, children }) => {
  return (
    <button className={`button ${type}`} onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

5. Sitio de documentación: puede crear un sitio de documentación basado en dumi e implementar optimizaciones como la aceleración de CDN y la publicación incremental. Las vistas previas de relaciones públicas se pueden implementar utilizando Surge.

<!-- packages/button/docs/index.md -->
# Button

A simple button component.

## Usage

import { Button } from 'button-library';

const MyComponent = () => {
  return <Button onClick={() => alert('Button clicked!')}>Click Me</Button>;
};

### Props

| Name     | Type                   | Default | Description                   |
| -------- | ---------------------- | ------- | ----------------------------- |
| type     | `primary` \| `secondary` | `primary` | The type of the button. |
| onClick  | `function`             |         | Event handler for click event. |

6. Pruebas unitarias: Es necesario considerar el uso de herramientas como jest y enzima para generar informes de cobertura de pruebas.

# 安装相关工具
npm install jest enzyme enzyme-adapter-react-16 react-test-renderer --save-dev
// packages/button/src/Button.test.js
import React from 'react';
import { mount } from 'enzyme';
import Button from './Button';

describe('Button', () => {
  it('renders without crashing', () => {
    const wrapper = mount(<Button>Click Me</Button>);
    expect(wrapper.exists()).toBe(true);
  });

  it('calls onClick function when clicked', () => {
    const onClickMock = jest.fn();
    const wrapper = mount(<Button onClick={onClickMock}>Click Me</Button>);

    wrapper.find('button').simulate('click');
    expect(onClickMock).toHaveBeenCalledTimes(1);
  });
});

7. Carga bajo demanda: debe cooperar con babel-plugin-import para realizar la carga bajo demanda, es decir, modificar la ruta de importación en tiempo de compilación para realizar la carga de componentes bajo demanda.

# 安装相关工具
npm install babel-plugin-import --save-dev
// .babelrc
{
  "plugins": [
    [
      "import",
      {
        "libraryName": "button-library",
        "style": "css"
      }
    ]
  ]
}

8. Diseño de componentes: deben tenerse en cuenta cuestiones como la capacidad de respuesta, los temas, la internacionalización y la compatibilidad con TypeScript para garantizar la flexibilidad y la escalabilidad de los componentes.

// packages/button/src/Button.js
import React from 'react';
import PropTypes from 'prop-types';

const Button = ({ type = 'primary', onClick, children }) => {
  return (
    <button className={`button ${type}`} onClick={onClick}>
      {children}
    </button>
  );
};

Button.propTypes = {
  type: PropTypes.oneOf(['primary', 'secondary']),
  onClick: PropTypes.func,
  children: PropTypes.node.isRequired,
};

export default Button;

9. Scripts automatizados antes del lanzamiento: es necesario escribir scripts automatizados para estandarizar el proceso de lanzamiento y garantizar la consistencia y confiabilidad de los lanzamientos.

// package.json
{
  "scripts": {
    "prepublish": "npm run lint && npm run test",
    "lint": "eslint .",
    "test": "jest"
  }
}

10. Procesamiento posterior al lanzamiento: considere problemas como actualizaciones de parches, lanzamiento simultáneo de sitios de documentación, etc., para solucionar los problemas a tiempo y proporcionar la documentación más reciente.

11. 制定 Contributing 文档:制定 Contributing 文档可以降低开源社区贡献的门槛,并确保社区成员了解如何参与项目。处理 issues 和 PR 需要有专人负责。

如何对一个组件库进行测试?

首先需要明确,组件库的测试大致可以分为两类:一类是针对组件本身的功能和性能的测试(例如,单元测试、性能测试),另一类是针对组件在集成环境下的行为和性能的测试(例如,集成测试、系统测试)。

1. 功能测试(单元测试)

通常来说,组件的功能测试可以通过单元测试来完成。单元测试的目的是验证组件的单个功能是否按照预期工作。这通常可以通过编写测试用例来完成,每个测试用例针对一个特定的功能。

import { Button } from '../src/Button';

test('Button should do something', () => {
    const component = new YourComponent();
    // your test logic here
    expect(component.doSomething()).toBe('expected result');
});

2. 边界测试

边界测试是一种特殊的功能测试,用于检查组件在输入或输出达到极限或边界条件时的行为。

test('Button should handle boundary condition', () => {
    const component = new YourComponent();
    // test with boundary value
    expect(component.handleBoundaryCondition('boundary value')).toBe('expected result');
});

3. 响应测试

响应测试通常涉及到 UI 组件在不同的设备或屏幕尺寸下的行为。这可能需要使用端到端(E2E)测试工具,如 Puppeteer、Cypress 等。

import { test } from '@playwright/test';

test('Button should be responsive', async ({ page }) => {
    await page.goto('http://localhost:3000/your-component');
    const component = await page.$('#your-component-id');
    expect(await component.isVisible()).toBe(true);

    // Simulate a mobile device
    await page.setViewportSize({ width: 375, height: 812 });
    // Check the component under this condition
    // your test logic here
});

4. 交互测试

交互测试也可以通过端到端(E2E)测试工具来完成。

test('Button should handle interactions', async ({ page }) => {
    await page.goto('http://localhost:3000/your-component');
    const component = await page.$('#your-component-id');

    // Simulate a click event
    await component.click();
    // Check the result of the interaction
    // your test logic here
});

5. 异常测试

异常测试用于验证组件在遇到错误或非法输入时能否正确处理。这通常可以通过在测试用例中模拟错误条件来完成。

test('Button should handle errors', () => {
    const component = new YourComponent();
    // Test with illegal argument
    expect(() => {
        component.doSomething('illegal argument');
    }).toThrow('Expected error message');
});

6. 性能测试

性能测试用于验证组件的性能,例如,加载速度、内存消耗等。

import { performance } from 'perf_hooks';

test('Button should have good performance', () => {
    const start = performance.now();
    const component = new YourComponent();
    component.doSomething();
    const end = performance.now();
    const duration = end - start;
    expect(duration).toBeLessThan(50);  // Expect the operation to finish within 50 ms
});

7. 自动化测试

单元测试、集成测试和系统测试都可以通过自动化测试工具进行。例如,Jest 和 Mocha 可以用于自动化运行 JavaScript 单元测试,Puppeteer 和 Selenium 可以用于自动化运行端到端测试。

module.exports = {
    roots: ['<rootDir>/src'],
    testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
    transform: {
        '^.+\\.(ts|tsx)$': 'ts-jest'
    }
};

Element-UI 的多语言方案是怎么设计的?

Element UI 使用了 Vue 的插件 vue-i18n 实现多语言支持,具体的设计和实现过程如下:

1. 定义语言包

首先,Element UI 定义了一个 JavaScript 对象作为语言包。每种语言都有一个对应的语言包,例如:

export default {
  el: {
    colorpicker: {
      confirm: 'OK',
      clear: 'Clear'
    },
    // ...other components
  }
};

2. 加载语言包

Element UI 提供了一个 i18n 方法用于加载语言包。

import ElementUI from 'element-ui';
import locale from 'element-ui/lib/locale/lang/en';

Vue.use(ElementUI, { locale });

3. 使用语言包

Element UI 的组件会使用 $t 方法获取语言包中的文本。例如:

<template>
  <el-button>{{ $t('el.button.confirm') }}</el-button>
</template>

在这个例子中,按钮的文本会根据当前的语言包来显示。

4. 集成 vue-i18n

如果你的项目中已经使用了 vue-i18n,Element UI 会优先使用 vue-i18n 提供的 $t 方法。你可以这样配置:

import Vue from 'vue';
import VueI18n from 'vue-i18n';
import ElementUI from 'element-ui';
import enLocale from 'element-ui/lib/locale/lang/en';
import zhLocale from 'element-ui/lib/locale/lang/zh-CN';

Vue.use(VueI18n);

const messages = {
  en: {
    message: 'hello',
    ...enLocale // 或者用 Object.assign({ message: 'hello' }, enLocale)
  },
  zh: {
    message: '你好',
    ...zhLocale // 或者用 Object.assign({ message: '你好' }, zhLocale)
  }
};

const i18n = new VueI18n({
  locale: 'zh', // set locale
  messages, // set locale messages
});

Vue.use(ElementUI, {
  i18n: (key, value) => i18n.t(key, value)
});

在这个例子中,我们先加载了 vue-i18n,然后定义了两种语言的语言包(英文和中文)。最后,我们配置了 Element UI 使用 vue-i18n$t 方法。

这样,Element UI 的组件就能够根据 vue-i18n 的语言设置显示对应的文本。

组件库如何实现在线主题定制的?

1. 使用 CSS 变量定义样式

将组件的样式使用 CSS 变量定义,这样可以通过改变 CSS 变量的值来修改样式。

:root {
  --primary-color: #1890ff;
}

.btn {
  background: var(--primary-color); 
}

2. 提供主题文件进行配置

让用户可以通过导入自定义的主题文件来覆盖默认样式。

// theme.js
export default {
  '--primary-color': '#409eff'
}

3. 在线主题编辑器

提供一个在线工具,用户可以在工具中配置主题,生成主题文件。

工具会提交主题配置,服务器端接收后动态编译生成新的样式,并返回给前端。

4. 前端应用新样式

前端通过加载服务器返回的 CSS 文件来应用新的主题样式,实现样式更新而无需重新打包。

// 请求主题文件
fetchTheme(theme).then(css => {
  // 动态创建style标签,插入css
  const style = document.createElement('style');
  style.innerHTML = css;
  document.head.appendChild(style);  
})

5. 持久化主题配置

将用户主题配置持久化本地存储,这样每次访问都可以应用上次选定的主题。

组件库的类型定义应该怎样设计?

组件库的类型定义设计取决于很多因素,包括库的大小、复杂度、可能的使用场景等。

1. 定义全局类型 versus 定义组件Props类型

在组件库中,我们经常需要定义一些可以在多个组件之间共享的全局类型,以及针对特定组件的props类型。例如:

// 全局类型
export interface Size {
  width: number;
  height: number;
}

// 组件Props类型
export interface ButtonProps {
  size?: Size;
  label: string;
  onClick?: () => void;
}

2. 类型导出应该集中还是分散?

是否集中导出类型取决于组件库的大小和复杂度。对于小型库,可以在一个单独的文件中集中导出所有类型;对于大型库,可能需要将类型定义分散在各个组件文件中,然后在一个单独的文件中重新导出它们。例如:

// 在各个组件文件中定义和导出类型
// button.ts
export interface ButtonProps { /*...*/ }

// 在一个单独的文件中重新导出所有类型
// types.ts
export type { ButtonProps } from './button';

3. 如何设计类型层级关系?类型复用?

在设计类型时,应尽可能地利用 TypeScript 的类型系统来构建类型层级关系,并复用类型。例如,你可以使用类型交叉(&)和类型联合(|)来复用类型:

type SmallSize = { width: number; height: number };
type LargeSize = SmallSize & { depth: number };

type Size = SmallSize | LargeSize;

4. 类型定义要充分还是精简?

类型定义应尽可能精简,同时提供足够的信息来描述类型的形状和行为。避免使用 anyunknown 类型,除非有特别的理由。例如:

// 不好的类型定义
interface ButtonProps {
  [key: string]: any;  // 这不提供任何有关props的信息
}

// 好的类型定义
interface ButtonProps {
  size?: Size;
  label: string;
  onClick?: () => void;
}

总的来说,设计好的类型定义可以提高代码的可读性和可维护性,同时减少运行时错误。

组件库的渐进升级策略应该怎么设计?

组件库的渐进升级策略通常会涉及到版本控制、向下兼容性、废弃通知以及旧版本的兼容性等多个方面。这种策略的主要目的是在保持库的稳定性和功能性的同时,尽可能地减少对用户的影响。

1. 版本控制策略

组件库通常遵循语义化版本 (SemVer) 规范进行版本控制。在语义化版本中,每个版本号都由三部分组成:主版本号、次版本号和补丁版本号。

例如,版本号为 1.2.3 表示主版本号为 1,次版本号为 2,补丁版本号为 3。

  • 主版本号(Major): 当你做了不兼容的 API 修改
  • 次版本号(Minor): 当你做了向下兼容的功能性新增
  • 补丁版本号(Patch): 当你做了向下兼容的问题修复

2. 向下兼容处理

向下兼容性是指在升级组件库时,保证新版本不会破坏旧版本的功能。例如,如果新版本的一个组件删除了一个属性,而这个属性在旧版本中是必需的,那么这个变化就不是向下兼容的。

在进行不向下兼容的变化时,应在主版本号上进行增加,以警告用户可能需要修改他们的代码。

3. 功能被废弃怎么通知用户升级?

当一个功能或者组件被废弃时,应在库的文档、更新日志以及相关的 API 文档中明确注明。在代码中,可以通过添加警告或者错误信息来提醒用户:

function deprecatedFunction() {
  console.warn('Warning: deprecatedFunction is deprecated and will be removed in the next major version.');
  // 功能的原始实现
}

4. 兼容旧版本的方案

兼容旧版本的策略取决于特定的需求和资源。一种常见的策略是在主版本升级后,继续维护旧版本的一个分支,以便在必要时进行修复和改进。例如,如果当前版本是 2.x.x,那么可以维护一个 1.x.x 的分支。

在实践中,以上的策略和方法可能需要根据具体的情况进行调整。一个好的渐进升级策略应能够平衡新功能的引入、旧功能的废弃以及向下兼容性的维护。

组件库的按需加载实现中存在哪些潜在问题,如何解决?

按需加载(也称为代码拆分)是现代前端开发中常见的一种优化手段,可以有效地减少应用的初始加载时间。对于组件库来说,它使用户只加载和使用他们真正需要的组件,而不是加载整个库。

babel-plugin-import

Babel 插件: 使用如 babel-plugin-import 的 Babel 插件可以在编译时将导入整个库的语句转换为仅导入使用的组件。

```javascript
import { Button } from 'your-ui-lib';
// 在编译时,babel-plugin-import 将上面的语句转换为以下语句:
// import Button from 'your-ui-lib/button';
```

tree-shaking

Webpack、Rollup 等工具都已经支持了 Tree shaking。在项目的配置中开启 Tree shaking,然后使用 ES Modules 的导入导出语法,即可实现按需加载。

但是在使用 Tree shaking 的时候,有一个需要特别注意的地方,就是“副作用(side effects)”。

有些模块的代码可能会在导入时执行一些副作用,例如改变全局变量、改变导入模块的状态等。这种情况下,即使模块中的部分导出没有被使用,由于其副作用,也不能被 Tree shaking 移除。否则,可能会导致程序运行出错。

例如,在 CSS in JS 的库中,可能存在这样的代码:

import './styles.css'; // 有副作用,改变了全局的样式

在这种情况下,你需要在 package.json 中显式地指定模块的副作用,以防止它们被错误地移除:

{
  "name": "your-library",
  "sideEffects": [
    "./src/styles.css"
  ]
}

如果你的库没有任何副作用,你可以将 sideEffects 设置为 false

{
  "name": "your-library",
  "sideEffects": false
}

样式如何实现真正的按需加载?避免样式重复打包?

imagen.png

样式和逻辑分离 样式和逻辑结合 样式和逻辑关联
开发打包流程 中等 简单 复杂
输出文件 JS 文件和 CSS 文件 JS 文件 JS 文件和 CSS 文件
使用方法 分别引入 JS 和 CSS 只引入 JS 只引入 JS
按需加载 需要额外支持 支持 支持
性能影响 带额外 runtime,可能有影响
SSR 支持 需要额外支持(部分方案不支持) 支持(可能需要使用者调整配置)
支持写法 常规 CSS / 零运行时 CSS in JS 常规 CSS / CSS in JS 常规 CSS / 零运行时 CSS in JS
关键样式提取 自行处理 支持 自行处理

样式和逻辑分离

这种方案中,组件的CSS和JS在代码层面上是分离的,开发时写在不同的文件里。在打包时生成独立的逻辑文件和样式文件。

优点:

  • 适用面广,可以支持不同的框架和技术栈。
  • 支持SSR,样式处理留给使用者。
  • 可以直接提供源码,便于主题定制。

缺点:

适合需要高适用性和灵活性的组件库。

样式和逻辑结合

这种方案将CSS和JS打包在一起,输出单一的JS文件。主要有两种实现形式:

  1. CSS in JS:样式以对象或字符串形式存在在JS中。
  2. 将CSS打包进JS:通过构建工具,将CSS文件内容注入到JS中。

优点:

  • 使用简单,只需要引入JS即可。
  • 天然支持按需加载。

缺点:

  • 需要额外的runtime,可能影响性能。
  • 难以利用浏览器缓存。
  • SSR需要框架额外支持。

样式和逻辑关联

这种方案下,虽然CSS和JS在源码层分离,但组件内会直接引用样式,且输出文件中保留import语句。

优点:

  • 使用简单,只引入JS即可。
  • 支持按需加载。

缺点:

  • 对构建和SSR都有一定要求。
  • 样式编译复杂。

设计一个组件库的 CI/CD 和发布流程。

可以参考antd

当你设计一个组件库的 CI/CD 和发布流程时,可以考虑以下步骤:

1. 分支管理:

开发者在开发新特性或修复 bug 时,应该在新的分支(通常称为 feature 分支)上进行开发。完成开发后,提交一个 pull request 到 mainmaster 分支,并进行代码审查。

git checkout -b feature/new-component
# 开发过程...
git add .
git commit -m "Add new component"
git push origin feature/new-component

2. 代码检查:

使用如 ESLint、Stylelint 等工具进行代码检查,使用 Jest 等工具进行单元测试和覆盖率检查。这些步骤可以在提交代码时或者 pull request 的过程中自动进行。

例如,可以在 package.json 中添加如下 scripts:

{
  "scripts": {
    "lint": "eslint --ext .js,.jsx,.ts,.tsx src",
    "test": "jest"
  }
}

并在 CI/CD 工具中(如 GitHub Actions、Jenkins 等)配置相应的任务:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14'
      - name: Install dependencies
        run: npm ci
      - name: Run lint
        run: npm run lint
      - name: Run tests
        run: npm run test

3. 版本管理:

在合并代码并发布新版本前,需要确认新的版本号,并生成相应的 changelog。可以使用如 standard-version 这样的工具自动化这个过程。

npx standard-version

4. 构建:

使用如 Webpack、Rollup 等工具进行构建,生成可以在不同环境(如浏览器、Node.js)下使用的代码。

npm run build

5. 发布:

将构建好的代码发布到 npm,同时更新文档网站。

npm publish

6. 部署:

部署到github pages或者自建服务

如何实现button按钮

import React, { CSSProperties, FC, MouseEvent, ReactNode } from 'react';

interface ButtonProps {
  lock?: boolean;
  classNames?: Record<string, string>;
  danger?: boolean;
  disabled?: boolean;
  ghost?: boolean;
  href?: string;
  htmlType?: 'button' | 'submit' | 'reset';
  icon?: ReactNode;
  loading?: boolean | { delay: number };
  shape?: 'default' | 'circle' | 'round';
  size?: 'large' | 'middle' | 'small';
  styles?: Record<string, CSSProperties>;
  target?: string;
  type?: 'primary' | 'dashed' | 'link' | 'text' | 'default';
  onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  children?: ReactNode;
}

const Button: FC<ButtonProps> = ({
  lock,
  classNames,
  danger,
  disabled,
  ghost,
  href,
  htmlType = 'button',
  icon,
  loading,
  shape,
  size,
  styles,
  target,
  type = 'default',
  onClick,
  children
}) => {
  const baseClassName = 'button';

  const className = [
    baseClassName,
    type && `${baseClassName}--${type}`,
    size && `${baseClassName}--${size}`,
    shape && `${baseClassName}--${shape}`,
    disabled && `${baseClassName}--disabled`,
    danger && `${baseClassName}--danger`,
    ghost && `${baseClassName}--ghost`,
    loading && `${baseClassName}--loading`,
    lock && `${baseClassName}--lock`,
  ].filter(Boolean).join(' ');

  const handleClick = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
    if (disabled) {
      e.preventDefault();
    } else if (onClick) {
      onClick(e);
    }
  };

  return href ? (
    <a
      className={className}
      href={href}
      target={target}
      onClick={handleClick}
    >
      {children}
    </a>
  ) : (
    <button
      className={className}
      type={htmlType}
      disabled={disabled}
      onClick={handleClick}
    >
      {children}
    </button>
  );
};

export default Button;

如何实现modal组件

interface IModalProps {
  afterClose?: () => void;
  bodyStyle?: CSSProperties;
  cancelButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  cancelText?: ReactNode;
  centered?: boolean;
  closeIcon?: boolean | ReactNode;
  confirmLoading?: boolean;
  destroyOnClose?: boolean;
  focusTriggerAfterClose?: boolean;
  footer?: ReactNode;
  forceRender?: boolean;
  getContainer?: HTMLElement | (() => HTMLElement) | string | false;
  keyboard?: boolean;
  mask?: boolean;
  maskClosable?: boolean;
  maskStyle?: CSSProperties;
  modalRender?: (node: ReactNode) => ReactNode;
  okButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  okText?: ReactNode;
  okType?: string;
  style?: CSSProperties;
  title?: ReactNode;
  open?: boolean;
  width?: string | number;
  wrapClassName?: string;
  zIndex?: number;
  onCancel?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onOk?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  afterOpenChange?: (open: boolean) => void;
}

const Modal: React.FC<IModalProps> = ({
  children,
  title = '',
  onCancel,
  onOk,
  open = false,
  mask = true,
}) => {
  return (
    <>
      {mask && <div className="modal-mask" style={{display: open ? 'block' : 'none'}}></div>}
      {open && (
        <div className="modal" style={{display: 'block'}}>
          <h2 className="modal-title">{title}</h2>
          <div className="modal-body">{children}</div>
          <div className="modal-footer">
            <button className="modal-footer-cancel" onClick={onCancel}>
              Cancel
            </button>
            <button className="modal-footer-ok" onClick={onOk}>
              OK
            </button>
          </div>
        </div>
      )}
    </>
  );
};

Modal.info = function(props: IModalProps) {
  const div = document.createElement('div');
  document.body.appendChild(div);

  function remove() {
    ReactDOM.unmountComponentAtNode(div);
    document.body.removeChild(div);
  }

  function onCancel(e: React.MouseEvent<HTMLButtonElement>) {
    if (props.onCancel) {
      props.onCancel(e);
    }
    remove();
  }

  function onOk(e: React.MouseEvent<HTMLButtonElement>) {
    if (props.onOk) {
      props.onOk(e);
    }
    remove();
  }

  ReactDOM.render(
    <Modal {...props} onCancel={onCancel} onOk={onOk} open={true} />,
    div
  );
};

如何实现高性能Tree组件

实现Tree组件的核心思路是什么?

Tree组件的核心思路是将原始的嵌套children数据结构平铺成一维数组,然后通过计算每个节点的深度(deep)、层级关系等信息,在渲染时动态计算缩进宽度、连接线等,从而实现树形结构的可视化。

Tree组件如何实现高性能大数据渲染?

  • 将原始树形数据平铺为一维数组,便于后续计算
  • 计算出实际需要渲染的节点数据,过滤隐藏的节点
  • 利用虚拟列表技术只渲染可视区域的数据,实现大数据量的高效渲染
function flattenTreeData(treeData = [], parent = null) {
  const nodes = [];

  treeData.forEach((node) => {
    const newNode = {
      ...node,
      parent,
    };

    nodes.push(newNode);

    if (newNode.children) {
      nodes.push(...flattenTreeData(newNode.children, newNode));
    }
  });

  return nodes;
}

如何计算Tree组件中节点的各种状态(展开/折叠、选中等)?

  • 展开/折叠状态根据ExpandedKeys计算
  • 复选框选中状态需要考虑受控/非受控,严格受控模式,及父子节点关联
  • Necesita calcular recursivamente el estado del nodo principal y los nodos secundarios
  • Use el índice en mosaico para consultar los nodos relacionados
function flattenTreeData(treeData = [], parent = null) {
  const nodes = [];

  treeData.forEach((node) => {
    const newNode = {
      ...node,
      parent,
    };

    nodes.push(newNode);

    if (newNode.children) {
      nodes.push(...flattenTreeData(newNode.children, newNode));
    }
  });

  return nodes;
}

¿Cómo realizar la interacción del componente Árbol? Haga clic en el nodo para expandirlo y contraerlo, marcar el cambio de estado de la casilla, etc.

  • Haga clic para expandir y plegar actualizando el estado del nodo, el estado visual y las teclas expandidas
  • Al hacer clic en la casilla de verificación, se debe actualizar recursivamente el estado de los nodos principal y secundario, y las claves relacionadas
  • Calcule y guarde el estado en tiempo real y notifique al exterior a través de la función de devolución de llamada
function toggleExpanded(nodes, node) {
  return nodes.map((currentNode) => {
    if (currentNode === node) {
      return {
        ...currentNode,
        expanded: !currentNode.expanded,
      };
    }

    return currentNode;
  });
}

// 在渲染时计算缩进:
function renderNode(node) {
  const indentLevel = getIndentLevel(node);
  const style = {
    paddingLeft: `${indentLevel * 16}px`,
  };

  return (
    <div style={style} onClick={() => handleNodeClick(node)}>
      {node.label}
    </div>
  );
}

¿Cómo implementar un componente Table de alto rendimiento?

Puede consultar ali-react-table: componente de tabla React de alto rendimiento

¿Dónde está el principal cuello de botella de rendimiento del componente de tabla?

  • Renderizar grandes cantidades de DOM;
  • La representación de actualización frecuente, como el cambio del estado de la fila seleccionada, hace que se vuelva a representar toda la tabla.

¿Cómo optimizar el rendimiento de representación del componente de tabla?

  1. Solo renderiza las columnas necesarias:
const columnsToRender = columns.filter(column => column.shouldRender);

return (
  <table>
    <thead>
      <tr>
        {columnsToRender.map(column => (
          <th key={column.key}>{column.title}</th>
        ))}
      </tr>
    </thead>
    <tbody>
      {data.map(row => (
        <tr key={row.id}>
          {columnsToRender.map(column => (
            <td key={column.key}>{row[column.key]}</td>
          ))}
        </tr>
      ))}
    </tbody>
  </table>
);
  1. Actualizaciones detalladas, solo actualice las filas/columnas modificadas. En React, puede usar React.memoo shouldComponentUpdatepara evitar una nueva renderización innecesaria:
function Row({ data, columns }) {
  return (
    <tr>
      {columns.map(column => (
        <Cell key={column.key} data={data[column.key]} />
      ))}
    </tr>
  );
}

const areEqual = (prevProps, nextProps) => {
  return prevProps.data === nextProps.data && prevProps.columns === nextProps.columns;
};

export default React.memo(Row, areEqual);
  1. Con la tecnología de virtualización, solo se representan las filas en el área visible. Esto se puede lograr utilizando bibliotecas de terceros como react-windowo react-virtualized:
import { FixedSizeList as List } from "react-window";

function Table({ data, columns }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {columns.map(column => (
        <Cell key={column.key} data={data[index][column.key]} />
      ))}
    </div>
  );

  return (
    <List
      height={500}
      itemCount={data.length}
      itemSize={35}
    >
      {Row}
    </List>
  );
}
  1. Use Web Workers para el procesamiento de datos o tareas computacionalmente intensivas:
// 创建一个新的 worker
const worker = new Worker('worker.js');

// 向 worker 发送数据
worker.postMessage(data);

// 监听 worker 的消息
worker.addEventListener('message', (event) => {
  // 更新表格数据
  updateTable(event.data);
});

en worker.js:

self.addEventListener('message', (event) => {
  // 处理数据
  const processedData = processData(event.data);

  // 发送处理后的数据
  self.postMessage(processedData);
});

Biblioteca de componentes del paquete basada en componentes web

Esto puede tomarse como una extensión para entender. En la actualidad, más y más bibliotecas de componentes de código abierto se están desarrollando en esta dirección. Puede consultar este artículo Cómo encapsular bibliotecas de componentes de interfaz de usuario basadas en WebComponents

Supongo que te gusta

Origin juejin.im/post/7261080561480089655
Recomendado
Clasificación