1. Введение
2018 подходит к концу, в этом году является полным и занятым годом. Однажды в середине года мой мозг горячий, и я вдруг хочу развиватьReact
Библиотека компонентов, раньше я иногда писал маленькие игрушки, поэтому интересно, смогу ли я написать большую игрушку? Я рад, что я не фанат трех минут. Я потратил три месяца, выходные и перерывы на работу, чтобы создать кук -ui в Сюда запишите свой опыт
2. Компонентизация
В 2011 году я еще учился в средней школе,Twitter
Два босса из-за того, что босс устроил им слишком много работы, много повторяющихся вещей, потому что они слишком ленивы, они случайно разработалиBootstrap
, Излишне говорить, хотя я не очень люблю это, но это, несомненно, самая популярная и самая ранняя партия интерфейсных UI-библиотек.Именно в то время я понял, что можноCV
Важность программирования по возможности без ББ
Три рамки теперь доминируют в мире, компоненты становятся неотъемлемой частью различныхUI
Библиотека бесконечна, самой популярной являетсяantd
, так что думаю позаимствовал (плагиат) волну и начал работать
3. Создайте проект
-
.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
В принципе сделано, конфигурация не выкладывается, нормальная работа
Теперь посмотрите на эффект
Вау, похоже, это так, так мило, хотя я и закончил с несколькими словами, на самом деле я столкнулся с множеством утомительных проблем, когда мастурбировал, таких как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
Посмотреть развернутый в данный момент статический веб-сайт
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 Запись конфигурации упаковки
Обычно библиотека компонентов предоставляет два способа импорта
- Упаковано с бабелем
babel components -d lib
- Импортировано с помощью тега script
UMD
Общая спецификация модуля
<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. Напишите остальные компоненты
Для других компонентов, хотя их логика отличается, процедура похожа.После моей тяжелой работы я завершил следующие компоненты.Следующие моменты заслуживают упоминания.
- кнопка кнопка
- Оповещение с предупреждением
- хлебные крошки
- Компоновка сетки сетки
- Поле ввода ввода
- Подсказка сообщения
- Модальный диалог
- Постраничный пейджер
- Текстовая подсказка во всплывающей подсказке
- Проигрыватель
- Планшет для рукописного ввода WordPad
- MusicPlayer Отзывчивый музыкальный проигрыватель
- Спиновая загрузка
- Backtop Вернуться к началу
- Индикатор выполнения
- Вкладки
- Количество логотипов на бейджах
- Выпадающее выпадающее меню
- Выдвижной ящик
- Радио кнопка радио
- Контейнер для упаковки контейнеров
- Прикрепите гвозди
- Хронология
- Флажок флажок
- переключатель переключатель
- Ярлык
- CityPicker средство выбора города
- Крах
- Выберите раскрывающийся селектор
- Средство выбора календаря DatePicker
- Окно напоминания об уведомлении
- НомерВведите поле ввода числа
- Ступенчатая полоса
- Загрузить
- Календарь
- всплывающая пузырьковая карта
- Окно подтверждения PopConfirm Bubble
- Карта
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
)
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Перетащите, визуализация построена
Наконец, вам нужно только вручную написать некоторую конфигурацию веб-пакета, упаковать ее и опубликовать наgithub page
Только что
7. Заключение
Правильно, другой классantd
Библиотека может быть бессмысленной.Благодаря этой библиотеке компонентов я узнал много знаний, с которыми я обычно не могу соприкоснуться, а также я понял тяжелую работу авторов фреймворка и библиотеки.偏右
В ожидании звезды большого парня мои коллеги также с большим энтузиазмом помогают мне упомянуть некоторыеBug fix
изPR
, В любом случае цели обучения в этом году выполнены, и это все еще очень приятно.Мы начнем работать над этим в январе следующего года.nest
а такжеflutter
Давай, давай, дерзкая свинья