React为简化Web开发做了很多工作。 React的基于组件的体系结构在原理上使分解和重用代码变得容易。 但是,对于开发人员来说,如何在项目之间共享其组件并不总是很清楚。 在这篇文章中,我将向您展示一些解决方法。
React使编写美观,富有表现力的代码变得更加容易。 但是,如果没有清晰的组件重用模式,代码将随着时间的流逝而发散,并且很难维护。 我见过相同的UI元素有十种不同实现的代码库! 另一个问题是,开发人员往往会过于紧密地将UI和业务功能耦合在一起,并且在UI更改时会遇到麻烦。
今天,我们将看到如何创建可共享的UI组件,以及如何在整个应用程序中建立一致的设计语言。
入门
您需要一个空的React项目才能开始。 最快的方法是通过create-react-app ,但是为此花费一些精力来设置Sass。 我创建了一个骨架应用程序,您可以从GitHub克隆它。 您也可以在我们的教程GitHub repo中找到最终项目 。
要运行,请执行yarn-install
以获取所有依赖项,然后运行yarn start
来yarn start
应用程序。
所有可视组件将与相应的样式一起位于design_system文件夹下。 任何全局样式或变量将在src / styles下 。
设置设计基准
您上次从设计同行那里看到的是我死定了吗,是因为填充错误了半个像素,还是无法区分各种灰度? (有人告诉我#eee
和#efefef
之间有区别,我打算在其中之一找到它。)
建立UI库的目的之一是改善设计与开发团队之间的关系。 前端开发人员已经与API设计人员进行了一段时间的协调,并且擅长建立API合同。 但是由于某些原因,在与设计团队进行协调时我们无法进行。 如果您考虑一下,UI元素只能存在有限数量的状态。例如,如果要设计Heading组件,则它可以是介于h1
和h6
之间的任何值,并且可以是粗体,斜体或下划线。 将其编纂起来应该很简单。
网格系统
着手进行任何设计项目之前的第一步是了解网格的结构。 对于许多应用程序来说,这只是随机的。 这导致分散的间距系统,并使开发人员很难确定要使用哪个间距系统。 所以选择一个系统! 当我第一次阅读它时,我爱上了4px-8px网格系统 。 坚持这样做有助于简化许多样式问题。
让我们从在代码中建立一个基本的网格系统开始。 我们将从设置布局的应用程序组件开始。
//src/App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.scss';
import { Flex, Page, Box, BoxStyle } from './design_system/layouts/Layouts';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Build a design system with React</h1>
</header>
<Page>
<Flex lastElRight={true}>
<Box boxStyle={BoxStyle.doubleSpace} >
A simple flexbox
</Box>
<Box boxStyle={BoxStyle.doubleSpace} >Middle</Box>
<Box fullWidth={false}>and this goes to the right</Box>
</Flex>
</Page>
</div>
);
}
}
export default App;
接下来,我们定义许多样式和包装器组件。
//design-system/layouts/Layout.js
import React from 'react';
import './layout.scss';
export const BoxBorderStyle = {
default: 'ds-box-border--default',
light: 'ds-box-border--light',
thick: 'ds-box-border--thick',
}
export const BoxStyle = {
default: 'ds-box--default',
doubleSpace: 'ds-box--double-space',
noSpace: 'ds-box--no-space'
}
export const Page = ({children, fullWidth=true}) => {
const classNames = `ds-page ${fullWidth ? 'ds-page--fullwidth' : ''}`;
return (<div className={classNames}>
{children}
</div>);
};
export const Flex = ({ children, lastElRight}) => {
const classNames = `flex ${lastElRight ? 'flex-align-right' : ''}`;
return (<div className={classNames}>
{children}
</div>);
};
export const Box = ({
children, borderStyle=BoxBorderStyle.default, boxStyle=BoxStyle.default, fullWidth=true}) => {
const classNames = `ds-box ${borderStyle} ${boxStyle} ${fullWidth ? 'ds-box--fullwidth' : ''}` ;
return (<div className={classNames}>
{children}
</div>);
};
最后,我们将在SCSS中定义CSS样式。
/*design-system/layouts/layout.scss */
@import '../../styles/variables.scss';
$base-padding: $base-px * 2;
.flex {
display: flex;
&.flex-align-right > div:last-child {
margin-left: auto;
}
}
.ds-page {
border: 0px solid #333;
border-left-width: 1px;
border-right-width: 1px;
&:not(.ds-page--fullwidth){
margin: 0 auto;
max-width: 960px;
}
&.ds-page--fullwidth {
max-width: 100%;
margin: 0 $base-px * 10;
}
}
.ds-box {
border-color: #f9f9f9;
border-style: solid;
text-align: left;
&.ds-box--fullwidth {
width: 100%;
}
&.ds-box-border--light {
border: 1px;
}
&.ds-box-border--thick {
border-width: $base-px;
}
&.ds-box--default {
padding: $base-padding;
}
&.ds-box--double-space {
padding: $base-padding * 2;
}
&.ds-box--default--no-space {
padding: 0;
}
}
这里有很多要解压的东西。 让我们从底部开始。 variables.scss是我们定义诸如颜色之类的全局变量并设置网格的地方。 由于我们使用的是4px-8px的网格,因此我们的基准为4px。 父组件是Page
,它控制页面的流程。 然后,最底层的元素是Box
,它确定如何在页面中呈现内容。 只是一个div
,它知道如何在上下文中呈现自己。
现在,我们需要一个Container
组件,它将多个div
粘合在一起。 我们选择了flex-box
,因此选择了富有创意的Flex
组件。
定义类型系统
类型系统是任何应用程序的关键组件。 通常,我们通过全局样式定义基准,并在需要时进行覆盖。 这通常会导致设计不一致。 让我们看看如何通过添加到设计库中轻松解决此问题。
首先,我们将定义一些样式常量和一个包装器类。
// design-system/type/Type.js
import React, { Component } from 'react';
import './type.scss';
export const TextSize = {
default: 'ds-text-size--default',
sm: 'ds-text-size--sm',
lg: 'ds-text-size--lg'
};
export const TextBold = {
default: 'ds-text--default',
semibold: 'ds-text--semibold',
bold: 'ds-text--bold'
};
export const Type = ({tag='span', size=TextSize.default, boldness=TextBold.default, children}) => {
const Tag = `${tag}`;
const classNames = `ds-text ${size} ${boldness}`;
return <Tag className={classNames}>
{children}
</Tag>
};
接下来,我们将定义将用于文本元素CSS样式。
/* design-system/type/type.scss*/
@import '../../styles/variables.scss';
$base-font: $base-px * 4;
.ds-text {
line-height: 1.8em;
&.ds-text-size--default {
font-size: $base-font;
}
&.ds-text-size--sm {
font-size: $base-font - $base-px;
}
&.ds-text-size--lg {
font-size: $base-font + $base-px;
}
&strong, &.ds-text--semibold {
font-weight: 600;
}
&.ds-text--bold {
font-weight: 700;
}
}
这是一个简单的Text
组件,表示文本可以处于的各种UI状态。我们可以进一步扩展该功能,以处理微交互,例如在剪切文本时呈现工具提示,或者为特殊情况(例如电子邮件,时间等)呈现不同的块。
原子形式分子
到目前为止,我们只构建了Web应用程序中可以存在的最基本的元素,它们本身没有用。 让我们通过构建一个简单的模态窗口来扩展此示例。
首先,我们为模态窗口定义组件类。
// design-system/Portal.js
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {Box, Flex} from './layouts/Layouts';
import { Type, TextSize, TextAlign} from './type/Type';
import './portal.scss';
export class Portal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
this.props.root.appendChild(this.el);
}
componentWillUnmount() {
this.props.root.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
export const Modal = ({ children, root, closeModal, header}) => {
return <Portal root={root} className="ds-modal">
<div className="modal-wrapper">
<Box>
<Type tagName="h6" size={TextSize.lg}>{header}</Type>
<Type className="close" onClick={closeModal} align={TextAlign.right}>x</Type>
</Box>
<Box>
{children}
</Box>
</div>
</Portal>
}
接下来,我们可以为模式定义CSS样式。
#modal-root {
.modal-wrapper {
background-color: white;
border-radius: 10px;
max-height: calc(100% - 100px);
max-width: 560px;
width: 100%;
top: 35%;
left: 35%;
right: auto;
bottom: auto;
z-index: 990;
position: absolute;
}
> div {
background-color: transparentize(black, .5);
position: absolute;
z-index: 980;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
.close {
cursor: pointer;
}
}
对于未初始化的对象, createPortal
与render
方法非常相似,不同之处在于它将子对象呈现到父组件的DOM层次结构之外的节点中。 它是在React 16中引入的。
使用模态组件
现在已经定义了组件,让我们看看如何在业务环境中使用它。
//src/App.js
import React, { Component } from 'react';
//...
import { Type, TextBold, TextSize } from './design_system/type/Type';
import { Modal } from './design_system/Portal';
class App extends Component {
constructor() {
super();
this.state = {showModal: false}
}
toggleModal() {
this.setState({ showModal: !this.state.showModal });
}
render() {
//...
<button onClick={this.toggleModal.bind(this)}>
Show Alert
</button>
{this.state.showModal &&
<Modal root={document.getElementById("modal-root")} header="Test Modal" closeModal={this.toggleModal.bind(this)}>
Test rendering
</Modal>}
//....
}
}
我们可以在任何地方使用模式,并在调用者中保持状态。 简单吧? 但是这里有一个错误。 关闭按钮不起作用。 那是因为我们已经将所有组件构建为一个封闭的系统。 它只会消耗所需的道具,而忽略其余的道具。 在这种情况下,文本组件将忽略onClick
事件处理程序。 幸运的是,这很容易解决。
// In design-system/type/Type.js
export const Type = ({ tag = 'span', size= TextSize.default, boldness = TextBold.default, children, className='', align=TextAlign.default, ...rest}) => {
const Tag = `${tag}`;
const classNames = `ds-text ${size} ${boldness} ${align} ${className}`;
return <Tag className={classNames} {...rest}>
{children}
</Tag>
};
ES6有一种方便的方法可以将其余参数提取为数组。 只需将其应用到整个组件即可。
使组件可发现
随着团队规模的扩大,很难使每个人都对可用组件保持同步。 故事书是使组件可被发现的好方法。 让我们建立一个基本的故事书组件。
首先,运行:
npm i -g @storybook/cli
getstorybook
这将为故事书设置所需的配置。 从这里开始,完成其余的设置很容易。 让我们添加一个简单的故事来表示Type
不同状态。
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Type, TextSize, TextBold } from '../design_system/type/Type.js';
storiesOf('Type', module)
.add('default text', () => (
<Type>
Lorem ipsum
</Type>
)).add('bold text', () => (
<Type boldness={TextBold.semibold}>
Lorem ipsum
</Type>
)).add('header text', () => (
<Type size={TextSize.lg}>
Lorem ipsum
</Type>
));
API表面很简单。 storiesOf
定义了一个新故事,通常是您的组件。 然后,您可以使用add
创建新的章节,以展示该组件的不同状态。
当然,这是非常基本的,但是故事书有几个附加组件,可以帮助您为文档添加功能。 我是否提到他们有表情符号支持? 😲
与现成的设计库集成
从头开始设计系统的工作量很大,对于较小的应用程序可能没有意义。 但是,如果您的产品丰富,并且您需要很大的灵活性和对所构建内容的控制,那么设置自己的UI库将在更长的时间内为您提供帮助。
我还没有看到一个很好的React的UI组件库。 我对react-bootstrap和material-ui(React的库,即不是框架本身)的经验不是很好。 与重用整个UI库相比,选择单个组件可能更有意义。 例如,实现多重选择是一个复杂的UI问题,需要考虑大量方案。 对于这种情况,使用像React Select或Select2这样的库可能会更简单。
不过要小心。 任何外部依赖项(尤其是UI插件)都是有风险的。 他们势必会经常更改其API,或者在另一个极端情况下,继续使用React的旧的,过时的功能。 这可能会影响您的技术交付,并且任何更改都可能代价高昂。 我建议对这些库使用包装器,这样就可以很容易地替换库而无需接触应用程序的多个部分。
结论
在这篇文章中,我向您展示了一些将应用程序拆分为原子视觉元素的方法,将它们像乐高积木一样使用以获得理想的效果。 这有利于代码重用和可维护性,并易于在整个应用程序中维护一致的UI。
请在评论部分中分享您对本文的看法!
翻译自: https://code.tutsplus.com/tutorials/build-a-reusable-design-system-with-react--cms-29954