React записывает полный процесс выпуска библиотеки компонентов частной библиотеки компонентов пользовательского интерфейса (выше)

React.js

предисловие

Пока вы пишете интерфейсный код, вы не можете избежать использования популярныхUIбиблиотека компонентов, напримерElementUI,Ant Design.但是我们只停留在使用层面上的话,未免显得肤浅。因此作者一直都想编写一个属于自己的UIКомпонентная библиотека.

На самом деле, знания, участвующие в создании собственной библиотеки компонентов, все еще очень обширны:

  • Уровень языка:TypeScipt,React,Sass;
  • Jest,React Testing Library;
  • Фронтенд-инжиниринг: инфраструктура библиотеки компонентов, упаковка библиотеки компонентов, публикация библиотеки компонентов, публикация документации библиотеки компонентов.

Базовые знания, необходимые на уровне языка и уровне теста, не будут объясняться в этой статье из-за ограничений по объему, надеюсь, у вас уже есть определенная база.

Помимо написания общих компонентов, сложность создания библиотеки компонентов связана с инженерным аспектом. Поэтому в этой статье шаг за шагом объясняется, как создать и опубликовать библиотеку компонентов уровня предприятия в следующем порядке.

  1. Построение базовой библиотеки компонентов;
  2. НапишиButtonкомпонент;
  3. Документация библиотеки компонентов и отладка;
  4. Упаковать и опубликовать библиотеку компонентов;
  5. Электронная документация публикуется автоматически.

Базовая структура библиотеки компонентов

Структура каталогов

├──README.md // 文档说明
├──node_modules
├──package.json
├──tsconfig.json // ts 配置文件
├──.gitignore
└──src
    ├──components // 组件库
    ├──styles // 公用样式库
    └──index.js // 组件库入口文件

Через структуру каталогов библиотеки компонентов вы обнаружите, что знакомыеApp.tsxУшли, действительно мы библиотека компонентов, а не приложение, так что нам это не нужно.

Просмотрите полный код в этой сводке

стилевые решения

использоватьcssязык предварительной обработкиsasssassДля использования вы можете обратиться к его официальной документации, которая не будет здесь подробно объясняться.

Основная структура каталогов и анализ функций:

└──styles
	├──_variables.scss // 各种变量以及可配置设置
	├──_mixins.scss // 全局 mixins
	├──_reboot.scss // 重置样式
		├──_functions.scss // 全局 functions
		└──index.scss // import styles 中所有样式
└──components
	└──Button
		└──style.scss // 组件独立样式

  • форма
  • кнопка
  • Границы и тени
  • ...

Определите файл переменных:_variables.scss  Преимущество определения переменных и констант заключается в том, что вся библиотека компонентов унифицирована, что будет проблематично при первом написании.Как только проект достигнет определенного размера, вы сможете ощутить его преимущества.

# 基础色彩系统
$blue:    #0d6efd !default;
$indigo:  #6610f2 !default;
...

# 字体系统
$font-family-sans-serif:      -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
// 等宽字体
$font-family-monospace:       SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
// 主要字体
$font-family-base:            $font-family-sans-serif !default;

// 字体大小
$font-size-base:              1rem !default; // Assumes the browser default, typically `16px`
$font-size-lg:                $font-size-base * 1.25 !default;
$font-size-sm:                $font-size-base * .875 !default;
$font-size-root:              null !default;

// 字重
$font-weight-lighter:         lighter !default;
$font-weight-light:           300 !default;
...

// 行高
$line-height-base:            1.5 !default;
...

// 标题大小
$h1-font-size:                $font-size-base * 2.5 !default;
...

!defaultЧто это такое?

Sassпри условии!defaultлоготип. Переменной присваивается значение только в том случае, если переменная не определена или ее значение пусто. В противном случае будет использовано существующее значение.

сброс стиля

использоватьnormalize.cssрешение, егоgithub адресGitHub.com/Колас/ни….

Что оно делает?

  • со многимиCSSсброс отличается, сохраняйте полезные значения по умолчанию;
  • Стандартизировать стили различных элементов;
  • Исправление ошибок и распространенных несоответствий браузера;
  • Улучшить удобство использования за счет незначительных модификаций;
  • Примечание описано с использованием подробного кода действия.

_reboot.scss 

body {
  margin: 0; // 1
  font-family: $font-family-base;
  font-size: $font-size-base;
  font-weight: $font-weight-base;
  line-height: $line-height-base;
  color: $body-color;
  text-align: $body-text-align;
  background-color: $body-bg; // 2
  -webkit-text-size-adjust: 100%; // 3
  -webkit-tap-highlight-color: rgba($black, 0); // 4
}
...省略

