当前位置:   article > 正文

作为面试官,为什么我推荐组件库作为前端面试的亮点?

组件库面试


大厂面试的时候,我也看到很多候选人写了xx组件的封装,很少见过二次组件库的封装或者维护开源组件库,其实这些都是项目上的亮点,一般面试官如果看到,都会详细考察

本文将会以antd Element vant等等组件库为例子,会进行分析对比

为什么需要二次封装组件库?

实际工作中,我们在项目中需要自定义主题色更改按钮样式自定义图标,自定义table组件等等,这些都可以基于antd组件库进行二次封装,减少重复工作,提升开发效率。

所以我们在封装的时候按照下面这四个原则进行思考就行了,另外本身封装组件库对于项目来说也是没有任何风险,因为一开始我们把PropsType直接进行转发,内部再进行增加业务的功能,这样就是达到完全的解耦

  • 统一风格:在一个大的项目或者多个相关的项目中,保持一致的界面风格和交互方式是非常重要的。通过二次封装,我们可以定义统一的样式和行为,减少不一致性。

  • 降低维护成本:当底层的组件库更新时,我们可能需要在项目的多个地方进行修改。但是如果我们有了自己的封装,只需要在封装层面进行更新即可,这大大降低了维护成本。

  • 增加定制功能:有些时候,我们需要在原有组件库的基础上增加一些特定的功能,如特定的验证、错误处理等。二次封装提供了这样的可能。

  • 提高开发效率:在一些常用的功能(如表单验证、全局提示等)上,二次封装可以提供更方便的API,提高开发效率。

请结合一个组件库设计的过程,谈谈前端工程化的思想

当我们结合一个组件库设计的过程来谈论前端工程化的思想时,需要理清这些要点:

1. 使用 Lerna 进行多包管理:通过 Lerna 来管理多个包(组件),实现组件级别的解耦、独立版本控制、按需加载等特性。

  1. # 安装 Lerna
  2. npm install -g lerna
  3. # 初始化一个 Lerna 仓库
  4. lerna init
  5. # 创建 "Button" 组件包
  6. lerna create button --yes

2. 规范化提交:使用规范化的提交信息可以提高 Git 日志的可读性,并且可以通过 conventional commits 自动生成 CHANGELOG。可以使用 commitizen、commitlint 等工具来配置。

  1. # 安装相关工具
  2. npm install commitizen cz-conventional-changelog --save-dev
  1. // package.json
  2. {
  3.   "scripts": {
  4.     "commit""git-cz"
  5.   },
  6.   "config": {
  7.     "commitizen": {
  8.       "path""cz-conventional-changelog"
  9.     }
  10.   }
  11. }

3. 代码规范化:通过 ESLint、Prettier 等工具实现代码规范化和格式化,并封装为自己的规范预设。

  1. # 安装相关工具
  2. npm install eslint prettier eslint-plugin-prettier eslint-config-prettier --save-dev
  1. // .eslintrc.js
  2. module.exports = {
  3.   extends: ['eslint:recommended''plugin:prettier/recommended'],
  4. };
  5. // .prettierrc.js
  6. module.exports = {
  7.   singleQuote: true,
  8.   trailingComma: 'es5',
  9. };

4. 组件开发调试:需要考虑热更新编译、软链接引用等问题,以方便在开发过程中进行组件的调试。

  1. // packages/button/src/Button.js
  2. import React from 'react';
  3. const Button = ({ type = 'primary', onClick, children }) => {
  4.   return (
  5.     <button className={`button ${type}`} onClick={onClick}>
  6.       {children}
  7.     </button>
  8.   );
  9. };
  10. export default Button;

5. 文档站点:可以基于 dumi 搭建文档站点,并实现 CDN 加速、增量发布等优化。可以使用 surge 实现 PR 预览。

  1. <!-- packages/button/docs/index.md -->
  2. # Button
  3. A simple button component.
  4. ## Usage
  5. import { Button } from 'button-library';
  6. const MyComponent = () => {
  7.   return <Button onClick={() => alert('Button clicked!')}>Click Me</Button>;
  8. };
  9. ### Props
  10. | Name     | Type                   | Default | Description                   |
  11. | -------- | ---------------------- | ------- | ----------------------------- |
  12. type     | `primary` \| `secondary` | `primary` | The type of the button. |
  13. | onClick  | `function`             |         | Event handler for click event. |

