قد يقرأ الواحد منّا عن رياكت React ولا يترك شرحًا أو توثيقًا إلًا واستوعبه، وقد يتكوّن لديه بذلك رصيد معرفي هائل من هذه التكنولوجيا. لكن لمّا يقرر أن يبدأ العمل بها يتبادر إلى ذهنه السؤال التالي: كيف أبدأ؟ هذه المقالة تجيب عن هذا السؤال من خلال تطبيق مكتسبات رياكت على مثال واقعي وهو تطبيق لتسجيل الجلسات (الزيارات) على موقع ما وعرضها للتشغيل. لن نتطرّق للجانب التقني (أي كيفية تسجيل وتشغيل الجلسات)، وإنما لواجهة المستخدم فقط التي نطوّرها برياكت.

هذه المقالة ليست دليلًا تطبيقيًا خطوة بخطوة ولا تشرح بالتفصيل الأرمِزة (الشِفرات / الأكواد) المعطاة، فأنا أفترض أن القارئ له مستوى يخوّله فهم الأرمزة بمجرد رؤيتها، وهدفي في هذه المقالة تقديم النظرة العامة لكيفية تخطيط وتقسيم التطبيقات لتطويرها برياكت.

التخطيط

يلزم دائمًا تخطيط الواجهة، فمهما كنت تعتقد أنها بسيطة أو بارزة في مخيّلتك، ثق بي يجب دائمًا تخطيطها، أي صنع نموذج أوّلي لها، ولفعل ذلك يمكنك أن تعتمد على أيّ من الأدوات التي ترتاح لها بدءًا من الورقة والقلم وصولًا إلى برامج التخطيط (النمذجة الأولية Prototyping) الكاملة.

شخصيًا فأنا أستخدم أي برنامج رسم توفّر لدي فالأمر لا يحتاج إلى أدوات متقدّمة، مادام البرنامج يستطيع رسم خطوط ومربعات وإضافة صور، فهو كاف بالنسبة إلي. وهذا هو النموذج الأولي الذي قمت بتخطيطه.

لاحظ: الشاشة على اليمين التي تعرض المدونة هي بمثابة الجلسة المسجّلة القابلة لإعادة التشغيل والمشاهدة.

التقسيم إلى مكونات

المرحلة الثانية من ما قبل كتابة الأرمِزة (الشِفرات / الأكواد)، هي تقسيم التخطيط إلى مكوّنات، وهذه العملية ليست موحدة ولا محددة، فهي تخضع لأهواء المبرمج أو المصمم كلّ يقسّمها بالشكل الذي يريد، لكن، إذا كان التقسيم ذكي ومدروس على طبيعة البرنامج، فإن عملية التطوير ستمضي سلسة، وإذا كان “عشوائيًا” أو غير مدروس كفاية فعملية التطوير ستمضي مُزَركَلة (لا أدري ما معنى مزركلة لكنها تبادرت إلي في اللحظة وهي توحي بشيء غير جيد).

فيما يتعلّق بي، فأنا لا أبالغ في التقسيم ولست من الفريق الذي يقول اجعل كلّ شيء مكوّنًا. فبالنسبة إلي المكوّن هو عنصر حي ومسؤول، أي أنه غير ثابت ويتغير مع الزمن ويتفاعل مع أحداث التطبيق و/أو مدخلات المستخدم. ففي التخطيط الذي بين يدينا، لعلّ أحدهم قد يعمد إلى التقسيم أسفله يحيث يبدأ بالمكونات العليا التالية:

  • لوحة الجلسات الجانبية
  • مشغل الجلسات
  • شريط سفلي

تتفرّع عنها المكوّنات الفرعية التالية:

  • لوحة الجلسات الجانبية
    • الترويس: حيث الشعار والعنوان
    • لائحة الجلسات: الجدول الذي يعرض الجلسات المسجلة
      • سطر تسجيل: سطر من هذا الجدول
    • مربع الإحصائيات: مربع واحد من المربعات الأربع أسفل لائحة الجلسات
  • مشغل الجلسات: المشغل حيث تظهر المدونة في التخطيط أعلاه
  • شريط سفلي: الشريط السفلي الذي تظهر فيه معلومات الجلسة جارية التشغيل ومتحكمات التشغيل