уже использован_variables

стиль импорта

index.scss 

// config
@import "variables";

//layout
@import "reboot";

ранее определяемый как_variables а также_reboot, почему при импорте нет подчеркивания?

Если у вас есть файл Scss или Sass, который необходимо импортировать, но вы не хотите, чтобы он был скомпилирован в файл CSS, вы можете запретить его компиляцию, добавив подчеркивание к имени файла. Это укажет Sass не компилировать его в файл CSS. Затем вы можете импортировать файл как обычно, но также опустить подчеркивание перед именем файла.

src/index.tsxПредставляем файлы стиля

...
import './styles/index.scss';
import Button from "./components/Button";

ReactDOM.render(
  <React.StrictMode>
    <Button />
  </React.StrictMode>,
  document.getElementById('root')
);

Просмотрите полный код в этой сводке

Компонент кнопки

Анализ требований к компонентам

давайте сначала посмотримAnt Design серединаButton На что это похоже?image.png image.png

Ant Designиспользовать в

<Button type="primary">Primary Button</Button>
<Button type="primary" disabled>Primary(disabled)</Button>
...

Кратко резюмируем:

  1. согласно сtypeДелить:Primary,Default,Danger,Link;
  2. согласно сsizeДелить:Normal,Small,Large;
  3. согласно сdisabledСтатус: Отключено, Нормально.

Спрос - это простой анализ здесь, давайте начнем кодировать.

компонентное кодирование

пишуButtonПрежде чем впервые представить полезный инструментclassnames, который является простым в использованииJavaScriptбиблиотека, посредством условного суждения будетclassNameСшейте вместе.

Установитьclassnames 

yarn add classnames @types/classnames

использоватьclassnames 

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
 
// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
 
// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

После введения инструмента мы официально начнем, потому что технический отбор используетTypeScript, поэтому мы можем сначала определить соответствующие типы в соответствии с приведенным выше анализом спроса:

// 定义按钮大小类型
export type ButtonSize = 'lg' | 'sm';

// 定义按钮type种类
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

// 定义Button组件基础入参属性
interface BaseButtonProps {
  className: string;
  disabled: boolean;
  size: ButtonSize;
  btnType: ButtonType;
  children: React.ReactNode;
  href: string;
}

// 定义按钮的基础类型与原生按钮的联合类型
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>;

// 定义按钮的基础类型与原生A标签的联合类型
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>;

// 执行 Partial,相当于所有属性都变为可选,如 {disabled?:boolean,...}.
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>;

Тип определен, следующая реализацияButtonОсновные функции компонента:

src/components/Button/Button.tsx 

const Button: React.FC<ButtonProps> = (props)=>{
  
  // 通过 ES6 对象的解构赋值取出所有属性,其中restProps就是除显示定义的剩下所有的属性。
  const {
    btnType,
    disabled,
    size,
    children,
    className,
    href,
    ...restProps //ES6 rest 语法
  } = props;
  
	// 利用 classNames 判断按钮的相应 class 值。
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType, // btnType 参数存在时则动态添加 `btn-${btnType}` 类
    [`btn-${size}`]: size, // size 参数存在时动态添加 `btn-${size}` 类
    'disabled': (btnType === 'link') && disabled // 由于 a 链接原生不带有 disabled 属性,因此需要手动给它添加一个 disabled 类。通过编写类的样式实现disabled效果
  })
  
	// 判断如果是 link 类型,则输出 a 链接,否则输出 button。
  if(btnType === 'link' && href){
    return (
      <a
        {...restProps}
        href={href}
        className={classes}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        {...restProps}
        className={classes}
        disabled={disabled}
      >
        {children}
      </button>
    )
  }
}
// 属性默认值
Button.defaultProps = {
  disabled: false,
  btnType: 'default'
}

Button/_styles.scss 

.btn {
  position: relative;
  display: inline-block;
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;
  background-image: none;
  border: $btn-border-width solid transparent;
  @include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;
  &.disabled,
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-shadow: none;
    > * {
      pointer-events: none; // 清除鼠标事件
    }
  }
}

.btn-lg {
  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}

... 省略

Помимо использования переменных в стиле,mixin ЖдатьsassДругие заклинания высокого порядкаCSSБез разницы, главное не забытьindex.scssПредставлен в:

// button
@import "../components/Button/styles";

оButtonБизнес-код компонента почти написан здесь, но у нас еще есть очень важная вещь, то есть модульное тестирование, Поскольку это общий компонент, он очень подходит для проверки того, соответствуют ли его функции ожиданиям посредством модульного тестирования.