6. 单元测试:需要考虑 jest、enzyme 等工具的配合使用,生成测试覆盖率报告。

  1. # 安装相关工具
  2. npm install jest enzyme enzyme-adapter-react-16 react-test-renderer --save-dev
  1. // packages/button/src/Button.test.js
  2. import React from 'react';
  3. import { mount } from 'enzyme';
  4. import Button from './Button';
  5. describe('Button', () => {
  6.   it('renders without crashing', () => {
  7.     const wrapper = mount(<Button>Click Me</Button>);
  8.     expect(wrapper.exists()).toBe(true);
  9.   });
  10.   it('calls onClick function when clicked', () => {
  11.     const onClickMock = jest.fn();
  12.     const wrapper = mount(<Button onClick={onClickMock}>Click Me</Button>);
  13.     wrapper.find('button').simulate('click');
  14.     expect(onClickMock).toHaveBeenCalledTimes(1);
  15.   });
  16. });

7. 按需加载:需要配合 babel-plugin-import 实现按需加载,即在编译时修改导入路径来实现组件的按需加载。

  1. # 安装相关工具
  2. npm install babel-plugin-import --save-dev
  1. // .babelrc
  2. {
  3.   "plugins": [
  4.     [
  5.       "import",
  6.       {
  7.         "libraryName""button-library",
  8.         "style""css"
  9.       }
  10.     ]
  11.   ]
  12. }

8. 组件设计:需要考虑响应式、主题、国际化、TypeScript 支持等问题,以保证组件的灵活性和可扩展性。

  1. // packages/button/src/Button.js
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. const Button = ({ type = 'primary', onClick, children }) => {
  5.   return (
  6.     <button className={`button ${type}`} onClick={onClick}>
  7.       {children}
  8.     </button>
  9.   );
  10. };
  11. Button.propTypes = {
  12.   type: PropTypes.oneOf(['primary''secondary']),
  13.   onClick: PropTypes.func,
  14.   children: PropTypes.node.isRequired,
  15. };
  16. export default Button;

9. 发布前的自动化脚本:需要编写自动化脚本来规范发布流程,确保发布的一致性和可靠性。

  1. // package.json
  2. {
  3.   "scripts": {
  4.     "prepublish""npm run lint && npm run test",
  5.     "lint""eslint .",
  6.     "test""jest"
  7.   }
  8. }

10. 发布后的处理:考虑补丁升级、文档站点同步发布等问题,以便及时修复问题并提供最新的文档。

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

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

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

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

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

  1. import { Button } from '../src/Button';
  2. test('Button should do something', () => {
  3.     const component = new YourComponent();
  4.     // your test logic here
  5.     expect(component.doSomething()).toBe('expected result');
  6. });

2. 边界测试

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

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

3. 响应测试

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

  1. import { test } from '@playwright/test';
  2. test('Button should be responsive', async ({ page }) => {
  3.     await page.goto('http://localhost:3000/your-component');
  4.     const component = await page.$('#your-component-id');
  5.     expect(await component.isVisible()).toBe(true);
  6.     // Simulate a mobile device
  7.     await page.setViewportSize({ width: 375, height: 812 });
  8.     // Check the component under this condition
  9.     // your test logic here
  10. });

4. 交互测试

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

  1. test('Button should handle interactions', async ({ page }) => {
  2.     await page.goto('http://localhost:3000/your-component');
  3.     const component = await page.$('#your-component-id');
  4.     // Simulate a click event
  5.     await component.click();
  6.     // Check the result of the interaction
  7.     // your test logic here
  8. });

5. 异常测试

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

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