لاحظ كيف قام بجعل كلّ شيء ضمن الصفحة كمكوّن أو مكوّن فرعي. لكنّي في هذا أنحو منحى آخر، المكوّنات التي أحددها هي التالية:

  • لائحة الجلسات: أي الجدول الجانبي الذي يعرض لائحة الجلسات المسجلة
    • سطر تسجيل: مكوّن فرعي سطر تسجيل واحد ضمن الجدول
  • مربع إحصاء: مربع واحد لعرض الإحصائيات مثل مجموع الجلسات، متوسط مدد الجلسات..
  • مشغل جلسة: هو المشغل العام على اليمين
  • شريط حالة: الشريط السفلي الذي يضم معلومات الجلسة

لو نظرتَ عن كثب سوف تجد أن المكوّن هو العنصر المستقل الحي المتفاعل على الصفحة، أما غير ذلك من أقسام الصفحة الأخرى مثل الترويس وغيرها فهي ليست مكوّنات وإنما فقط عناصر سيتم عرضها بشكل عادي ضمن الصفحة جنبًا إلى جنب مع المكوّنات. فالمكوّن هو جزء يمكن النظر إليه بشكل مستقل خارج سياق الصفحة، مثل لائحة الجلسات فهي جدول لعرض البيانات يمكن إعادة استخدامه في مشاريع أخرى، كذلك مربع الإحصائيات وشريط الحالة. أما ترويسة الشعار فلا يمكن اعتبارها خارج إطار الصفحة أو المشروع وبالتالي لا داعي للمبالغة وجعلها مكوّن. هذا مذهبي ولا مشكلة في اعتماد الطريقة التي تراها تناسب فلسفتك ونظرتك للأمور، ففي الأخير هذه برمجة، أي حرّية الصنع والتفكير، فلا تجعل أحدًا يقيّدها لك بفرض طريقته عليك.

تحديد الشبكة

مخطط الشبكة الرئيسي

هلمّ بنا نضع المخطط الشبكي Grid Layout للتخطيط السابق الذي أنجزناه، سوف أعتمد من أجل ذلك مكتبة متريال التي تضم إحدى أفضل وأكفأ مكوّنات المخطط الشبكي. وسوف يتم بناء الشبكة على مراحل، المرحلة الأولى تحديد الحاويات الرئيسية المتمثلة في التخطيط التالي:

نعدّل على الملف App.js ونستخدم مكوّن الشبكة (المخطط الشبكي) للوصول برمجيًا إلى نفس النتيجة أعلاه.

import React from 'react';
import logo from './logo.svg';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import './App.css';

const useStyles = makeStyles((theme) => ({
    paper: {
      textAlign: 'center',
      color: theme.palette.text.secondary,
      height: '100%',
      border: '1px solid black'
    },
    fullHeight: {
        height: '99vh'
    }
}));

function App() {
    const classes = useStyles();
    return (
        <Grid container alignItems="stretch" className={classes.fullHeight}>
            <Grid item md={4} xs={12} style={{height: '100%'}}>
                <Paper className={classes.paper}>Side Bar (xs=12, md=4)</Paper>
            </Grid>

            <Grid item container md={8} xs={12} direction="column" className={classes.fullHeight}>
                <Grid item style={{height: '94%'}}>
                    <Paper className={classes.paper}>Player (xs=12, md=8)</Paper>
                </Grid>
                <Grid item style={{height: '6%'}}>
                    <Paper className={classes.paper}>Status Bar (xs=12, md=8)</Paper>
                </Grid>
            </Grid>
        </Grid>
    );
}

export default App;

الرماز أعلاه يعطي النتيجة أسفله، لاحظ أننا استخدمنا المكوّن Paper فقط لإظهار عناصر الشبكة وسيتمّ تعويضه لاحقًا، عدا ذلك فنحن استخدمنا مكوّن الشبكة وبضعة سمات جسس JSS.

مخطط الشريط الجانبي

بعد ذلك، نأتي إلى مخطط الشريط الجانبي الذي يشمل الترويس وقسم جدول التسجيلات، ومربعات الإحصائيات. إذا استكملنا عملنا على التخطيط الذي بدأناه فنحن نريد الوصول إلى النتيجة التالية.

