使用React构建可重用的设计系统

React为简化Web开发做了很多工作。 React的基于组件的体系结构在原理上使分解和重用代码变得容易。 但是,对于开发人员来说,如何在项目之间共享其组件并不总是很清楚。 在这篇文章中,我将向您展示一些解决方法。

React使编写美观,富有表现力的代码变得更加容易。 但是,如果没有清晰的组件重用模式,代码将随着时间的流逝而发散,并且很难维护。 我见过相同的UI元素有十种不同实现的代码库! 另一个问题是,开发人员往往会过于紧密地将UI和业务功能耦合在一起,并且在UI更改时会遇到麻烦。

今天,我们将看到如何创建可共享的UI组件,以及如何在整个应用程序中建立一致的设计语言。

入门

您需要一个空的React项目才能开始。 最快的方法是通过create-react-app ,但是为此花费一些精力来设置Sass。 我创建了一个骨架应用程序,您可以从GitHub克隆它。 您也可以在我们的教程GitHub repo中找到最终项目

要运行,请执行yarn-install以获取所有依赖项,然后运行yarn startyarn start应用程序。

所有可视组件将与相应的样式一起位于design_system文件夹下。 任何全局样式或变量将在src / styles下

项目文件夹结构

设置设计基准

您上次从设计同行那里看到的是我死定了吗,是因为填充错误了半个像素,还是无法区分各种灰度? (有人告诉我#eee#efefef之间有区别,我打算在其中之一找到它。)

建立UI库的目的之一是改善设计与开发团队之间的关系。 前端开发人员已经与API设计人员进行了一段时间的协调,并且擅长建立API合同。 但是由于某些原因,在与设计团队进行协调时我们无法进行。 如果您考虑一下,UI元素只能存在有限数量的状态。例如,如果要设计Heading组件,则它可以是介于h1h6之间的任何值,并且可以是粗体,斜体或下划线。 将其编纂起来应该很简单。

网格系统

着手进行任何设计项目之前的第一步是了解网格的结构。 对于许多应用程序来说,这只是随机的。 这导致分散的间距系统,并使开发人员很难确定要使用哪个间距系统。 所以选择一个系统! 当我第一次阅读它时,我爱上了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;
    }
}

对于未初始化的对象, createPortalrender方法非常相似,不同之处在于它将子对象呈现到父组件的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创建新的章节,以展示该组件的不同状态。

一个简单的Type故事书

当然,这是非常基本的,但是故事书有几个附加组件,可以帮助您为文档添加功能。 我是否提到他们有表情符号支持? 😲

与现成的设计库集成

从头开始设计系统的工作量很大,对于较小的应用程序可能没有意义。 但是,如果您的产品丰富,并且您需要很大的灵活性和对所构建内容的控制,那么设置自己的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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值