6. 性能测试

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

  1. import { performance } from 'perf_hooks';
  2. test('Button should have good performance', () => {
  3.     const start = performance.now();
  4.     const component = new YourComponent();
  5.     component.doSomething();
  6.     const end = performance.now();
  7.     const duration = end - start;
  8.     expect(duration).toBeLessThan(50);  // Expect the operation to finish within 50 ms
  9. });

7. 自动化测试

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

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

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

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

1. 定义语言包

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

  1. export default {
  2.   el: {
  3.     colorpicker: {
  4.       confirm: 'OK',
  5.       clear: 'Clear'
  6.     },
  7.     // ...other components
  8.   }
  9. };

2. 加载语言包

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

  1. import ElementUI from 'element-ui';
  2. import locale from 'element-ui/lib/locale/lang/en';
  3. Vue.use(ElementUI, { locale });

3. 使用语言包

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

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

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

4. 集成 vue-i18n

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

  1. import Vue from 'vue';
  2. import VueI18n from 'vue-i18n';
  3. import ElementUI from 'element-ui';
  4. import enLocale from 'element-ui/lib/locale/lang/en';
  5. import zhLocale from 'element-ui/lib/locale/lang/zh-CN';
  6. Vue.use(VueI18n);
  7. const messages = {
  8.   en: {
  9.     message: 'hello',
  10.     ...enLocale // 或者用 Object.assign({ message: 'hello' }, enLocale)
  11.   },
  12.   zh: {
  13.     message: '你好',
  14.     ...zhLocale // 或者用 Object.assign({ message: '你好' }, zhLocale)
  15.   }
  16. };
  17. const i18n = new VueI18n({
  18.   locale: 'zh'// set locale
  19.   messages, // set locale messages
  20. });
  21. Vue.use(ElementUI, {
  22.   i18n: (key, value) => i18n.t(key, value)
  23. });

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

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

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

1. 使用 CSS 变量定义样式

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

  1. :root {
  2.   --primary-color: #1890ff;
  3. }
  4. .btn {
  5.   background: var(--primary-color); 
  6. }

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

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

  1. // theme.js
  2. export default {
  3.   '--primary-color''#409eff'
  4. }

3. 在线主题编辑器

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

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

4. 前端应用新样式

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

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

5. 持久化主题配置

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

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

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

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

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

  1. // 全局类型
  2. export interface Size {
  3.   width: number;
  4.   height: number;
  5. }
  6. // 组件Props类型
  7. export interface ButtonProps {
  8.   size?: Size;
  9.   label: string;
  10.   onClick?: () => void;
  11. }

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

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

  1. // 在各个组件文件中定义和导出类型
  2. // button.ts
  3. export interface ButtonProps { /*...*/ }
  4. // 在一个单独的文件中重新导出所有类型
  5. // types.ts
  6. export type { ButtonProps } from './button';

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

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

  1. type SmallSize = { width: number; height: number };
  2. type LargeSize = SmallSize & { depth: number };
  3. type Size = SmallSize | LargeSize;

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

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

  1. // 不好的类型定义
  2. interface ButtonProps {
  3.   [key: string]: any;  // 这不提供任何有关props的信息
  4. }
  5. // 好的类型定义
  6. interface ButtonProps {
  7.   size?: Size;
  8.   label: string;
  9.   onClick?: () => void;
  10. }

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

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

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

1. 版本控制策略

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

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

  • 主版本号(Major): 当你做了不兼容的 API 修改

  • 次版本号(Minor): 当你做了向下兼容的功能性新增

  • 补丁版本号(Patch): 当你做了向下兼容的问题修复

2. 向下兼容处理

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

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

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

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

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

4. 兼容旧版本的方案

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

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

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

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

babel-plugin-import

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

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

tree-shaking

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

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

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

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

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

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

  1. {
  2.   "name""your-library",
  3.   "sideEffects": [
  4.     "./src/styles.css"
  5.   ]
  6. }

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

  1. {
  2.   "name""your-library",
  3.   "sideEffects"false
  4. }

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

78229a3251cb9367ae2d1adf3e2378d2.png
image.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,样式处理留给使用者。

  • 可以直接提供源码,便于主题定制。