نعدّل على الوظيفة App() ضمن الملف App.js من أجل إدخال الشبكات الجديدة لتنظيم العناصر الجديدة السالف تخطيطها.

function App() {
    const classes = useStyles();
    return (
        <Grid container alignItems="stretch" className={classes.fullHeight}>

            <Grid item container md={4} xs={12} style={{height: '100%'}}>
                <Grid item xs={2} style={{height: '8%'}}>
                    <Paper className={classes.paper}>Logo</Paper>
                </Grid>
                <Grid item xs={10} style={{height: '8%'}}>
                    <Paper className={classes.paper}>Header</Paper>
                </Grid>

                <Grid item xs={12} style={{height: '68%'}}>
                    <Paper className={classes.paper}>Data Table</Paper>
                </Grid>

                <Grid item container xs={12} style={{height: '24%'}}>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 1</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 2</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 3</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 4</Paper>
                    </Grid>
                </Grid>
            </Grid>

            <Grid item container md={8} xs={12} direction="column" className={classes.fullHeight}>
                <Grid item style={{height: '94%'}}>
                    <Paper className={classes.paper}>Player (xs=12, md=8)</Paper>
                </Grid>
                <Grid item style={{height: '6%'}}>
                    <Paper className={classes.paper}>Status Bar (xs=12, md=8)</Paper>
                </Grid>
            </Grid>
        </Grid>
    );
}

النتيجة تصبح كالتالي:

عناصر الصفحة

أقصد بعناصر الصفحة العناصر التي لم نحدّدها كمكوّنات، وفي حالتنا هذه هما عنصرين فقط، الشعار والترويس، سنقوم بإدراجهما بالأسلوب المعتاد ثم أتطرّق للمكوّنات في الفقرة التي تلي.

هذه الوظيفة App() بعد إضافة الشعار والترويس.

function App() {
    const classes = useStyles();
    return (
        <Grid container alignItems="stretch" className={classes.fullHeight}>

            <Grid item container md={4} xs={12} style={{height: '100%', backgroundColor: '#113559'}}>

                <Grid item xs={2} style={{height: '8%'}} className={classes.logo}>
                    <img src={logo} />
                </Grid>

                <Grid item xs={10} style={{height: '8%'}} className={classes.header}>
                    <headText>WATCHBACK</headText>
                    <subHeadText>Session Record & Replay</subHeadText>
                </Grid>

                <Grid item xs={12} style={{height: '68%'}}>
                    <Paper className={classes.paper}>Data Table</Paper>
                </Grid>

                <Grid item container xs={12} style={{height: '24%'}}>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 1</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 2</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 3</Paper>
                    </Grid>
                    <Grid item xs={6}>
                        <Paper className={classes.paper}>Stat 4</Paper>
                    </Grid>
                </Grid>

            </Grid>

            <Grid item container md={8} xs={12} direction="column" className={classes.fullHeight}>
                <Grid item style={{height: '94%'}}>
                    <Paper className={classes.paper}>Player (xs=12, md=8)</Paper>
                </Grid>
                <Grid item style={{height: '6%'}}>
                    <Paper className={classes.paper}>Status Bar (xs=12, md=8)</Paper>
                </Grid>
            </Grid>
        </Grid>
    );
}

الجدير بالذكر أننا جعلنا العنصرين (الشعار والترويس) يبدوان كأنهما مكوّنات، وما تلك سوى خدعة على مستوى جسس JSS الذي قمت بتعديله ليصبح كالتالي:

const useStyles = makeStyles((theme) => ({
    logo: {
        '& img': {
            maxHeight: '90%',
            position: 'relative',
            marginTop: '2px'
        }
    },
    header: {
        paddingLeft: '10px',
        color: 'white',
        '& headText': {
            display: 'block',
            fontSize: '30px',
            fontWeight: 'bold'
        },
        '& subHeadText': {
            display: 'block',
        }
    },
    paper: {
      textAlign: 'center',
      color: theme.palette.text.secondary,
      height: '100%',
      border: '1px solid black'
    },
    fullHeight: {
        height: '99vh'
    }
}));

