آموزش فریمورک React – ساخت یک سیستم طراحی با قابلیت استفاده مجدد


ریاکت (React) تأثیر زیادی بر سادهتر شدن مراحل توسعه وب داشته است. معماری مبتنی بر کامپوننت ریاکت موجب شده است که استفاده از آن برای تجزیه و استفاده مجدد از کد آسان باشد. با این وجود، در تمام موارد برای توسعهدهندگان روشن نیست که چگونه میتوانند کامپوننت های خود را در سراسر پروژه به اشتراک بگذارند. در این نوشته قصد داریم برخی روشهای این کار را به شما نشان بدهیم.
ریاکت امکان نوشتن کدهای زیبا و گویا را آسانتر ساخته است. با این حال بدون وجود الگوهای روشن برای استفاده مجدد از کامپوننت، کد در طی زمان واگرا میشود و نگهداری از آن بسیار دشوار خواهد بود. برخی منابع کد وجود دارند که در آن از یک عنصر UI، دهها پیادهسازی صورت گرفته است! مشکل دیگر این است که در موارد متعدی توسعهدهندگان میل دارند UI را چنان به کارکردهای تجاری گره بزنند که وقتی بعدتر مجبور میشوند تغییراتی در UI ایجاد کنند، به دردسر میافتند. در این نوشته خواهیم دید که چگونه میتوانیم کامپوننت های UI قابل اشتراک ایجاد کنیم و چگونه میتوانیم زبان طراحی منسجمی را در سراسر برنامه داشته باشیم.
سرآغاز
برای شروع به یک پروژه خالی ریاکت نیاز داریم. سریعترین روش برای انجام این کار از طریق create-react-app است؛ اما راهاندازی Saas بدین ترتیب دشواریهایی دارد. ما برنامه پایهای ساختهایم که میتوانید از گیتهاب دانلود کنید. همچنین پروژه نهایی این راهنما را از ریپو گیتهاب مربوطه دانلود کنید.
برای اجرا دستور yarn-install را وارد کنید تا همه وابستگیها دانلود شوند و سپس دستور yarn start را اجرا کنید تا برنامه آغاز شود. همه کامپوننت های ویژوال در پوشه design_system به همراه سبکهای مربوطه ایجاد میشوند. همه سبکها یا متغیرهای عمومی در پوشه src/styles قرار دارند.
راهاندازی کد پایه برای طرحی
آخرین باری که قصد داشتید یک padding را که جای اشتباهی بوده است به اندازه نیم پیکسل جابهجا کنید و یا خواستهاید تفاوت بین رنگهای خاکستری را تشخیص دهید چه زمانی بوده است؟ در این موارد همکاران خود را میبینید که شما را به چشم یک موجود مفلوک مینگرند. البته ما بر این باوریم که بین رنگهای eee# و efefef# تفاوتی وجود دارد و یکی از همین روزها آن را اثبات خواهیم کرد.
یکی از اهداف ساخت یک کتابخانه UI این است که رابطه بین تیم طراحی و توسعه بهبود یابد. توسعهدهندگان فرانتاند (Front-end) اینک مدتی است که با طراحان API هماهنگ شدهاند و به خوبی قراردادهای API را ساختهاند. اما به دلایل مختلف هماهنگسازی تیم طراحی با تیم توسعه هنوز با موانعی مواجه است. اگر به دقت به این رابطه فکر کنید، موارد کاملاً معدودی هستند که یک عنصر UI در آن وجود داشته باشد. برای مثال اگر ما به عنوان یکی از اعضای تیم توسعه بخواهیم یک کامپوننت Heading را طراحی کنیم، این عنوان میتواند چیزی بین h1 تا h6 باشد و به صورت ایتالیک یا زیرخط دار باشد. اما برای این که بتوان چیزی را کدنویسی کرد باید سرراست باشد.
سیستم شبکهبندی (Grid)
نخستین گام پیش از آشنا شدن با پروژه طراحی این راهنما این است که میبایست با چگونگی ساخته شدن شبکهها آشنا باشید. در بسیاری از برنامهها این شبکهها به صورت تصادفی استفاده میشوند. این امر منجر به پراکنده شدن سیستم فضابندی میشود و اندازهگیری فضایی که از سوی سیستم استفاده میشود، برای توسعهدهندگان دشوار خواهد بود. معمولاً توسعهدهندگان از سیستم شبکهبندی 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;
سپس چند کامپوننت سبک و wrapper را تعریف میکنیم.
//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>); };
در نهایت سبکهای CSS را در SCCS تعریف میکنیم.
/*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 استفاده میکنیم، مبنای ما بر روی 4 پیکسل خواهد بود. کامپوننت والد، page است و این کامپوننت است که گردش کار در صفحه را تعیین میکند. عنصر دارای پایینترین سطح یک Box است که تعیین میکند محتوا در یک صفحه چگونه رندر میشود. این Box صرفاً یک div است که میتواند خود را به صورت زمینهای رندر کند.
اینک به یک کامپوننت Container نیاز داریم که چند div موجود را به هم بچسباند. ما flex-box را انتخاب کردهایم، زیرا کامپوننت Flex به صورت خلاقانهای نامگذاری شده است.
تعریف کردن یک سیستم Type
سیستم تایپ، یک کامپوننت حیاتی برای هر برنامهای است. ما معمولاً یک پایه تعریف میکنیم که سبکها و متغیرهای عمومی را در صورت نیاز از طریق آن تعریف میکنیم. این امر غالباً منجر به ناهماهنگی در طراحی میشود. در ادامه میبینیم که چگونه میتوانیم این وضعیت را با اضافه کردن آن به کتابخانه طراحی رفع کنیم.
در ابتدا برخی ثابتهای سبک بندی و یک کلاس پوششی (wrapper) تعریف میکنیم.
// 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 برای متن میتواند باشد. ما میتوانیم این کامپوننت را با برای مدیریت تعاملهای خرد مانند رندر کردن tooltip ها هنگام انتخاب متن یا رندر کردن ناگتهای مختلف برای مواردی خاصی مانند ایمیل، زمان و غیره بسط بیشتری بدهیم.
اتمهایی برای مولکولها
تا به این جای کار ما تنها ابتداییترین اجزایی که میتوانند در یک برنامه وب وجود داشته باشند را ساختهایم و این موارد به خودی خود هیچ کاربردی ندارند. این موارد را از طریق یک مثال برای ساخت یک پنجره (منفرد) model نمونه مورد استفاده قرار میدهیم.
در ابتدا کلاس کامپوننت را برای این پنجره منفرد تعریف میکنیم.
// 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 کامپوننت والد قرار دارند. این متد در ریاکت نسخه 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>} //.... } }
ما میتوانیم از این پنجره مودال در هر جایی استفاده کنیم و حالت (state) را در فراخواننده حفظ کنیم. این روند کاملاً ساده است؛ اما در اینجا یک باگ وجود دارد. دکمه بستن کار نمیکند و دلیل این مسئله آن است که ما همه کامپوننت ها را به صورت یک سیستم بسته طراحی کردهایم. این سیستم مواردی که در اختیار دارد را مصرف کرده و باقی را کنار میگذارد. در این چارچوب کامپوننت متنی، مدیریت رویداد 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 روش مناسبی برای استخراج پارامترهای باقیمانده به صورت یک آرایه دارد. Storybook ها موارد مناسبی برای قابلشناسایی ساختن کامپوننتها محسوب میشوند. در ادامه یک کامپوننت 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 یک استوری جدید تعریف میکند که معمولاً کامپوننتهای شما هستند. سپس میتوانید با استفاده از add یک chapter جدید ایجاد کنید تا حالتهای مختلف این کامپوننت را نشان دهید.
البته این وضعیت یک وضعیت کاملاً ساده است؛ اما storybook ها چند افزونه دارند که به شما کمک میکنند و کارکردهایی به مستندات شما اضافه میکنند. همچنین آنها از ایموجیها ? نیز پشتیبانی میکنند.
ادغام در کتابخانههای طراحی موجود از قبل
طراحی کردن یک سیستم طراحی از صفر نیازمند حجم کار زیادی است و برای برنامههای کوچک چندان معنیدار نیست. اما اگر یک محصول بزرگ طراحی میکنید و نیازمند انعطافپذیری و کنترل زیادی روی آن چیزی که میسازید هستید، راهاندازی یک کتابخانه شخصی برای UI در بلندمدت راهگشا خواهد بود.
شاید به نظر شما نیز کتابخانههای کامپوننت UI مناسبی برای ریاکت وجود ندارد. تجربه کار با react-bootstrap و material-ui (کتابخانهاش برای ریاکت و نه خود فریمورک) عالی نیستند. به جای این که از کل یک کتابخانه UI به طور مجدد استفاده کنید، میتوانید کامپوننت های منفرد را انتخاب کنید و کاربرد مناسبتری داشته باشید. برای نمونه پیادهسازی انتخاب چندگانه (multi-select) یک مسئله پیچیده UI محسوب میشود و حالات بسیار مختلفی وجود دارند که باید در نظر گرفت. در این مورد بهتر است از کتابخانهای مانند React Select یا Select2 استفاده کرد.
البته باید حواستان جمع باشد، زیرا وابستگیهای بیرونی به خصوص در مورد افزونههای UI یک ریسک هستند. API آنها به طور مکرر تغییر مییابد و یا این که ممکن است از سوی دیگر به ندرت تغییر یابند و از این رو قدیمی شوند و از برخی ویژگیهای منسوخشده ریاکت بهره گرفته باشند. این مسئله میتواند برای توانایی تحویل پروژه شما تأثیر بگذارد و هر تغییری را پرهزینه سازد. توصیه میشود که از یک wrapper برای این کتابخانهها استفاده کنید و بدین ترتیب جایگزینی کتابخانه بدون دستکاری بخشهای مختلف برنامه کار آسانی خواهد بود.
نتیجهگیری
در این راهنما برخی روشهای تقسیم برنامه به عناصر ویژوال منفرد و استفاده از آنها مانند بلوکهای لگو برای دستیابی به جلوههای مطلوب را بررسی کردیم. این امر موجب تسهیل استفاده مجدد از کد و قابلیت دستکاری کد میشود و از طرف دیگر نگهداری یک UI منسجم را در سراسر برنامه به امری آسان تبدیل میکند.
اگر به این نوشته علاقهمند بودید، موارد زیر نیز احتمالاً مورد توجه شما قرار خواهند گرفت:
- مجموعه آموزش های طراحی و برنامه نویسی وب
- آموزش فریمورک PHP کدایگنایتر (CodeIgniter)
- ۱۰ کتابخانه و فریمورک جاوا اسکریپت که باید آنها را بشناسید
- ابزارها و راهکارهای مدیریت وبسایتها
- مقایسه Swift و React-Native از فریمورکهای ساخت اپلیکیشن در iOS
- ۳ فریمورک برتر برای ساختن بازیهای اندروید
==