react 在简化 Web 开发方面做了很多工作。React 基于组件的架构原则上使得分解和重用代码变得容易。但是,对于开发人员来说,如何跨项目共享他们的组件并不总是很清楚。在这篇文章中,我将向您展示一些解决此问题的方法。
React 让编写漂亮、富有表现力的代码变得更加容易。但是,如果没有明确的组件重用模式,代码会随着时间的推移而发散,并且变得非常难以维护。我见过代码库,其中相同的 UI 元素有十种不同的实现!另一个问题是,通常情况下,开发人员倾向于将 UI 和业务功能耦合得太紧,并且在 UI 发生变化时会遇到困难。
今天,我们将了解如何创建可共享的 UI 组件,以及如何在您的应用程序中建立一致的设计语言。
入门
你需要一个空的 React 项目才能开始。最快的方法是通过create-react-app,但是使用它来设置 Sass 需要一些努力。我创建了一个骨架应用程序,您可以从GitHub克隆它。您还可以在我们的教程 GitHub 存储库中找到最终项目。
要运行,请执行 ayarn-install
以拉入所有依赖项,然后运行yarn start
以启动应用程序。
所有可视化组件都将 与相应的样式一起位于design_system文件夹下。任何全局样式或变量都将位于src/styles下。
设置设计基线
你最后一次从你的设计同行那里得到一个你死定了的样子是什么时候,因为填充错误了半个像素,或者无法区分各种灰色阴影?#eee
(有人告诉我和之间有区别#efefef
,我打算在这些日子里找出它。)
构建 UI 库的目的之一是改善设计和开发团队之间的关系。前端开发者与api设计师合作有一段时间了,擅长建立 API 合约。但由于某种原因,在与设计团队协调时,它却让我们望而却步。如果你仔细想想,一个 UI 元素只能存在有限数量的状态。例如,如果我们要设计一个 Heading 组件,它可以是介于h1
和之间的任何东西h6
,可以是粗体、斜体或下划线。对此进行编码应该很简单。
网格系统
开始任何设计项目之前的第一步是了解网格的结构。对于许多应用程序,它只是运行dom。这会导致分散的间距系统,并使开发人员很难确定使用哪种间距系统。所以选择一个系统!当我第一次阅读 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
,它控制页面的流程。然后最低级别的元素是 a Box
,它决定了内容在页面中的呈现方式。它只是div
知道如何根据上下文呈现自己。
现在,我们需要一个将多个sContainer
粘合在一起的组件。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 问题,需要考虑大量的场景ios。对于这种情况,使用 React Select 或 Select2 之类的库可能更简单。
不过,请注意。任何外部依赖项,尤其是 UI 插件,都是一种风险。他们必然会经常更改他们的 API,或者在另一个极端,继续使用旧的、已弃用的 React 功能。这可能会影响您的技术交付,并且任何更改都可能代价高昂。我建议在这些库上使用包装器,因此无需触及应用程序的多个部分即可轻松替换库。
结论
在这篇文章中,我向您展示了一些将您的应用程序拆分为原子视觉元素的方法,像乐高积木一样使用它们来实现所需的效果。这有助于代码重用和可维护性,并且可以轻松地在整个应用程序中维护一致的 UI。
- 网格系统
- 定义类型系统
- 原子形成分子
- 使用模态组件