فمن أجل تفادي كثرة التسميات في جسس JSS، فإنني أعطي اسمًا لكل عنصر شبكة Grid Item، ومن خلاله أصل إلى مختلف العناصر الموجودة ضمنه بواسطة الرمز & لأطبّق عليها الأسلوب المرغوب. فعلى سبيل المثال هنا، قمت بتسمية عنصر الشبكة الذي يضم نصوص الترويسة بـ header، أو بالأحرى أعطيته الصنف header، ومن خلاله وصلت إلى العنصر المسمى headText الذي جعلت خاصية العرض لديه block كي يتصرّف وكأنه قسم div عادي وبذلك يصير الرِماز عندي وكأنه مكوّن وله اسم ذو معنى headText وذلك دون كثير من التعقيد على مستوى جسس مع ربح في المقروئية Readability. وهذه النتيجة:

الجدير بالذكر أن طريقة “شبه المكوّنات” التي بيّنتها أعلاه تعطي تحذيرًا على مستوى المتصفح:

Warning: The tag <subHeadText> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.

لم أكن قد انتبهت لهذا التحذير من قبل، لكن والآن بعد أن رأيته لم أرد حذف ما سبق بيانه كي تعرف أن الأمر ممكن ويعمل بطريقة صحيحة جدًا بغض النظر عن التحذير المذكور. وقد عدّلت الفكرة قليلًا بحيث جعلت العنصرين أقسام div مع إضافة خاصية type ثم الوصول إليها باستخدام جسس. وفي ما يلي التعديل الذي أقدمتُ عليه:

<Grid item xs={10} style={{height: '8%'}} className={classes.header}>
	<div type="HeadText">WATCHBACK</div>
	<div type="SubHeadText">Session Record & Replay</div>
</Grid>

لاحظ كيف أن HeadText و SubHeadText أصبحا خاصيتين ضمن div عوض أن يكونا وسمَين كما سبق. وللوصول إليهما في جسس عدّلتُ كما يلي:

header: {
	paddingLeft: '10px',
	color: 'white',
	'& div[type="HeadText"]': {
		display: 'block',
		fontSize: '30px',
		fontWeight: 'bold'
	},
	'& div[type="subHeadText"]': {
		display: 'block',
	}
},

مكوّن مربّع الإحصائيات

مربّع الإحصائيات هو مكوّن يعرض نصًا يُمثّل نوع الإحصائية، وقيمة تمثل قيمة الإحصائية. والهدف هو صنع مكوّن يمكن استخدامه كما يلي:

<Grid item container xs={12} justify="center" style={{height: '24%'}}>
	<Grid item xs={5} className={classes.statBox}>
		<StatBox text="Today Sessions" value="362"></StatBox>
	</Grid>
	<Grid item xs={5} className={classes.statBox}>
		<StatBox text="Sessions this Week" value="3027"></StatBox>
	</Grid>
	<Grid item xs={5} className={classes.statBox}>
		<StatBox text="Avg Session Length" value="03:12"></StatBox>
	</Grid>
	<Grid item xs={5} className={classes.statBox}>
		<StatBox text="Bounce Rate" value="12%"></StatBox>
	</Grid>
</Grid>

ندرجه باستخدام الوسم StatBox ونعطيه خاصيتين هما نص الإحصائية text و قيمة الإحصائية value. من أجل ذلك قمت بإنشاء ملف جديد أسميته StatBox.js وجعلتُ فيه الرماز التالي:

import React from 'react';
import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
    StatBox: {
        backgroundColor: 'white',
        height: '100%',
        position: 'relative',
        boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)',
        borderRadius: '4px',
        '& div[type="stat-text"]': {
            position: 'absolute',
            bottom: '6px',
            right: '5px',
            fontSize: '14px',
            color: '#113559'
        },
        '& div[type="stat-value"]': {
            position: 'absolute',
            left: '5px',
            top: '2px',
            fontSize: '32px',
            color: '#ee312d'
        }
    }
});

class StatBox extends React.Component {
    
    render() {
        const { classes } = this.props;
        const text = this.props.text;
        const value = this.props.value;

        return (
            <div className={classes.StatBox}>
                <div type="stat-text">{text}</div>
                <div type="stat-value">{value}</div>
            </div>
        )
    }
}

export default withStyles(styles)(StatBox);

لاحظ أن معظم أرمزة المكوّن هي تنسيقات جسس JSS. يليها صنف المكوّن ثم وظيفة التصيير render(). لقد تمّ تحديد خاصيتين props للمكوّن هما text و value. هذا كلّ شيء، النتيجة أمامكم.