тестовый код компонента

в настоящее время более популярныReactРешение для модульного тестированияJEST а такжеReact Testing Library. Поскольку юнит-тестирование — очень обширная тема, в этой статье мы не намерены подробно ее освещать.

JEST

Jest ЯвляетсяJavaScriptПлатформа тестирования, разработанная для того, чтобы гарантировать, что любойJavaScriptправильность кода. Он позволяет использовать доступные, знакомые и многофункциональныеAPIПриходите и пишите тесты, чтобы быстро получить результаты.

Функции:

  • Нулевая конфигурация:JestЦель состоит в том, чтобы быть в большинствеJavaScriptЕго можно использовать из коробки на проекте без настройки.
  • Снимки: сборки могут легко отслеживать большиеObjectТестовое задание. Снимки могут быть независимыми от тестового кода, а также могут быть интегрированы во внутренние строки кода.
  • Изолированный: программа тестирования работает параллельно в своем собственном процессе, чтобы максимизировать производительность.
  • начальствоapi :отit прибыть expect,JestСоберите весь комплект в одном месте, легко писать, легко обслуживать, очень удобно.
  • Быстро и безопасно: гарантируя, что ваши тесты имеют уникальное глобальное состояние,JestТесты можно надежно запускать параллельно. Чтобы ускорить процесс тестирования,JestСначала запускает предыдущий неудачный тест и реорганизуйте тест в соответствии с тестовым файлом.
  • --coverageJestВесь проект может быть собран из информации о покрытии кода, включая непроверенные документы.

React Testing Library

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps } from './Button'

const defaultProps = {
  onClick: jest.fn()
}

describe('test Button component', () => {
  it('should render the correct default button', () => {
    //  wrapper 获取到通过render方法解析的React组件实例信息
    const wrapper = render(<Button {...defaultProps}>Nice</Button>)
    // element 则是通过 text 值获取到的类似“DOM”
    const element = wrapper.getByText('Nice') as HTMLButtonElement
  	// 通过 JEST 框架可以做一系列断言
    expect(element).toBeInTheDocument()
    expect(element.tagName).toEqual('BUTTON')
    expect(element).toHaveClass('btn btn-default')
    expect(element.disabled).toBeFalsy()
  	// 触发元素click事件
    fireEvent.click(element)
  	// 断言模拟事件被触发
    expect(defaultProps.onClick).toHaveBeenCalled()
  })
})

Из этого простого примера можно увидеть, что он фактически моделирует траекторию использования пользователя: «Получение определенных результатов, делая что-то».

На данный момент, один и функция, стиль, тестButtonКомпоненты все готовы.

Просмотрите полный код в этой сводке

Документация библиотеки компонентов и отладка

Хотя мы только разработалиButtonкомпонент, но есть проблема, которая нас беспокоит, то есть сложность отладки и компонент написан, и нет соответствующего вывода документа.Если вы хотите использоватьButtonКомпоненты должны смотреть исходный код. Очевидно, что это очень неразумно, нам нужно использовать инструменты, которые помогут нам реализовать функцию документа.

Storybook

Storybook — это инструмент с открытым исходным кодом для изолированной разработки компонентов пользовательского интерфейса для React, Vue, Angular и т. д.

Функции:

  • Предоставляет песочницу для изолированного строительстваUIкомпонент;
  • Предоставляет некоторые расширенные функции плагина для более быстрой сборкиUI, документирование библиотек компонентов и оптимизация рабочих процессов;
  • StorybookПозволяет нам легко включать техническую документацию в нашу систему проектирования, что еще больше упрощает разработку компонентов.

Установить:

npx -p @storybook/cli sb init
或者
yarn global @storybook/cli && sb init

запускать:

npm run storybook

Добавьте файл:Button/Button.stories.tsx 

import React from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import Button , { ButtonProps } from './Button';
import "../../styles/index.scss";

export default {
  title: 'Button',
  component: Button,
} as Meta;

const Template: Story<ButtonProps> = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  btnType: 'danger',
  children: "确定"
};

Конечный результат документа:

QQ20201210-183845.gif

Используется только здесьstorybookГотовая функция, ее функция очень мощная, заинтересованные студенты могут читатьОфициальная документация сборника рассказов.

Просмотрите полный код в этой сводке

Библиотека компонентов упакована и выпущена

Пакет

React AppПроцесс упаковки, поindex.tsxфайл является записью,AppПакет корневого компонента и, наконец, генерировать разделJavaScriptСкрипт, динамически вставляемый на страницуDOMи стили для формирования полной страницы.