缺点:

  • 使用时需要分别引入逻辑和样式,按需加载实现复杂,需要借助`babel-plugin-import`[1]、`unplugin-vue-components`[2]等。

  • 样式文件打包可能存在冗余。

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

样式和逻辑结合

这种方案将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 分支,并进行代码审查。

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

2. 代码检查:

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

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

  1. {
  2.   "scripts": {
  3.     "lint""eslint --ext .js,.jsx,.ts,.tsx src",
  4.     "test""jest"
  5.   }
  6. }

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

  1. # .github/workflows/ci.yml
  2. name: CI
  3. on: [push, pull_request]
  4. jobs:
  5.   build:
  6.     runs-on: ubuntu-latest
  7.     steps:
  8.       - name: Check out code
  9.         uses: actions/checkout@v2
  10.       - name: Use Node.js
  11.         uses: actions/setup-node@v2
  12.         with:
  13.           node-version: '14'
  14.       - name: Install dependencies
  15.         run: npm ci
  16.       - name: Run lint
  17.         run: npm run lint
  18.       - name: Run tests
  19.         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按钮

jcode

  1. import React, { CSSProperties, FC, MouseEvent, ReactNode } from 'react';
  2. interface ButtonProps {
  3. lock?: boolean;
  4. classNames?: Record<string, string>;
  5. danger?: boolean;
  6. disabled?: boolean;
  7. ghost?: boolean;
  8. href?: string;
  9. htmlType?: 'button' | 'submit' | 'reset';
  10. icon?: ReactNode;
  11. loading?: boolean | { delay: number };
  12. shape?: 'default' | 'circle' | 'round';
  13. size?: 'large' | 'middle' | 'small';
  14. styles?: Record<string, CSSProperties>;
  15. target?: string;
  16. type?: 'primary' | 'dashed' | 'link' | 'text' | 'default';
  17. onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
  18. children?: ReactNode;
  19. }
  20. const Button: FC<ButtonProps> = ({
  21. lock,
  22. classNames,
  23. danger,
  24. disabled,
  25. ghost,
  26. href,
  27. htmlType = 'button',
  28. icon,
  29. loading,
  30. shape,
  31. size,
  32. styles,
  33. target,
  34. type = 'default',
  35. onClick,
  36. children
  37. }) => {
  38. const baseClassName = 'button';
  39. const className = [
  40. baseClassName,
  41. type && `${baseClassName}--${type}`,
  42. size && `${baseClassName}--${size}`,
  43. shape && `${baseClassName}--${shape}`,
  44. disabled && `${baseClassName}--disabled`,
  45. danger && `${baseClassName}--danger`,
  46. ghost && `${baseClassName}--ghost`,
  47. loading && `${baseClassName}--loading`,
  48. lock && `${baseClassName}--lock`,
  49. ].filter(Boolean).join(' ');
  50. const handleClick = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
  51. if (disabled) {
  52. e.preventDefault();
  53. } else if (onClick) {
  54. onClick(e);
  55. }
  56. };
  57. return href ? (
  58. <a
  59. className={className}
  60. href={href}
  61. target={target}
  62. onClick={handleClick}
  63. >
  64. {children}
  65. </a>
  66. ) : (
  67. <button
  68. className={className}
  69. type={htmlType}
  70. disabled={disabled}
  71. onClick={handleClick}
  72. >
  73. {children}
  74. </button>
  75. );
  76. };
  77. export default Button;

如何实现modal组件

