图解算法小册

最近整顿了一本算法小册,感兴趣的同学能够加我微信linwu-hi进行获取

前言

在上一篇作为面试官,为什么我举荐微前端作为前端面试的亮点?反馈成果不错,我接着出第二篇组件库专题次要是我抉择的方向,前端同学都能够很轻易尝试,这样我的项目上就减少很多亮点了

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

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

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

理论工作中,咱们在我的项目中须要自定义主题色更改按钮款式自定义图标,自定义table组件等等,这些都能够基于antd组件库进行二次封装,缩小反复工作,晋升开发效率。

所以咱们在封装的时候依照上面这四个准则进行思考就行了,另外自身封装组件库对于我的项目来说也是没有任何危险,因为一开始咱们把PropsType间接进行转发,外部再进行减少业务的性能,这样就是达到齐全的解耦

  • 对立格调:在一个大的我的项目或者多个相干的我的项目中,保持一致的界面风格和交互方式是十分重要的。通过二次封装,咱们能够定义对立的款式和行为,缩小不一致性。
  • 升高保护老本:当底层的组件库更新时,咱们可能须要在我的项目的多个中央进行批改。然而如果咱们有了本人的封装,只须要在封装层面进行更新即可,这大大降低了保护老本。
  • 减少定制性能:有些时候,咱们须要在原有组件库的根底上减少一些特定的性能,如特定的验证、错误处理等。二次封装提供了这样的可能。
  • 进步开发效率:在一些罕用的性能(如表单验证、全局提醒等)上,二次封装能够提供更不便的API,进步开发效率。

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

当咱们联合一个组件库设计的过程来议论前端工程化的思维时,须要理清这些要点:

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

# 装置 Lernanpm install -g lerna# 初始化一个 Lerna 仓库lerna init# 创立 "Button" 组件包lerna create button --yes

2. 规范化提交:应用规范化的提交信息能够进步 Git 日志的可读性,并且能够通过 conventional commits 主动生成 CHANGELOG。能够应用 commitizen、commitlint 等工具来配置。

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

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

# 装置相干工具npm install eslint prettier eslint-plugin-prettier eslint-config-prettier --save-dev
// .eslintrc.jsmodule.exports = {  extends: ['eslint:recommended', 'plugin:prettier/recommended'],};// .prettierrc.jsmodule.exports = {  singleQuote: true,  trailingComma: 'es5',};

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

// packages/button/src/Button.jsimport React from 'react';const Button = ({ type = 'primary', onClick, children }) => {  return (    <button className={`button ${type}`} onClick={onClick}>      {children}    </button>  );};export default Button;

5. 文档站点:能够基于 dumi 搭建文档站点,并实现 CDN 减速、增量公布等优化。能够应用 surge 实现 PR 预览。

<!-- packages/button/docs/index.md --># ButtonA simple button component.## Usageimport { 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. 单元测试:须要思考 jest、enzyme 等工具的配合应用,生成测试覆盖率报告。

# 装置相干工具npm install jest enzyme enzyme-adapter-react-16 react-test-renderer --save-dev
// packages/button/src/Button.test.jsimport 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. 按需加载:须要配合 babel-plugin-import 实现按需加载,即在编译时批改导入门路来实现组件的按需加载。

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

8. 组件设计:须要思考响应式、主题、国际化、TypeScript 反对等问题,以保障组件的灵活性和可扩展性。

// packages/button/src/Button.jsimport 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. 公布前的自动化脚本:须要编写自动化脚本来标准公布流程,确保公布的一致性和可靠性。

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

10. 公布后的解决:思考补丁降级、文档站点同步公布等问题,以便及时修复问题并提供最新的文档。

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.jsexport 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.tsexport interface ButtonProps { /*...*/ }// 在一个独自的文件中从新导出所有类型// types.tsexport 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 插件能够在编译时将导入整个库的语句转换为仅导入应用的组件。
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}

款式如何实现真正的按需加载?防止款式反复打包?

款式和逻辑拆散款式和逻辑联合款式和逻辑关联
开发打包流程中等简略简单
输入文件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-importunplugin-vue-components等。
  • 款式文件打包可能存在冗余。

适宜须要高适用性和灵活性的组件库。

款式和逻辑联合

这种计划将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.ymlname: CIon: [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按钮

jcode

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组件

jcode

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计算
  • 复选框选中状态须要思考受控/非受控,严格受控模式,及父子节点关联
  • 须要递归计算父节点和子节点的状态
  • 利用平铺后的索引进行相干节点查问
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实现
  • 点击复选框须要递归更新父子节点的状态,及相干keys
  • 计算并保留实时状态,通过回调函数告诉内部
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>  );}

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

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

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

  • 渲染大量 DOM;
  • 频繁的更新渲染,如选中行状态扭转引起整个表格从新渲染。

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

  1. 只渲染必要的列:
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. 细粒度更新,只更新变动行/列。在React中,能够应用React.memo或者shouldComponentUpdate来防止不必要的重渲染:
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. 采纳虚拟化技术,只渲染可视区的行。能够应用第三方库如react-window或者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. 应用Web Workers来解决数据处理或计算密集型工作:
// 创立一个新的 workerconst worker = new Worker('worker.js');// 向 worker 发送数据worker.postMessage(data);// 监听 worker 的音讯worker.addEventListener('message', (event) => {  // 更新表格数据  updateTable(event.data);});

worker.js中:

self.addEventListener('message', (event) => {  // 解决数据  const processedData = processData(event.data);  // 发送解决后的数据  self.postMessage(processedData);});

基于Web Components封装组件库

这个能够当做拓展理解一下,目前有越来越多的开源组件库往这个方向倒退,能够参考这篇文章如何基于 WebComponents 封装 UI 组件库