🥒 Пользовательский интерфейс Cucumber: подключаемая библиотека компонентов React.

React.js UI Kit

1. Введение

2018 подходит к концу, в этом году является полным и занятым годом. Однажды в середине года мой мозг горячий, и я вдруг хочу развиватьReactБиблиотека компонентов, раньше я иногда писал маленькие игрушки, поэтому интересно, смогу ли я написать большую игрушку? Я рад, что я не фанат трех минут. Я потратил три месяца, выходные и перерывы на работу, чтобы создать кук -ui в Сюда запишите свой опыт

GITHUB | Официальный сайт

2. Компонентизация

В 2011 году я еще учился в средней школе,TwitterДва босса из-за того, что босс устроил им слишком много работы, много повторяющихся вещей, потому что они слишком ленивы, они случайно разработалиBootstrap, Излишне говорить, хотя я не очень люблю это, но это, несомненно, самая популярная и самая ранняя партия интерфейсных UI-библиотек.Именно в то время я понял, что можноCVВажность программирования по возможности без ББ

Три рамки теперь доминируют в мире, компоненты становятся неотъемлемой частью различныхUIБиблиотека бесконечна, самой популярной являетсяantd, так что думаю позаимствовал (плагиат) волну и начал работать

3. Создайте проект

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/26/167e99f8ca02460a~tplv-t2oaga2asx-image.image

  • .storebookНекоторая конфигурация storebook
  • componentsУпомянутый antd, где размещены все компоненты
  • scriptsПубликация, упаковка и некоторые связанные сценарии
  • storiesСтатический документ проекта, отвечающий за демонстрацию демо
  • testsсвязанные с тестомsetup

Больше нечего сказать, это все обычные файлы, теперь я должен жаловаться, что для сборки проекта требуется все больше и больше файлов конфигурации.

3.1 Веб-сайт сборки сборника рассказов

Библиотеке компонентов определенно нужен статический веб-сайт, демонстрирующий демонстрацию, напримерКнопка antdЯ сделал сравнение и выбрал более простой.storebookДля создания веб-сайта

import React from "react"
import { configure, addDecorator } from '@storybook/react';
import { name, repository } from "../package.json"
import { withInfo } from '@storybook/addon-info';
import { withNotes } from '@storybook/addon-notes';
import { configureActions } from '@storybook/addon-actions';
import { withOptions } from '@storybook/addon-options';
import { version } from '../package.json'
import '@storybook/addon-console';
import "../components/styles/index.less"
import "../stories/styles/code.less"

function loadStories() {
  // 介绍
  require('../stories/index');
  // 普通
  require('../stories/general');
  // 视听娱乐
  require('../stories/player');
  // 导航
  require('../stories/navigation')
  // 数据录入
  require('../stories/dataEntry');
  // 数据展示
  require('../stories/dataDisplay');
  // 布局
  require('../stories/grid');
  // 操作反馈
  require('../stories/feedback');
  // 其他
  require('../stories/other'); 
}

configureActions({
  depth: 100
})

addDecorator(withInfo({
  header: true,
  maxPropsIntoLine: 100,
  maxPropObjectKeys: 100,
  maxPropArrayLength: 100,
  maxPropStringLength: 100,
}))
addDecorator(withNotes);
addDecorator(withOptions({
  name: `${name} v${version}`,
  url: repository,
  sidebarAnimations: true,
}))

addDecorator(story => <div style={{ padding: "0 60px 50px" }}>{story()}</div>)
configure(loadStories, module);

записыватьstories

import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Button from '../components/button';
import './styles/button.less';

import "../components/button/styles.less";
import { SuccessIcon } from '../components/icon';

storiesOf('普通', module).add(
  'Button 按钮',
  () => (
    <div className="button-example">
      <h2>基本使用</h2>

      <Button onClick={action('clicked')}>默认</Button>
     </div>
  )
)

Переделкаwebpack.config.jsВ принципе сделано, конфигурация не выкладывается, нормальная работа

Теперь посмотрите на эффект

WX20181226-140129@2x.png