jcode

  1. interface IModalProps {
  2. afterClose?: () => void;
  3. bodyStyle?: CSSProperties;
  4. cancelButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  5. cancelText?: ReactNode;
  6. centered?: boolean;
  7. closeIcon?: boolean | ReactNode;
  8. confirmLoading?: boolean;
  9. destroyOnClose?: boolean;
  10. focusTriggerAfterClose?: boolean;
  11. footer?: ReactNode;
  12. forceRender?: boolean;
  13. getContainer?: HTMLElement | (() => HTMLElement) | string | false;
  14. keyboard?: boolean;
  15. mask?: boolean;
  16. maskClosable?: boolean;
  17. maskStyle?: CSSProperties;
  18. modalRender?: (node: ReactNode) => ReactNode;
  19. okButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
  20. okText?: ReactNode;
  21. okType?: string;
  22. style?: CSSProperties;
  23. title?: ReactNode;
  24. open?: boolean;
  25. width?: string | number;
  26. wrapClassName?: string;
  27. zIndex?: number;
  28. onCancel?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  29. onOk?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  30. afterOpenChange?: (open: boolean) => void;
  31. }
  32. const Modal: React.FC<IModalProps> = ({
  33. children,
  34. title = '',
  35. onCancel,
  36. onOk,
  37. open = false,
  38. mask = true,
  39. }) => {
  40. return (
  41. <>
  42. {mask && <div className="modal-mask" style={{display: open ? 'block' : 'none'}}></div>}
  43. {open && (
  44. <div className="modal" style={{display: 'block'}}>
  45. <h2 className="modal-title">{title}</h2>
  46. <div className="modal-body">{children}</div>
  47. <div className="modal-footer">
  48. <button className="modal-footer-cancel" onClick={onCancel}>
  49. Cancel
  50. </button>
  51. <button className="modal-footer-ok" onClick={onOk}>
  52. OK
  53. </button>
  54. </div>
  55. </div>
  56. )}
  57. </>
  58. );
  59. };
  60. Modal.info = function(props: IModalProps) {
  61. const div = document.createElement('div');
  62. document.body.appendChild(div);
  63. function remove() {
  64. ReactDOM.unmountComponentAtNode(div);
  65. document.body.removeChild(div);
  66. }
  67. function onCancel(e: React.MouseEvent<HTMLButtonElement>) {
  68. if (props.onCancel) {
  69. props.onCancel(e);
  70. }
  71. remove();
  72. }
  73. function onOk(e: React.MouseEvent<HTMLButtonElement>) {
  74. if (props.onOk) {
  75. props.onOk(e);
  76. }
  77. remove();
  78. }
  79. ReactDOM.render(
  80. <Modal {...props} onCancel={onCancel} onOk={onOk} open={true} />,
  81. div
  82. );
  83. };

如何实现高性能Tree组件

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

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

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

  • 将原始树形数据平铺为一维数组,便于后续计算

  • 计算出实际需要渲染的节点数据,过滤隐藏的节点

  • 利用虚拟列表技术只渲染可视区域的数据,实现大数据量的高效渲染

  1. function flattenTreeData(treeData = [], parent = null) {
  2.   const nodes = [];
  3.   treeData.forEach((node) => {
  4.     const newNode = {
  5.       ...node,
  6.       parent,
  7.     };
  8.     nodes.push(newNode);
  9.     if (newNode.children) {
  10.       nodes.push(...flattenTreeData(newNode.children, newNode));
  11.     }
  12.   });
  13.   return nodes;
  14. }

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

  • 展开/折叠状态根据ExpandedKeys计算

  • 复选框选中状态需要考虑受控/非受控,严格受控模式,及父子节点关联

  • 需要递归计算父节点和子节点的状态

  • 利用平铺后的索引进行相关节点查询

  1. function flattenTreeData(treeData = [], parent = null) {
  2.   const nodes = [];
  3.   treeData.forEach((node) => {
  4.     const newNode = {
  5.       ...node,
  6.       parent,
  7.     };
  8.     nodes.push(newNode);
  9.     if (newNode.children) {
  10.       nodes.push(...flattenTreeData(newNode.children, newNode));
  11.     }
  12.   });
  13.   return nodes;
  14. }

Tree组件的交互如何实现?点击节点展开折叠,复选框状态切换等

  • 点击展开折叠通过更新节点自身状态、可视状态及ExpandedKeys实现

  • 点击复选框需要递归更新父子节点的状态,及相关keys

  • 计算并保存实时状态,通过回调函数通知外部

  1. function toggleExpanded(nodes, node) {
  2.   return nodes.map((currentNode) => {
  3.     if (currentNode === node) {
  4.       return {
  5.         ...currentNode,
  6.         expanded: !currentNode.expanded,
  7.       };
  8.     }
  9.     return currentNode;
  10.   });
  11. }
  12. // 在渲染时计算缩进:
  13. function renderNode(node) {
  14.   const indentLevel = getIndentLevel(node);
  15.   const style = {
  16.     paddingLeft: `${indentLevel * 16}px`,
  17.   };
  18.   return (
  19.     <div style={style} onClick={() => handleNodeClick(node)}>
  20.       {node.label}
  21.     </div>
  22.   );
  23. }