Библиотека компонентов не является приложением, очевидно, что ее нельзя так упаковать, давайте посмотримant designКак компоненты используются приложением:

import {Button} from "antd";

Это также означает, что входной файл нашей библиотеки компонентов должен использоваться как внешний поставщик всех компонентов.

в стадии трансформацииindex.tsx 

export { default as Button } from "./components/Button";

Конечно, здесь мы просто пишемButtonКомпоненты, если их несколько, их можно экспортировать с одинаковым синтаксисом.

Как мы все знаем,webpackявляется упаковщиком модулей, он проанализирует зависимости в модуле и сделает большойJavaScriptpackage, однако наша библиотека компонентов может бытьESModuleПуть модуля предоставляется непосредственно пользователю. Так что нам просто нужно положитьTypeScriptES5Грамматика может быть. Итак, когда пакет помощиts

создать новыйtsconfig.build.jsonКонфигурация в виде файла пакета:

{
  "compilerOptions": {
    "outDir": "dist", // 打包输出位置
    "module": "esnext", // 设置生成代码的模块标准,可以设置为 CommonJS、AMD 和 UMD 等等。
    "target": "es5", // 目标语言的版本
    "declaration": true, // 生成声明文件,记得 inde.d.ts
    "jsx": "react", // 等效 React.createElement调用
    "moduleResolution":"Node", // 模块解析策略,这里提供两种解析策略 node 和 classic,ts 默认使用 node 解析策略。
    "allowSyntheticDefaultImports": true, // 允许对不包含默认导出的模块使用默认导入。这个选项不会影响生成的代码,只会影响类型检查。
    "skipLibCheck": true // 跳过类库检查
  },
  "include": [
    "src" // 编译文件夹
  ],
  "exclude": [ // 排除编译文件
    "src/**/*.test.tsx",
    "src/**/*.stories.tsx",
    "src/setupTests.ts",
    "stories/**/*.svg",
    "stories/**/*.mdx"
  ]
}

package.json Увеличиватьscripts 

// 清除文件命令,需要手动安装rimraf包
"clean": "rimraf ./dist",
// 通过 tsconfig.build.json 配置编译 ts 文件
"build-ts": "tsc -p tsconfig.build.json",
// 编译sass文件
"build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
// 总的编译命令,继发执行  
"build": "npm run clean && npm run build-css && npm run build-ts"

воплощать в жизньnpm run buildКоманда, смотриdistФайлы, созданные в каталоге:

image.png

Задача упаковки завершена,Просмотрите полный код в этой сводке.

выпускать

После того, как упаковка завершена, нам нужно опубликовать пакет в Интернете.npmпо управлению пакетами. Итак, нам нужно сначала войти и зарегистрироватьсяnpm.

# 查看是否登录
npm whoami

# 查看 npm 配置表
npm config ls

# 注册
npm adduser
Username:shiyou
Email:(xxx@xxx.com)

# 登录
npm login
然后填写注册信息即可

package.jsonоптимизировать

{
  "name": "lion-design", // 包名,必须要是唯一的
  "version": "1.0.1", // 版本号
  "author": "Lion", // 作者
  "private": false, // 非私有
  "main": "dist/index.js", // 项目的入口文件
  "module": "dist/index.js", // 指向具有 ES2015 模块语法的模块,但仅指向目标环境支持的语法功能。
  "types": "dist/index.d.ts", // 只在 TypeScript 中生效的字段,指向声明文件
  "license": "MIT", // 使用许可证
  "homepage": "https://github.com/shiyou00/lion-design", // 是包的项目主页或者文档首页。
  "repository": { // 代码托管的位置
    "type": "git",
    "url": "https://github.com/shiyou00/lion-design"
  },
  "files": [ // 项目上传至npm服务器的文件,可以是单独的文件、整个文件夹,或者通配符匹配到的文件。
    "dist"
  ],
  "scripts": {
    "clean": "rimraf ./dist",
    "build-ts": "tsc -p tsconfig.build.json",
    "build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
    "build": "npm run clean && npm run build-css && npm run build-ts",
    "prepublishOnly": "npm run build" // 执行 npm publish 之前会默认执行的命令
  },
  // 开发版和发布版需要的依赖
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "classnames": "^2.2.6",
  },
   // 开发环境依赖包
  "devDependencies": {
    "@storybook/addon-actions": "^6.1.10",
    "@storybook/addon-essentials": "^6.1.10",
    "@storybook/addon-links": "^6.1.10",
    "@storybook/node-logger": "^6.1.10",
    "@storybook/preset-create-react-app": "^3.1.5",
    "@storybook/react": "^6.1.10",
    "@types/classnames": "^2.2.11",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^16.9.53",
    "@types/react-dom": "^16.9.8",
    "rimraf": "^3.0.2",
    "node-sass": "^4.13.0",
    "react-scripts": "4.0.1",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
	"typescript": "^4.0.3"
  },
  // 说明还需要依赖的版本,但是用户不进行强行安装,以免造成版本冲突
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "resolutions": {
    "@storybook/react/babel-loader": "8.1.0" // 允许您覆盖特定嵌套依赖项的版本。解决 storybook 与 react-scripts 中 babel 依赖冲突 
  }
}