Вау, похоже, это так, так мило, хотя я и закончил с несколькими словами, на самом деле я столкнулся с множеством утомительных проблем, когда мастурбировал, таких какwebpack4 babel@7.xа такжеstorybookВерсия несовместимость и т. Д., Различные поискиissueАх, это наконец разрешено

storybookПредоставляет статический плагин для публикации, который решает мою последнюю проблему, опубликованную на github.gh-page, добавьте две строкиnpm scripts

"scripts": {
    "start": "yarn dev",
    "clean": "rimraf dist && rimraf lib",
    "dev": "start-storybook -p 8080 -c .storybook",
    "build:docs": "build-storybook -c .storybook -o .out",
    "pub:docs": "yarn build:docs && storybook-to-ghpages --existing-output-dir=.out",
}
"storybook-deployer": {
    "gitUsername": "cuke-ui",
    "gitEmail": "xx@xx.com",
    "commitMessage": "docs: deploy docs"
},

затем беги

yarn pub:docs

Принцип очень простой, первый проходwebpackупакуйте документ, затемgit add .потомpushкогда удаленноgh-pagesветвь,

в состоянии пройтиrepo => Setting => Github PagesПосмотреть развернутый в данный момент статический веб-сайт

WX20181226-140129@2x.png

3.2 Начать писать компоненты

Сайт настроен, это равносильно покупке кухонной утвари, можно приступать к готовке, где же овощи?Ну надо свои овощи выращивать, теперь начинаем выращиватьButtonэто блюдо

cd components && mkdir button

существуетcomponentsСоздайте новый в каталогеbuttonсодержание

  • __tests__// тестовое задание
    • index.test.js
  • index.js//запись компонента
  • styles.less// стиль компонента
// index.js

import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import cls from "classnames";

export default class Button extends PureComponent {

    // 具体代码
}

// styles.less

@import "../styles/vars.less";
@import "../styles/animate.less";
@import "../styles/mixins.less";
@prefixCls : cuke-button;
.@{prefixCls} {
  // 具体样式
}
// index.test.js

import React from "react";
import assert from "power-assert";
import { render, shallow } from "enzyme";
import toJson from "enzyme-to-json";
import Button from "../index";