如何实现高性能表格Table组件?

可参考ali-react-table:高性能 React 表格组件

表格组件的性能瓶颈主要在哪里?

  • 渲染大量 DOM;

  • 频繁的更新渲染,如选中行状态改变引起整个表格重新渲染。

如何优化表格组件的渲染性能?

  1. 只渲染必要的列:

  1. const columnsToRender = columns.filter(column => column.shouldRender);
  2. return (
  3.   <table>
  4.     <thead>
  5.       <tr>
  6.         {columnsToRender.map(column => (
  7.           <th key={column.key}>{column.title}</th>
  8.         ))}
  9.       </tr>
  10.     </thead>
  11.     <tbody>
  12.       {data.map(row => (
  13.         <tr key={row.id}>
  14.           {columnsToRender.map(column => (
  15.             <td key={column.key}>{row[column.key]}</td>
  16.           ))}
  17.         </tr>
  18.       ))}
  19.     </tbody>
  20.   </table>
  21. );
  1. 细粒度更新,只更新变化行/列。在React中,可以使用React.memo或者shouldComponentUpdate来避免不必要的重渲染:

  1. function Row({ data, columns }) {
  2.   return (
  3.     <tr>
  4.       {columns.map(column => (
  5.         <Cell key={column.key} data={data[column.key]} />
  6.       ))}
  7.     </tr>
  8.   );
  9. }
  10. const areEqual = (prevProps, nextProps) => {
  11.   return prevProps.data === nextProps.data && prevProps.columns === nextProps.columns;
  12. };
  13. export default React.memo(Row, areEqual);
  1. 采用虚拟化技术,只渲染可视区的行。可以使用第三方库如react-window或者react-virtualized来实现:

  1. import { FixedSizeList as List } from "react-window";
  2. function Table({ data, columns }) {
  3.   const Row = ({ index, style }) => (
  4.     <div style={style}>
  5.       {columns.map(column => (
  6.         <Cell key={column.key} data={data[index][column.key]} />
  7.       ))}
  8.     </div>
  9.   );
  10.   return (
  11.     <List
  12.       height={500}
  13.       itemCount={data.length}
  14.       itemSize={35}
  15.     >
  16.       {Row}
  17.     </List>
  18.   );
  19. }
  1. 使用Web Workers来处理数据处理或计算密集型任务:

  1. // 创建一个新的 worker
  2. const worker = new Worker('worker.js');
  3. // 向 worker 发送数据
  4. worker.postMessage(data);
  5. // 监听 worker 的消息
  6. worker.addEventListener('message', (event) => {
  7.   // 更新表格数据
  8.   updateTable(event.data);
  9. });

worker.js中:

  1. self.addEventListener('message', (event) => {
  2.   // 处理数据
  3.   const processedData = processData(event.data);
  4.   // 发送处理后的数据
  5.   self.postMessage(processedData);
  6. })

参考资料

[1]

https://github.com/umijs/babel-plugin-import: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fumijs%2Fbabel-plugin-import

[2]

https://github.com/antfu/unplugin-vue-components: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fantfu%2Funplugin-vue-components

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我个小忙:

  1. 点个「喜欢」或「在看」,让更多的人也能看到这篇内容

  2. 我组建了个氛围非常好的前端群,里面有很多前端小伙伴,欢迎加我微信「sherlocked_93」拉你加群,一起交流和学习

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

a6b0ee28be573254e250b4d2cc8a56ed.png

e00a642cf28337439e9f94ff4278313d.png

点个喜欢支持我吧,在看就更好了

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/134193
推荐阅读
相关标签
  

闽ICP备14008679号