用 React 构建可复用的设计系统

React 让 web 开发简化了很多。原则上 React 基于组件的模式让代码分解和复用变得更加容易。 然而,开发者并不总是清楚如何跨项目分享他们的组件。在这片文章中,我会展示几种可用的方法。

React 让书写漂亮,并富有表达力的代码更加容易。然而,如果组件不能很好的复用,随着时间的推移代码变得更加零散和更加难以维护。 我曾经看到的代码库中,同样的 UI 有十几种不同的实现!另外一个问题,开发者通常会把 UI 和业务代码耦合在一起,当 UI 需要改变时就变的很困难。

今天,我们将会看到如何创建可共享的 UI 组件,如何构建贯穿整个应用的一致的设计语言。

开始

一开始你需要一个空的 React 项目。最快捷的方式就是 create-react-app,但是,还是需要设置一下 Sass。 我创建了一个应用框架,你可以在 GitHub 克隆它。你也可以在教程的代码仓中找到完整的项目(https://github.com/tutsplus/build-a-reusable-design-system-with-react)。

运行yarn-install 安装所有的依赖,然后通过 yarn start 启动应用。

所有的视觉组件和相应的样式单独保存在 design_system 目录下。任何全局样式和变量保存在 src/styles

设置设计的基准

最近一次被设计同行鄙视是什么时候,padding 半个像素的错误,或者不能区分各个灰色色调的区别?(我被告知,#eee#efefef 有不同,我打算在一天内找出来)

构建 UI 库其中之一的目的是为了提升设计和开发团队的关系。前端开发者和 API 设计者已经可以很好的沟通并构建很好的 API 协议。 但是,由于某些原因,在跟设计团队沟通时总是逃避。想象一下,对于一个 UI 元素只能存在有限的几个状态。 例如,如果,我们设计一个标题组件,它可以是 h1h6 任何一个标签,可以是粗体、斜体或者有下划线。这个实现起来应该很直接。

网格系统

在着手构建任何设计项目时首先考虑的是需要理解网格是如何构建的。对于很多应用来说,这很随意。这会导致间距系统非常零散,并且开发者很难确定该使用那个间距。 因此需要确定一个合适的间距。当我第一次阅读 4px - 8px 网格系统时就爱上了它。遵守这一规则会简化我们样式的很多问题。

让我们在代码中先设置一个基本的网格系统。我们从设置布局的 app 组件开始。

//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.jsimport 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 中定义样式。

/*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

定义 Type 系统

Type 系统是任何应用的关键组件。通常,我们会定义一个基本的全局样式,在需要的情况下复写它。 这经常会导致设计不一致。让我们看看如何通过设计库来轻松的解决这个问题。

首先,我们会定义一些样式常量和一个 class 容器。

// design-system/type/Type.jsimport 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>};

接下来,我们会为这些 text 元素定义样式。

/* 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 的各个状态。我们可以进一步扩展这个功能来处理交互功能,比如:当文本被省略的时候现实一个 tooltip,或者为 email、time 渲染不同的样式等等。

分子组成原子

目前为止,我们仅创建了 web 应用中最基本的元素,只是这样,它们是没有用的。我们可以在示例的基础上扩展构建一个简单的模态弹窗。

首先,我们定义了模态弹窗的组件类。

// design-system/Portal.jsimport 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 方法类似。在 React 16 有详细介绍。

使用 Modal 组件

现在,组件已经定义好了,让我们看看如何在业务场景中使用它。

//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>}
            //....
        }
    }

我们可以在任何地方使用 modal,然后在调用的地方维护它的状态。很简单,对吧?但是,这有个 bug。关闭按钮无效。这是因为我们构建的所有组件都是一个封闭的系统。 它只会使用需要的 props,并且无视其他的。在当前的示例中,text 组件忽略了 onClick 事件。幸运的是,这很容易被修复。

// In design-system/type/Type.jsexport 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 可以很容易的把剩余的参数以数组的方式提取出来。使用这种方法,然后把参数传递给组件。

分享组件

随着团队的扩大,很难把有效的组件同步给每个人。Storybooks 是一种很好的分享组件的方法。让我们配置一个基础的 storybook。

开始:

npm i -g @storybook/cli 
getstorybook

storybook 还需要一些必要的配置。从这里开启,剩下的设置都很简单。让我们添加一个简单的 story 代表 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 定义了一个新的 story,一般就是你的组件。然后,通过 add 添加新的章节,为了展示组件不同的状态。

当然,这是非常基本的,但是 storybooks 有一些插件可以帮助你添加文档。我还注意到她们也支持 emoji?

原文发布于微信公众号 - 前端达人(frontend84)

原文发表时间:2018-09-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ThoughtWorks

使用Enzyme测试React(Native)组件|洞见

组件化与UI测试 在组件化出现之前,我们不谈UI的单元测试,哪怕是对于UI页面进行测试都是一件非常困难的事情。其实组件化并不完全是为了复用,很多情况下也恰恰是为...

3164
来自专栏Material Design组件

后台系统设计(上篇:选择)

在单个选项下,存在多组互斥选项,且互斥选项组之间存在一定关系,可以考虑混用分段控件和常规按钮,由于分段控件在视觉上占用更大的面积,故给人在层级上更加置前。

4072
来自专栏hightopo

基于 HTML5 的 WebGL 3D 智能楼宇监控系统

2973
来自专栏Modeng的专栏

Vue一个案例引发的动态组件与全局事件绑定总结

最近在自学 Vue 也了解了一些基本用法,也记录了一些笔记有兴趣的朋友可以去查看我的其他文章,技术这东西真的不能光靠看,看是没有的,你必须要动手实践,只有在实战...

1130
来自专栏阿凯的Excel

巧用格式刷解决合并单元格无法统计问题

最近小编一直在分享Python,有朋友和我聊! 我是谁!我来自哪里!我要去往何处! 好吧,为了能对得起我的名字《阿凯的Excel》 本小编决定每周不少于一...

3696
来自专栏转载gongluck的CSDN博客

MFC ActiveX (ocx)控件的开发

前言 ActiveX是Microsoft对于一系列策略性面向对象程序技术和工具的称呼,其中主要的技术是组件对象模型(COM)。 ActiveX控件是一种实现...

7057
来自专栏咖啡的代码人生

tablecloth 使用笔记

tablecloth.js是一个能够只用寥寥几行代码即可快速美化你页面上HTML代码的jQuery插件。 tablecloth.js自身携带了多种表格风格...

3766
来自专栏Danny的专栏

&nbsp在IE和FireFox中显示不一致

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

973
来自专栏DeveWork

WordPress文章版权保护:复制文字自动添加版权信息

这年头,个人博客抄袭成风;某些博主非常不厚道,常常是原封不动地拿过去,不署名来源是常事,还有更可恨的是说成自己的。本站DeveWork.com 为了不必要的纠纷...

20210
来自专栏知无涯

前端必看!各大浏览器 CSS Hack 收集

37613

扫码关注云+社区

领取腾讯云代金券