воплощать в жизньnpm publishОтправьте пакет, нажмите, чтобы просмотреть после успешной отправки пакетаLion-Conslance Адрес линии. 发布成功后我们就可以在项目中安装使用自己编写的UIButtonКомпоненты.

использовать

# 新建一个项目
create-react-app lion-design-test

# 安装包
npm install lion-design

# index.js 中引入css文件
import "lion-design/dist/index.css";

# App.js 中使用
import { Button } from "lion-design"
<Button btnType="primary">确定</Button>

image.png

Изучив это, каждый получит общее представление о написании и использовании библиотеки компонентов. Как упоминалось выше, нам также нужна онлайн-документация для пользователей.

Автоматическая публикация онлайн-документов

знакомыйgithubдрузья должны знатьGitHub Pages, он может разместитьgithubСтраница проекта.

Теперь, что я хочу сделать, когда я изменяю документ в проекте, просто выполняюgit pushПосле этого онлайн-документация автоматически обновляется. Как это сделать автоматически? А вот и важная концепция разработки программного обеспечения.CI/CD.

CI/CD

  • CIНепрерывная интеграция, частая интеграция кода в транкmainВетвление, его цель - позволить продукту быстро повторяться, сохраняя при этом качество.
  • CDНепрерывная доставка, непрерывное развертывание, частая доставка новых версий программного обеспечения группе обеспечения качества или пользователям и автоматическое развертывание в рабочей среде после проверки кода.

После краткого понимания концепции, есть онлайнCI/CD ПлатформаTravis, что может помочь нам выполнить эту серию задач автоматизации.

Travis

1. ИспользуйтеgithubРазрешите войти и зарегистрироваться.

2. будет.travis.ymlДобавьте файл в корневой каталог проекта (пожалуйста, удалите закомментированный код)

language: node_js // 部署时使用 node_js 语言
node_js:
  - "stable" // node.js 版本
cache:
  directories:
  - node_modules // node_modules 设置缓存
env:
  - CI=true // 设置环境变量
script:
  - npm run build-storybook // 执行的脚本命令,这里执行的是构建线上文档
deploy: // 自动部署github pages 的配置
  provider: pages
  skip_cleanup: true
  github_token: $github_token
  local_dir: storybook-static // 需部署的文件夹
  on:
    branch: main // 基于main分支进行部署

первый вgithubВ продаже, затем вtravis

image.png[Уведомление]github_token то естьtravis

После завершения настройки выполнитеgit pushЭти автоматизированные процессы запускаются, и в конечном итоге документация библиотеки компонентов обновляется.

travisАнализ рабочего процесса:

  1. ПолучатьgitПоследний код для соответствующей ветки (Main).
  2. Используйте настроенныйnode.jsисполнение версииyarn installкоманда (если в проекте естьyarn.lock) генерироватьnode_modules.
  3. правильноnode_modulesкешировать.
  4. npm run build-storybookКоманда генерировать документы библиотеки компонентов.
  5. deploygithub pagesОб использовании развертыванияtokenТокенgithub_token, Развертывание контентаstorybook-static папка.

ЭтоtravisАвтоматизируйте процесс развертывания. Конечно, его возможности гораздо больше.

Ассортимент библиотеки компонентов Онлайн адрес документа

Просмотрите полный код в этой сводке

резюме

В этой статье шаг за шагом создается базовая структура библиотеки компонентов и пишетсяButtonКомпонент как простой каркас библиотеки компонентов, опубликованный в Интернете.npmpackage, а также публикует онлайн-документацию для библиотеки компонентов.

Благодаря изучению и саморазвитию этой статьи я считаю, что вы, по крайней мере, создали и выпустили библиотеку компонентов. Так что просто сделать это точно недостаточно.Ввиду ограниченности места автор напишет его в двух частях.Основой второй части является некоторое обдумывание и написание самого общего компонента.

如果喜欢本文,请点个赞吧! ! !