مكوّن جدول التسجيلات

أول شيء يلزمنا هو تحديد البيانات التي سيعرضها الجدول، تأتي هذه البيانات طبعا من قاعدة البيانات وتكون على صيغة جيزون، في الوقت الحالي لن نخوض في تفاصيل مُنطلق ومُستقر البيانات وإنما سنحددها يدويًا في الملف App.js:

const SESSIONS = [
    {_id: 12, browser: 'Chrome', os: 'Windows10', location: 'Morocco', length:'13:33', time: moment('2020-02-04T14:48:00', 'YYYY-DD-MMThh:mm:ss').fromNow()},
    {_id: 13, browser: 'Chrome', os: 'Windows10', location: 'Morocco', length:'10:11', time: moment('2020-02-04T15:48:00', 'YYYY-DD-MMThh:mm:ss').fromNow()},
    {_id: 14, browser: 'Chrome', os: 'Windows10', location: 'Morocco', length:'01:12', time: moment('2020-02-04T16:48:00', 'YYYY-DD-MMThh:mm:ss').fromNow()}
];

المكوّن نريد استخدامه على الشاكلة أسفله، نمرر له مصفوفة الجلسات وهو يتكفّل بعرضها:

<DataTable sessions={SESSIONS} />

وصنف الكائن DataTable يضم صنفًا فرعيًا يمثل سطر بيانات هو DataRow. كِلا المكوّنين سأضعهما في ملف واحد سمّيته DataTable.js. لم أرغب في جعل ملف مستقل لكلّ واحد منهما إذ لا معنى لـ DataRow دون DataTable فلا داعي لتخصيص ملفٍ خاص به.

import React from 'react';
import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({

    tableContainer: {
        backgroundColor: 'white',
        boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)',
        borderRadius: '4px',
        height: '100%',
        overflow: 'auto'
    },
    table: {    
        width: '100%',
        '& thead': {
            textAlign: 'left'
        },
        '& thead th': {
            position: 'sticky',
            top: 0,
            backgroundColor: 'white'
        },
        '& td, th': {
            height: '40px',
            borderBottom: '1px solid #ddd',
            cursor: 'pointer'
        },
    }

});

class DataTable extends React.Component {

    render() {
        const { classes } = this.props;
        const dataRows = [];

        this.props.sessions.forEach((session) => {
            dataRows.push(
                <DataRow session={session} />
            );
        });

        return (
            <div className={classes.tableContainer}>
                <table className={classes.table}>
                    <thead>
                        <tr>
                            <th>SESSION TIME</th>
                            <th>SESSION LENGTH</th>
                            <th>COUNTRY</th>
                        </tr>
                    </thead>
                    <tbody>
                        {dataRows}
                    </tbody>
                </table>
            </div>
        );
    }

}

class DataRow extends React.Component {

    render() {
        const session = this.props.session;

        return (
            <tr>
                <td>{session.time}</td>
                <td>{session.length}</td>
                <td>{session.location}</td>
            </tr>
        );
    }

}

export default withStyles(styles)(DataTable);

والنتيجة كالتالي:

خاتمة

لا أريد أن أطيل أكثر من هذا بعد أن تبيّن الرشد من الغي. لقد أعطيتُ طريقة بناء المُكوّنَين الأكثر أهمية في التطبيق، أما مكوّن مشغل الجلسات فموضوعه خارج سياق المقالة، وأما الشريط السفلي فشأنه كشأن سابقيه فلا داعي للخوض فيه هاهنا. لقد كانت هذه المقالة بمثابة ضوء ينير طريق من يريد العمل برياكت لكن لا يعرف من أين يبدأ، أو لمن يريد أن يدرس طرُق العمل عند الآخرين كي يجمع أفضل الممارسات منها، أو ربما لمن يشك في طريقة عمله ويريد أن يستقين هل هو على طريق الصواب أم انحرف عنه. حاصل الأمر أنك تجد على النت مقالات تبسط كيف تفعل كذا وكيف تفعل كذا، لكن قلّما تجد مقالة تعطيك المنهج العام وطريقة التحليل والتخطيط، وهذا ما حاولت تقديمه في مقالتي هذه.

لديك سؤال؟ استيضاح؟ لا تتردد.