describe("<Button/>", () => {
  it("should render a <Button/> components", () => {
    const wrapper = render(
    <Button>你好</Button>
    )
    expect(toJson(wrapper)).toMatchSnapshot();
  })

Таким образом, компонент написан, мы предполагаем, что пока существует только одна библиотека компонентов.ButtonКомпоненты, осталось последнее, опубликовать вnpm

Пусть пользователь использует это так

import { Button } from "cuke-ui"
import "cuke-ui/dist/cuke-ui.min.css"

ReactDOM.render(
    <Button>你好</Button>,
    document.getElementById('root')
)

3.3 Запись конфигурации упаковки

Обычно библиотека компонентов предоставляет два способа импорта

  1. Упаковано с бабелем
babel components -d lib
  1. Импортировано с помощью тега scriptUMDОбщая спецификация модуля
<link rel="stylesheet" href="https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.css">
<script type="text/javascript" src="https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.js"></script>

Когда я использовал для записи плагинов, я использовал только первый метод, и второй метод должен был поставить код различных проектов с открытым исходным кодом, чтобы узнать, что его можно пропустить черезwebpackПакетumd

// scripts/build.umd.js

const config = {
  mode: "production",
  entry: {
    [name]: ["./components/index.js"]
  },

  //umd 模式打包
  output: {
    library: name,
    libraryTarget: "umd",
    umdNamedDefine: true, // 是否将模块名称作为 AMD 输出的命名空间
    path: path.join(process.cwd(), "dist"),
    filename: "[name].min.js"
  },
  
  ...
}

module.exports = config

использовать здесьwebpack4так что уточнитеmodeДля производственной среды автоматически помогите оптимизировать, сосредоточьтесь на следующемentryа такжеoutput

Найдите запись пакетаcomponnetsпоследующийindex.jsИ войти вdistкаталог, создатьcuke-ui.min.js,

В это время я обнаружил, что нам на самом деле не хватает файла записи

// components/index.js

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

Здесь модуль по умолчанию экспортируется с псевдонимом, преимущество заключается в том, что имя компонента, доступное пользователю, может управляться единообразно.

Наконец мыnpm scriptsДобавьте команду, не нужно каждый раз упаковывать вручную

"clean": "rimraf dist && rimraf lib",
"build": "yarn run clean && yarn build:lib && yarn build:umd && yarn build:css",
"build:css": "cd scripts && gulp",
"build:lib": "babel components -d lib",
"build:umd": "webpack --config ./scripts/build.umd.js",
    
  • cleanЭто делается для того, чтобы каталоги dist и lib не изменялись или не изменялись, удаляйте их перед каждой упаковкой,
  • build:libПакет Babel доesмодуль кlibсодержание
  • build:umdтолько что объяснил

на этот раз беги

yarn build

части, связанные с js, теперь не проблема, и их можно использовать напрямую

import { Button } from './lib'

<script type="module">
import {Button} from "https://unpkg.com/cuke-ui@latest/dist/cuke-ui.min.js"
</script>

В это время вы обнаружите, что по-прежнему не хватает упаковки для css, и вы будете усерднее работать и добавлятьgulpКонфигурация

Эта конфигурация является плагиатомdragon-uiКонфигурация, немного измененная

const path = require('path');
const gulp = require('gulp');
const concat = require('gulp-concat');
const less = require('gulp-less');
const autoprefixer = require('gulp-autoprefixer');
const cssnano = require('gulp-cssnano');
const size = require('gulp-filesize');
const sourcemaps = require('gulp-sourcemaps');
const rename = require('gulp-rename');
const { name } = require('../package.json')
const browserList = [
  "last 2 versions", "Android >= 4.0", "Firefox ESR", "not ie < 9"
]

const DIR = {
  less: path.resolve(__dirname, '../components/**/*.less'),
  buildSrc: [
    path.resolve(__dirname, '../components/**/styles.less'),
    path.resolve(__dirname, '../components/**/index.less'),
  ],
  lib: path.resolve(__dirname, '../lib'),
  dist: path.resolve(__dirname, '../dist'),
};

gulp.task('copyLess', () => {
  return gulp.src(DIR.less)
    .pipe(gulp.dest(DIR.lib));
});

gulp.task('dist', () => {
  return gulp.src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(less({
      outputStyle: 'compressed',
    }))
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(concat(`${name}.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(rename(`${name}.css.map`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))

    .pipe(cssnano())
    .pipe(concat(`${name}.min.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(rename(`${name}.min.css.map`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist));
});

gulp.task('default', ['copyLess', 'dist']);

Этот код находит все меньше файлов под компонентами, сжимает и компилирует их и упаковывает вdistкаталог, сгенерироватьcuke-ui.min.cssдокумент

4. Опубликуйте компонент

Я думаю, что все знают, как публиковатьnpmПакет здесь повторяться не будет, наверное вставьте код

// package.json
 "name": "cuke-ui",
  "version": "1.2.1",
  "main": "lib/index.js",
  "description": "A React.js UI components for Web",
  "repository": "https://github.com/cuke-ui/cuke-ui.git",
  "homepage": "https://cuke-ui.github.io/cuke-ui-landing/",
  "author": "Jinke.Li <jkli@thoughtWorks.com>",
  "license": "MIT",
  "private": false,
  "files": [
    "lib",
    "dist",
    "LICENSE"
  ],
  "scripts": {
    "prepublish": "yarn build"
    }

Указывает, что корневой каталог библиотекиlib/index.js

когда пользовательyarn add cuke-uiпосле использования

import {Button} from 'cuke-ui'

можно понимать как соответствующий

import {Button} from './node_modules/cuke-ui/lib/index.js'

После написания соответствующего описания его можно опубликовать

npm publish .

Если это бета-версия, добавьте ее--tagТолько что

npm publish . --tag=next

5. Напишите остальные компоненты

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

5.1 Компонент класса подсказки сообщения

message, notification

Идеальное состояние — звонить напрямую через API.

import { message } from 'cuke-ui'
message.success('xxx')

использоватьclass staticСтатические свойства упрощают

static renderElement = (type, title, duration, onClose, darkTheme) => {
    const container = document.createElement("div");
    const currentNode = document.body.appendChild(container);
    const _message = ReactDOM.render(
      <Message
        type={type}
        title={title}
        darkTheme={darkTheme}
        duration={duration}
        onClose={onClose}
      />,
      container
    );
    if (_message) {
      _message._containerRef = container;
      _message._currentNodeRef = currentNode;
      return {
        destroy: _message.destroy
      };
    }
    return {
      destroy: () => {}
    };
  };
  static error(title, duration, onClose, darkTheme) {
    return this.renderElement("error", title, duration, onClose, darkTheme);
  }
  static info(title, duration, onClose, darkTheme) {
    return this.renderElement("info", title, duration, onClose, darkTheme);
  }
  static success(title, duration, onClose, darkTheme) {
    return this.renderElement("success", title, duration, onClose, darkTheme);
  }
  static warning(title, duration, onClose, darkTheme) {
    return this.renderElement("warning", title, duration, onClose, darkTheme);
  }
  static loading(title, duration, onClose, darkTheme) {
    return this.renderElement("loading", title, duration, onClose, darkTheme);
  }

Рассматривайте статический метод каждого класса как API, а затем вызывайтеapi, создайте «div» в теле,ReactDOM.renderметод рендеринга

5.2 Компонент класса всплывающей подсказки

Modal

существуетreact-domпри условииcreatePortalПосле апи писать компоненты поп класса станет очень просто, то есть через так называемый портал, будут монтироваться в тело ниже dom

    return createPortal(
      <>
        <div class="mask"/>
        <div class="modal"/>
      </>,
      document.body
  ) 

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/12/26/167e99f8cc3df6fa~tplv-t2oaga2asx-image.image

Tooltip

TooltipЕсть два варианта реализации, один прямо абсолютно позиционируется на родительском элементе, что уменьшит код расчета, но принесет проблему

     <span
      ref={this.triggerWrapper}
      className={cls(`${prefixCls}-trigger-wrapper`)}
    >
      {this.props.children}
    </span>

Если родительский элемент имеетoverflow:hiddenтакие свойства, какtooltipЧасть его может быть перехвачена, поэтому принимается второе решение, которое монтируется наbodyпо

    this.triggerWrapper = React.createRef();
    const {
      width,
      height,
      top,
      left
    } = this.triggerWrapper.current.getBoundingClientRect();

Получите текущую информацию о местоположении, динамически назначьте ее текущемуdiv, и, наконец, привязатьresizeСобытие, решить проблему неправильного положения после смены окна

  componentWillUnmount() {
    window.removeEventListener("click", this.onClickOutsideHandler, false);
    window.removeEventListener("resize", this.onResizeHandler);
    this.closeTimer = undefined;
  }
  componentDidMount() {
    window.addEventListener("click", this.onClickOutsideHandler, false);
    window.addEventListener("resize", this.onResizeHandler);
  }

5.3 Проблема мерцания анимации инициализации

Когда многим компонентам требуется анимация появления и исчезновения, я свяжу два класса, соответствующих анимации появления и исчезновения.

 state = {
    visible: false
 }
 <div
    className={cls(`${prefixCls}-content`, {
      [`${prefixCls}-open`]: visible,
      [`${prefixCls}-close`]: !visible,
      ["cuke-ui-no-animate"]: visible === null
    })}
    ref={this.wrapper}
    style={{
      width,
      left,
      top
    }}
>

// xx.less
  &-open {
    animation: cuke-picker-open @default-transition forwards;
  }
  &-close {
    animation: cuke-picker-close @default-transition forwards;
    pointer-events: none;
  }
  
  .cuke-ui-no-animate {
    animation: none !important;
    }

В это время будет проблема, потому что по умолчанию visiblefalseТаким образом, анимация закрытия будет выполнена, что приведет к мерцанию, поэтому просто инициализируйте и установите состояние наnull, когда null устанавливает css вanimation:noneэто решено

5.4 Единый визуальный стиль

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

//vars.less
@primary-color: #31c27c;
@warning-color: #fca130;
@error-color: #f93e3e;
@success-color: #35C613;
@info-color: #61affe;
@bg-color: #FAFAFA;
@border-color: #e8e8e8;
@label-color: #333;
@default-color: #d9d9d9;
@loading-color: #61affe;
@font-color: rgba(0, 0, 0, .65);
@disabled-color: #f5f5f5;
@disabled-font-color: fade(@font-color, 25%);
@font-size: 14px;
@border-radius: 4px;
@default-shadow: 0 4px 22px 0 rgba(15, 35, 95, 0.12);
@default-section-shadow: 0 1px 4px 0 rgba(15, 35, 95, 0.12);
@default-text-shadow: 0 1px 0 rgba(0, 0, 0, .1);
@picker-offset-top: 5px;
@mask-bg-color: rgba(0, 0, 0, .5);

// 响应式断点
@media-screen-xs-max : 576px;
@mobile: ~ "screen and (max-width: @{media-screen-xs-max})";

//动画时间
@loading-time: 1.5s;
@loading-opacity: .7;
@animate-time : .5s;
@animate-type: cubic-bezier(0.165, 0.84, 0.44, 1);
@animate-type-easy-in-out: cubic-bezier(.9, .25, .08, .83);
@default-transition: @animate-time @animate-type;

5.5 Умелое использование React.cloneElement

При написании компонентов часто необходимо сопоставить проблемы, которые необходимо сопоставить, напримерCollapse

<Collapse rightArrow>
    <Collapse.Item title="1">1</Collapse.Item>
    <Collapse.Item title="2">2</Collapse.Item>
    <Collapse.Item title="3">3</Collapse.Item>
</Collapse>

<Collapse>а также<Collapse.Item>Все компоненты, которые мы предоставляем пользователям, должны использоваться вместе.Например, в приведенном выше примере естьrightArrowатрибут сообщает каждому<Collapse.Item>Стрелки все справа, значит нужно пройтиcloneElementпередать значение дочернему компоненту

// collapse.js

   const items = React.Children.map(children, (element, index) => {
      return React.cloneElement(element, {
        key: index,
        accordion,
        rightArrow,
        activeKey: String(index),
        disabled: element.props.disabled, hideArrow: element.props.hideArrow
      });
    });

Каждый дочерний компонент получает родительский компонентrightArrowПосле атрибута можно установить соответствующий класс, аналогичноRow Col, TimelineВот так это реализовано

Ï

5.6 getDerivedStateFromProps

Во многих подобных компонентах приходится полагаться на определенный атрибут состояния сцены реквизита.

    <Tabs activeKey="1">
      <Tabs.TabPane tab="选项1" key="1">
        1
      </Tabs.TabPane>
      <Tabs.TabPane tab="选项2" key="2">
        2
      </Tabs.TabPane>
      <Tabs.TabPane tab="选项3" key="3">
        3
      </Tabs.TabPane>
    </Tabs>

такие как вышеTabsкомпонент принимаетactiveKeyЧтобы отобразить текущую опцию, компонент может выглядеть так

export default class Steps extends PureComponent {
  state = {
    activeKey: ~~(this.props.activeKey || this.props.defaultActiveKey)
  };
   onTabChange = () => {
     this.setState({ activeKey: key })
    }
  };

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

 static getDerivedStateFromProps({ activeKey }, state) {
    if (activeKey !== state.activeKey) {
      return {
        activeKey
      };
    }
    return null;
  }

6. Используйте antd-landing для создания главной страницы веб-сайта

После непрерывных усилий по преобразованию компоненты почти одинаковы, но все же есть разница, напримерant.design/index-cnТакая крутая домашняя страница, с помощью поиска я нашелantd-landingПеретащите, визуализация построена

landing

Наконец, вам нужно только вручную написать некоторую конфигурацию веб-пакета, упаковать ее и опубликовать наgithub pageТолько что

7. Заключение

Правильно, другой классantdБиблиотека может быть бессмысленной.Благодаря этой библиотеке компонентов я узнал много знаний, с которыми я обычно не могу соприкоснуться, а также я понял тяжелую работу авторов фреймворка и библиотеки.偏右В ожидании звезды большого парня мои коллеги также с большим энтузиазмом помогают мне упомянуть некоторыеBug fixизPR, В любом случае цели обучения в этом году выполнены, и это все еще очень приятно.Мы начнем работать над этим в январе следующего года.nestа такжеflutterДавай, давай, дерзкая свинья

landing

landing