مثال کاملی از پیاده سازی یک برنامه وبی پایه بر React

A perfect React application on the cutting edge of technology
A perfect React application on the cutting edge of technology

-- مقدمه

افرادی که در حوزه وب فعالیت می‌کنن، به راحتی می‌تونن جستجو بزنن و خیلی از موارد مربوط به یک برنامه وبی پایه بر React رو ببینن و نمونه‌ پیاده سازی‌های اون رو داشته باشن، موارد و مفهوم‌های مثل:

  • پایش سمت سرور (Server Side Rendering)
  • استفاده از React Router
  • استفاده از CSS Modules
  • استفاده از PostCSS
  • تنظیمات Webpack
  • نوشتن Express
  • تنظیمات برای جداسازی محیط develop و محیط production
  • ابزارهای قوی برای Deploy برنامه‌های بزرگ
  • مقیاس پذیری یا همون Scale کردن برنامه‌ها
  • برنامه‌های تک صفحه‌ای (Single Page Application)
  • برنامه‌های پراگرسیو (Progressive Web Application)
  • و...

موارد زیاد هستن، و خیلی گیج کننده که گاهی همین زیادی تکنیک‌ها و تکنولوژی‌ها باعث میشه، این‌ها رو خوب کنار هم نگذاریم و چیز خوبی از آب در نیاد. یا اینکه کنار هم می‌گذاریم ولی چنگی به دل خودمون نمی‌زنه، یه چیز‌هایی رو توی سایت‌های شرکت‌های بزرگی مثل Pinterest یا Instagram یا Facebook و غیره، می‌بینیم که برامون سواله که این‌ها چطور عمل می‌کنند و چطور یک برنامه بزرگ React رو با تمام استاندارد‌ها پیکربندی می‌کنن و حجم عظیمی از برنامه نویس‌ها کنار هم بدون داشتن کوچکترین تداخلی کار می‌کنند.

توی این مقاله قصد دارم با یک نگاه ساده یک مثال یا بهتره بگم که یه پلتفورم خوب، کامل و قابل بزرگ شدن در مقیاس‌های عظیم رو قدم به قدم توضیح بدم.

در انتهای این مقاله شما یک ریپوی کامل دارید که می‌تونید یک برنامه React ی خوب و تمیز داشته باشید که شامل خیلی از تکنیک‌ها و تکنولوژی‌های روز و ترند هست. خروجی‌ شما خیلی خیلی شبیه کارهای خوب توی این حوزه خواهد بود و سعی کردم که همه گل‌ها رو یک جا جمع کنم. ولی این دلیلی بر این نیست که شما از اینی که هست نتونید بهترش کنید.

خز؛پن ! (خیلی زیاد؛پس نخون) : اگر فاقد حوصله هستین و دوست دارید نتیجه رو نگاه کنید ریپو رو ببینین و ازش استفاده کنید.

چیزی که من دلم می‌خواد اینه که اگر شما هم ایده یا نگرشی برای بهتر کردن این ریپو دارید، یه سر به Github بزنید، Fork کنید و کارتون رو Pull Request کنید. من و Andrew، کاملا استقبال می‌کنیم.


-- می‌خوایم چی بسازیم؟

اگر یه چرخ بزنید برای دیدن مثال‌های پیاده سازی یک React App موارد زیادی پیدا می‌کنید که هر کدوم ویژگی‌هایی رو دارند، مثل SSR یا استفاده از React Helmet و خیلی چیزهای دیگه اما بیشتر اون‌ها برای اهداف develop نوشته و انتشار داده می‌شن، هیچ کدوم هر دو هدف develop و deploy صحیح رو دنبال نمی‌کنن و استانداردهای این دو محیط رو اصلا بیان نمی‌کنن.

من توی این مقاله می‌خوام هم محیط develop خوبی رو براتون شرح بدم که بدون خون ریزی و با سرعت زیاد کارتون رو انجام بدید هم محیط production رو با حداقل استانداردها داشته باشیم. باز هم تاکید می‌کنم این مقاله فوق العاده ساده هست و موارد زیادی رو من توش نیاوردم. که می‌تونید شما بیارید و در تکمیل این مقاله، مقاله بدید و کد‌ها رو هم به ریپوی اصلی Pull Request بدید و اینطوری باعث پیشرفتش بشید.

-- ابزارها و تکنولوژی‌های مورد استفاده در این مقاله:

  • React
  • React router 4
  • React Helmet
  • CSS Modules
  • PostCSS
  • Webpack 3
  • Babel
  • Express
  • PM2

-- نیازمندی‌ اولیه

در ابتدا شما باید Node نصب داشته باشید، ترجیحاً آخرین نسخه پایدار، که اگر ندارید از سایت نود دانلود و نصبش کنید.


-- خوب حالا چیکار کنیم؟

قدم بعدی اینه که برای برنامه‌ای که قراره بنویسیم یک فولدر بسازیم. از همین اول اتمام حجت کنم. با فولدر بندی، هم کار خودت دسته بندی میشه هم منظم‌تر میشه هم تا درصدی برای مقیاس پذیری داری آماده‌اش می‌کنی. هرکسی در فولدر بندی شیوه‌ای داره من اینطوری هستم که توی این مقاله هست. شما می‌تونی برای خودت اونی که احساس می‌کنی بهتره رو پیاده سازی کنی.

خوب مثلا اسم فولدر اصلیمون رو می‌ذاریم react-example و بعد بسته به سیستم عاملتون با محیط کامندی وارد فولدر مربوطه بشید، برای ویندوزی‌ها cmd یا powershell هست و برای مک و لینوکس هم terminal ، من فرض می‌کنم که وارد شدید، حالا دستور زیر رو تایپ کنید:

npm init

این دستور در هنگام اجرا شدن چندتا سوال ازتون می‌پرسه که پاسخش رو بدید، نگران نباشید این سوال و جواب‌ها برای ساختن فایل package.json هست که به راحتی می‌تونید بعدا اصلاحش کنید، پس اصلا نگران نباشید. یک سری سوالاتش رو هم که حال نکردید اصلا جواب ندید و کلید اینتر رو بزنید که سریع‌تر از این مرحله خلاص بشیم.

خوب داره جالب میشه، من خودم خیلی ذوق دارم، حالا دستور زیر رو بزنید:

 npm install --save react@16.3.2 react-dom@16.3.2

در واقع شما با دستور بالا دو dependency اصلی این مقاله رو نصب کردید، فولدری کنار فایل package.json ساخته میشه به نام node_modules که این dependency ها و الباقی موارد رو اونجا نگه می‌داره که شما خیلی تحویلشون نگیرید، فعلا توی اون مراحل نیستیم که به این موارد اهمیت بدیم. اون عدد‌ها هم نسخه‌هایی هست که من دارم باهاشون کار می‌کنم و تاکید می‌کنم شما هم از همین نسخه استفاده کنید، چون این موارد خیلی زود بروز می‌شن و اگر شما خیلی دیر این مقاله رو بخونید ممکنه نسخه‌های جدید مشکلاتی ایجاد که عملا این مقاله رو بدرد نخور می‌کنه، برای همین شما با نسخه‌ای که من وارد می‌کنم نصب کنید که اجرای موفقی رو داشته باشید و بعدش اگر دلتون خواست به نسخه‌های جدیدتر بروزرسانی کنید، این کار رو خودم کردم، Andrew زمانی که این استک رو نوشت داستانش فرق داشت، خیلی چیز‌ها رو نداشت و از نسخه‌های پایین‌تری استفاده می‌کرد. من وقتی که مقاله‌اش رو خوندم حس کردم یه سری چیز‌ها می‌تونه بروزتر بشه و همین کار رو هم کردم و با ویرایش‌های دیگه بهش PR دادم و این مثال خوب رو پیشرفت دادم.

میریم مرحله بعدی، خوب دستور زیر رو بزنید:

 npm install --save-dev babel-loader@7.0.0 babel-core@6.24.1 webpack@3.11.0

این‌ها هم dependency های این پروژه هستند منتهی برای محیط ساختن یا همون develop برای همینه که بعد دستور install از فرمان save-dev استفاده کردم، بعدا که برید فایل package.json رو بخونید متوجه میشید چطوری دارن جدا میشن و در آینده میگم که اصلا این نوع سوا کردنشون یعنی چی.

خوب حالا نوبت این شده که Babel رو نصب کنیم، سوال اینه که این چیه؟ ساده بگم که قراره شما کد‌های خیلی خیلی آپدیتی بزنید و از ویژگی‌های جدید جاوااسکریپت استفاده کنید ولی قرار نیست وقتی خروجی می‌گیرید و برنامه رو برای deploy آماده می‌کنید مرورگرها هم مثل شما خفن باشن، ممکنه مردم از مرورگرهای زمان قلی قلی میرزا استفاده کنن پس باید هوای اون‌ها رو هم داشته باشیم. Babel به شما اجازه میده خفن باشید و خفن کد بزنید ولی در نهایت زمانی که Webpack برای شما خروجی می‌سازه، خروجی‌ای میده بیرون که هر مرورگر پیرپاتال یا جوونی بفهمه. پس حالا کد زیر رو بزنید:

npm install --save-dev babel-preset-es2015@6.24.1 babel-preset-react@6.24.1 babel-preset-env@1.5.1 babel-preset-stage-0@6.24.1

این‌ها dependency های مورد نیاز بری تنظیم Babel توی این مقاله هستش، بسیاری پلاگین و dependency برای Babel هست که بعدا خودتون با نیاز‌ها و مشکلاتی که براتون پیش میاد باهاشون آشنا میشید، اینجا هم برای حداقلی این‌ها رو گذاشتم. روش استفاده‌اش هم اینه که شما باید یه فایل بسازی به نام .babelrc که متاسفانه بخاطر زبان فارسی نقطه در انتها نمایش داده شده. اصل اسم به صورت زیر هست:

.babelrc

توی این فایل باید تنظیمات Babel رو انجام بدید:

{
  "presets": [
    "env",
    "es2015",
    "react",
    "stage-0"
  ]
}

هر کدوم از این‌ها یک معنایی داره مثلا es2015 برای اینه که کد‌هایی که ES6 ی زده میشه مثل class یا constructor تبدیل میشه به ES5 و این باعث میشه اون مرورگر‌های قدیمی هم بفهمنش، یا مثلا react باعث میشه که کد‌های JSX تبدیل به توابع جاوااسکریپتی React بشن مثل createElement . من بیشتر از توضیح نمی‌دم، می‌تونید در مورد الباقی جستجو و تحقیق کنید که چه به درد می‌خورن یا چه تنظیماتی دارند و چه چیز‌های دیگه‌ای میشه گذاشت، اگر سوال داشتید در همین مقاله در کامنت سوالاتتون رو بپرسید.

-- تنظیمات Webpack

تا الآن همه فایل‌ها و تنظیمات پایه‌ای بودن و همه رو توی روت پروژه یا همون فولدرمون گذاشتم، و از اینجا به بعد می‌خوام که فایل‌ها رو دسته بندی بریم جلو،‌ طوری که هم منظم باشن هم بدونیم چی کجاست. این نوع فولدربندی باعث میشه در آینده برای تغییرات هم راحت‌تر باشید هم به راحتی برنامه‌تون رو به مقیاس بزرگ‌تری ببرید.

همونطور که در بالا گفته بودم ما دوتا محیط داریم، یکی برای ساختن یا develop و یکی هم برای محیط عملیاتی یا production ، پس برای این منظور یک پوشه به نام webpack بسازید و داخلش یک فایلی بسازید به نام webpack.development.config.js و محتوای اون رو اینطوری بنویسید:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

const distDir = path.join(__dirname, '../dist');
const srcDir = path.join(__dirname, '../src');

module.exports = [
    {
        name: 'client',
        target: 'web',
        entry: `${srcDir}/client.jsx`,
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'client.js',
            publicPath: '/dist/',
        },
        resolve: {
            extensions: ['.js', '.jsx']
        },
        devtool: 'source-map',
        module: {
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules[\\\/])/,
                    use: [
                        {
                            loader: 'babel-loader',
                        }
                    ]
                },
                {
                    test: /\.pcss$/,
                    use: ExtractTextPlugin.extract({
                        fallback: 'style-loader',
                        use: [
                            {
                                loader: 'css-loader',
                                options: {
                                    modules: true,
                                    importLoaders: 1,
                                    localIdentName: '[local]',
                                    sourceMap: true,
                                }
                            },
                            {
                                loader: 'postcss-loader',
                                options: {
                                    config: {
                                        path: `${__dirname}/../postcss/postcss.config.js`,
                                    }
                                }
                            }
                        ]
                    })
                },
            ],
        },
        plugins: [
            new ExtractTextPlugin({
                filename: 'styles.css',
                allChunks: true
            })
        ]
    },
    {
        name: 'server',
        target: 'node',
        entry: `${srcDir}/server.jsx`,
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'server.js',
            libraryTarget: 'commonjs2',
            publicPath: '/dist/',
        },
        resolve: {
            extensions: ['.js', '.jsx']
        },
        module: {
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules[\\\/])/,
                    use: [
                        {
                            loader: 'babel-loader',
                        }
                    ]
                },
                {
                    test: /\.pcss$/,
                    use: [
                        {
                            loader: 'isomorphic-style-loader',
                        },
                        {
                            loader: 'css-loader',
                            options: {
                                modules: true,
                                importLoaders: 1,
                                localIdentName: '[local]',
                                sourceMap: false
                            }
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                config: {
                                    path: `${__dirname}/../postcss/postcss.config.js`,
                                }
                            }
                        }
                    ]
                }
            ],
        },
    }
];

در این فایل می‌بینید که قسمت client و server رو جدا کردم و برای هر قسمت تنظیماتی گذاشتم که بعضی‌هاش مشابه هست و بعضی‌هاش فرق داره که اینجا خیلی توضیح نمی‌دونم، چون همینطوری این مقاله طولانی هست و توضیحات هر بخش اون یک مقاله جدا می‌خواد، اگر مایل بودید در کامنت یا توئیت به من امر کنید حتما توضیح می‌دم یا در صورت نیاز مقاله‌ای جدا می‌نویسم.

-- تنظیمات PostCSS

خوب، لازمه بگم که ماجرای این پیش پردازشگر CSS یکم از الباقی دوستانش جداست، شما نه تنها باید خودش رو نصب کنید، بلکه باید پلاگین‌هاش رو هم نصب کنید و اینطوری هی اون رو قوی و قوی‌تر می‌کنید، هرجا احساس کردید کاری رو انجام نمیده جستجو کنید، حتما پلاگین جالبی براش هست، من برای سادگی نصب خودش و حداقل پلاگین‌هاش رو در زیر قرار می‌گذارم:

npm install --save-dev autoprefixer@8.3.0 css-loader@0.28.4 css-mqpacker@6.0.2 isomorphic-style-loader@4.0.0 postcss@6.0.21 postcss-apply@0.10.0 postcss-cssnext@3.1.0 postcss-custom-properties@7.0.0 postcss-extend@1.0.5 postcss-loader@2.1.4 postcss-nested@3.0.0 postcss-nested-ancestors@2.0.0 postcss-partial-import@4.1.0 postcss-scss@1.0.5 style-loader@0.21.0 extract-text-webpack-plugin@3.0.2

داخل روت پروژه فولدری به نام postcss بسازید و داخلش فایلی بسازید به نام postcss.config.js و تنظیمات این CSS Preprocessor محبوب رو در اون قرار بدید، شاید بگید چرا از SCSS استفاده نکردم. اگر در مورد PostCSS یکم مطالعه کنید متوجه می‌شید چه فوایدی نصبت به الباقی هم خانواده‌هاش داره، یکی از فواید خوبش داشتن پلاگینی به نام autoprefixer هست که عملا نیاز شما رو به mixin ها از بین می‌بره و با یک تنظیم ساده در کد زیر می‌تونید سطح پوشش پشتیبانی از نظر CSS رو روی مرورگرها تغییر بدید:

module.exports = {
    ident: 'postcss',
    syntax: 'postcss-scss',
    map: {
        'inline': true,
    },
    plugins: {
        'postcss-partial-import': {
            'prefix': '_',
            'extension': '.pcss',
            'glob': false,
            'path': ['./../src/styles']
        },
        'postcss-nested-ancestors': {},
        'postcss-apply': {},
        'postcss-custom-properties': {},
        'postcss-nested': {},
        'postcss-cssnext': {
            'features': {
                'nesting': false
            },
            'warnForDuplicates': false
        },
        'postcss-extend': {},
        'css-mqpacker': {
            'sort': true
        },
        'autoprefixer': {
            'browsers': ['last 15 versions']
        },
    }
};


-- حالا خود برنامه

خیلی واضحه که برنامه ما ممکنه چندین صفحه داشته باشه و باید چیزی وجود داشته باشه که مسیر این صفحات رو هندل کنه و اینجاست که من دوست عزیزم react-router رو وارد بازی می‌کنم. فقط توجه داشته باشید من می‌خوام از نسخه آخر یا همون ۴ استفاده کنم برای همین با دستور زیر آخرین نسخهء ۴ رو نصب می‌کنم:

 npm install --save react-router-dom@4.2.2

بعد از این کار یک فولدری بسازید به نام src و در اون فایلی به نام client.jsx بسازید و محتویات زیر رو در اون قرار بدید:

import React from 'react';
import {hydrate} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import App from './app/App';

hydrate((
    <BrowserRouter>
        <App/>
    </BrowserRouter>
), document.getElementById('root'));

بعد کنار همین فایلی که ساختید فایلی بسازید به نام server.jsx و کد‌های زیر رو در اون قرار بدید:

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from "react-helmet";
import Template from './app/template';
import App from './app/App';

export default function serverRenderer({clientStats, serverStats}) {
    return (req, res, next) => {
        const context = {};
        const markup = ReactDOMServer.renderToString(
            <StaticRouter location={req.url} context={context}>
                <App/>
            </StaticRouter>
        );
        const helmet = Helmet.renderStatic();

        res.status(200).send(Template({
            markup: markup,
            helmet: helmet,
        }));
    };
}

حالا کنار این‌ها دو پوشه بسازید، توی یکیش فایل‌های اصلی React یا همون برنامه خودمون رو می‌نویسیم و توی یکی دیگه قراره فایل‌های PostCSS یا همون استایل‌های CSS رو قرار بدیم، اولی رو app و دومی رو styles نامگذاری کنید.

-- -- فایل‌های برنامه

داخل فولدر app در ابتدا فایل به نام template.jsx بسازید و تمپلیت اصلی html ی که قراره همه چیز در اون تزریق بشه رو بسازید:

export default ({ markup, helmet }) => {
   return `<!DOCTYPE html>
         <html ${helmet.htmlAttributes.toString()}>
            <head>
               ${helmet.title.toString()}
               ${helmet.meta.toString()}
               ${helmet.link.toString()}              
            </head>
            <body ${helmet.bodyAttributes.toString()}>
               <div id="root">${markup}</div>
               <script src="/dist/client.js" async></script>
            </body>
         </html>`;
};

بعد یک فایلی بسازید به نام App.jsx که این فایلی هست که برنامه‌ اصلی رو داخلش می‌نویسید، من فعلا چیز ساده‌ای می‌نویسم، اما شما به دلخواه خودتون می‌تونید پوشه بندی کنید و یک مدل بزرگ‌تری رو خلق کنید:

import React, {Component} from 'react';

export default class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>                
                <h1>Hello World!</h1>
            </div>
        );
    }
}

یکم جلوتر دوباره سراغ این فایل میام و یکم بهش چیز میز اضافه می‌کنم اما فعلا بذاریم ساده باشه که یه چیز ساده بتونیم توی مرورگر تماشا کنیم که خیالمون راحت باشه همه چیز داره درست کار می‌کنه.

فقط یه چیز خیلی ساده اضافه کنم، react-helmet، این یکی انقدر ساده هستش که نگو، و فوق العاده بدرد بخور، ازوناست که کلی فواید SEOی داره برامون. درواقع قسمت head هر صفحه رو برامون dynamic میسازه و کلی آپشن داره که میشه ازش به راحتی استفاده کرد، برای نصب این از دستور زیر استفاده کنید:

npm install --save react-helmet@5.2.0

فعلا فقط این رو نصب کنید، زمانی که دوباره به فایل App.jsx برگشتیم، خودتون متوجه می‌شید که چه کارهای ساده ولی به درد بوخوری میشه باهاش کرد.

-- -- فایل‌های استایل

داخل فولدر styles یک فایل اصلی بسازید به نام styles.pcss و کنارش یک فولدر بسازید به نام partials و داخلش فایلی اضافه کنید به نام _partial.pcss و محتویات زیر رو در اون کپی کنید:

// styles.pcss
@import "partials/partial";

.component {
   @extend %box;
   color: #2f95ff;
}

.text {
   display: flex;
   @extend %box;
}

.test {
   display: flex;
}

.active {
   color: red;
}

و

// partials/_partial.pcss
%box {
  box-shadow: 0 0 10px 1px #ff6fc3;
}


-- تنظیمات سرور develop

سرور محیط develop ما express.js خواهد بود، در واقع به این صورت هست که ما یک سری middleware استفاده می‌کنیم که زمانی که تغییراتی در فایل‌های PostCSS یا فایل‌های برنامه ایجاد می‌کنیم، به عبارتی داریم develop می‌کنیم، خودش خود به خود فایل‌های نهایی رو بسازه و ما نتیجه آخرین تغییراتمون رو داخل مرورگر ببینیم.

ابتدا باید پکیج‌های dependency رو نصب کنیم:

npm i --save-dev express@4.15.3 webpack-dev-middleware@2.0.6 webpack-hot-middleware@2.22.1 webpack-hot-server-middleware@0.5.0

بعد در روت پروژه یا همون فولدر react-example پوشه‌ای می‌سازیم به نام express و تنظیمات محیط develop رو برای express درش قرار می‌دیم، برای این کار فایلی بسازید به نام development.js و داخلش اینطوری بنویسید:

const express = require('express');
const app = express();
const webpack = require('webpack');
const config = require('./../webpack/webpack.development.config.js');
const compiler = webpack(config);
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackHotServerMiddleware = require('webpack-hot-server-middleware');

app.use(webpackDevMiddleware(compiler, {
    serverSideRender: true,
    publicPath: "/dist/",
}));
app.use(webpackHotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client')));
app.use(webpackHotServerMiddleware(compiler));

const PORT = process.env.PORT || 3000;

app.listen(PORT, error => {
    if (error) {

        return console.error(error);

    } else {

        console.log(`Development Express server running at http://localhost:${PORT}`);
    }
});


-- ببینیم چی شد تا الآن

حالا وقتشه یه تست ریز بریم ببینم تا الآن چیکار کردیم، برای اینکه ببینیم همه چیز رو تا الآن درست به کار گرفتیم یا نه دستور زیر رو در محیط کامندیمون اجرا می‌کنیم:

node ./express/development.js

اگر ویندوز دارید و خطای NODE_ENV رو دیدید اصلا نگران نشید به این آدرس یه سر بزنید، اونجا نوشتم که چیکار باید بکنید، اگر خطا نداد که باید توی محیط کامندیتون جمله زیر رو ببینید:

Development Express server running at http://localhost:3000

اگر آره که نصف راه رو به سلامت رسیدید و می‌تونید مرورگرتون رو با آدرس http://localhost:3000 باز کنید و از شاهکاری که کاشتید لذت ببرید.




-- تنظیمات React Router

تا الآن همه چیز فوق العاده بود. تقریبا کلی چیزای باحال رو کنار هم به درستی گذاشتیم و همه چیز با هم کار کرد. تاکید می‌کنم اگر مشکلی داشتید توی همین مقاله در قسمت کامنت مطرح کنید، دربست در خدمتم.

ولی، این react-router رو فقط نصب کردیم و توی برنامه هنوز نیاوردیمش، اونجا که در مورد فایل App.jsx گفتم که بر میگردم، آهان، الآن وقتشه که برگردیم با هم و اون رو به شکلی یکم کامل‌تر بنویسیم. فایل App.jsx رو با کد‌های زیر دوباره ویرایشش کنید:

import React, {Component} from 'react';
import Helmet from "react-helmet";
import {Switch, Route} from 'react-router-dom';
import {Link, NavLink} from 'react-router-dom';
import styles from '../styles/styles.pcss';

class Menu extends Component {
    render() {
        return (
            <div>
                <ul>
                    <li>
                        <NavLink exact to={'/'} activeClassName={styles.active}>Homepage</NavLink>
                    </li>
                    <li>
                        <NavLink activeClassName={styles.active} to={'/about'}>About</NavLink>
                    </li>
                    <li>
                        <NavLink activeClassName={styles.active} to={'/contact'}>Contact</NavLink>
                    </li>
                </ul>
            </div>
        );
    }
}


class Homepage extends Component {

    render() {
        return (
            <div className={styles.component}>
                <Helmet title="Welcome to our Homepage"/>
                <Menu/>
                <h1>Homepage</h1>
            </div>
        );
    }
}

class About extends Component {
    render() {
        return (
            <div>
                <Helmet title="About us"/>
                <Menu/>
                <h1>About</h1>
            </div>
        );
    }
}

class Contact extends Component {
    render() {
        return (
            <div>
                <Helmet title="Contact us"/>
                <Menu/>
                <h1>Contact</h1>
            </div>
        );
    }
}


export default class App extends Component {

    render() {
        return (
            <div>
                <Helmet
                    htmlAttributes={{lang: "en", amp: undefined}} // amp takes no value
                    titleTemplate="%s | React App"
                    titleAttributes={{itemprop: "name", lang: "en"}}
                    meta={[
                        {name: "description", content: "Server side rendering example"},
                        {name: "viewport", content: "width=device-width, initial-scale=1"},
                    ]}
                    link={[{rel: "stylesheet", href: "/dist/styles.css"}]}
                />
                <Switch>
                    <Route exact path='/' component={Homepage}/>
                    <Route path='/about' component={About}/>
                    <Route path='/contact' component={Contact}/>
                </Switch>
            </div>
        );
    }
}

خوب یه توضیح ریز بدم، ببینید الآن توی این فایل App.jsx جدیدمون سه تا کامپوننت جدید آوردیم به اسم‌های Homepage و About و Contact که در واقع نقش سه صفحه ما رو بازی می‌کنن.

بعدش یه کامپوننتی ساختیم به اسم Menu که اولی همین فایل App.jsx قرارش دادیم و اون رو هم توی هر سه کامپوننت ایمپورت کردیم تا در هر سه صفحه مجزایی که داریم منو هم دیده بشه.

نکته‌ای که وجود داره اینه که دیگه مثل گذشته قرار نیست که کلاس‌های تگ‌ها رو اضافه کنیم. ما دیگه با کلاس شدیم و قراره که با CSS-Modules کار کنیم. در طی روند این مقاله من خیلی ریز تنظیماتش رو وارد کردم و شما فقط نیازه بدونی که توی هر کامپوننتی که مجزاش می‌کنی باید با کد:

 import styles from '../styles/styles.pcss';

استایل‌ها رو مثل یک آبجکت اضافه کنید و توی هر تگ که خواستید کلاس بهش بدید از کد زیر استفاده کنید:

<div className={styles.container}>
    <div className={styles['container-top']}
        example
    </div>
</div>

فکر کنم خیلی واضحه دیگه، توی فایل‌های PostCSS با هر اسمی که خواستید کلاس‌هاتون رو بسازید و اینجا به جای اینکه مستقیم اسمش رو صدا بزید اون رو به صورت فرزندی از آبجکت styles صدا بزنید، این کار در نسخه‌ای که برای deploy آماده می‌کنید یا همون نسخه production اسم کلاس‌ها رو در DOM به هش تبدیل می‌کنه و می‌تونید تا حداقل ۵ حرف ببریدش، اینطوری فایل CSS آخری که می‌سازید فوق العاده فشرده شده خواهد بود. حالا بعدا، زمانی که دارم در مورد تنظیمات Webpack در محیط production صحبت می‌کنم بیشتر باهاش آشنا می‌شید.

نکتهء دیگه‌ای که خیلی عاشقشم دوست خوبم Helmet هست که به وضوح توی کد‌ها دیده میشه، در هر کامپوننت به صورت مجزا وجود داره، در کامپوننت root یا اصلی یا همون App کامپوننت هم دیده میشه که تنظیمات کلی‌ای رو داره اجرا می‌کنه. انقدر این تنظیمات ساده و خوانا هست که من اینجا از توضیحش صرف نظر می‌کنم.

حالا برای اینکه دوباره ببینم چیکاره هستیم یکبار دیگه دستور اجرا رو برای develop اجرا می‌کنیم:

node ./express/development.js

به نظر معجزه میاد، آره، شما تونستید یه اسکلت عالی از یک برنامه پایه بر React بسازید که همه چیز‌های عالی رو داره. هر صفحه رو که با فشار منو صدا می‌زنید از سمت سرور داره پویش یا همون render میشه و کاملا هم سمت مرورگر شما dynamic هست. ولی به نظر شما کار ما تمام شده؟

هم آره هم نه، آره: چون ما محیط develop عالی‌ای رو درست کردیم و نه: چون هنوز نمی‌دونیم چطور باید اون رو برای deploy روی سرور آماده کنیم.

-- آماده سازی برای اعزام به محیط عملیاتی

تا الآن هر کار کردیم، برای این بوده که محیط develop خوبی رو داشته باشیم، ولی از الآن به بعد دیگه قراره که تنظیمات متفاوتی برای production قرار بدیم که سه هدف اصلی رو حتما باید در نظر بگیریم:

  1. باندل کردن کل فایل‌های برنامه و فشرده سازی و از بین بردن debuggerها و console.logها و تبدیل به نسخه ES5.1
  2. استخراج فایل styles.css در یک فایل جدا و فشرده سازی و پاک کردن همه کامنت‌ها
  3. ساختن فایل stats.json که درواقع عواملی Webpackی که برای محیط عملیاتی در سرور برای express نیاز هست رو توش نگه می‌داریم و اون‌هایی که برای محیط develop بوده رو دیگه نمی‌خواهیم.

پیش به سوی تنظیمات برای محیط عملیاتی، کد‌های زیر را اجرا می‌کنیم:

npm install --save-dev clean-webpack-plugin@0.1.19 stats-webpack-plugin@0.6.0 optimize-css-assets-webpack-plugin@3.2.0

خوب حالا باید توی فولدر webpack یک فایل دیگه به نام webpack.production.config.js بسازید و داخلش رو با کد‌های زیر پر کنید:

const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

const distDir = path.join(__dirname, '../dist');
const srcDir = path.join(__dirname, '../src');

module.exports = [
    {
        name: 'client',
        target: 'web',
        entry: `${srcDir}/client.jsx`,
        output: {
            path: distDir,
            filename: 'client.js',
            publicPath: distDir,
        },
        resolve: {
            extensions: ['.js', '.jsx']
        },
        module: {
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules[\\\/])/,
                    use: [
                        {
                            loader: 'babel-loader',
                        }
                    ]
                },
                {
                    test: /\.pcss$/,
                    use: ExtractTextPlugin.extract({
                        fallback: 'style-loader',
                        use: [
                            {
                                loader: 'css-loader',
                                options: {
                                    modules: true,
                                    importLoaders: 1,
                                    localIdentName: '[hash:base64:10]',
                                    sourceMap: false,
                                }
                            },
                            {
                                loader: 'postcss-loader',
                                options: {
                                    config: {
                                        path: `${__dirname}/../postcss/postcss.config.js`,
                                    }
                                }
                            }
                        ]
                    })
                }
            ],
        },
        plugins: [
            new ExtractTextPlugin({
                filename: 'styles.css',
                allChunks: true
            }),
            new webpack.DefinePlugin({
                'process.env': {
                    NODE_ENV: '"production"'
                }
            }),
            new CleanWebpackPlugin(distDir),
            new webpack.optimize.UglifyJsPlugin({
                compress: {
                    warnings: false,
                    screw_ie8: true,
                    drop_console: true,
                    drop_debugger: true
                }
            }),
            new webpack.optimize.OccurrenceOrderPlugin(),
        ]
    },
    {
        name: 'server',
        target: 'node',
        entry: `${srcDir}/server.jsx`,
        output: {
            path: distDir,
            filename: 'server.js',
            libraryTarget: 'commonjs2',
            publicPath: distDir,
        },
        resolve: {
            extensions: ['.js', '.jsx']
        },
        module: {
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules[\\\/])/,
                    use: [
                        {
                            loader: 'babel-loader',
                        }
                    ]
                },
                {
                    test: /\.pcss$/,
                    use: [
                        {
                            loader: 'isomorphic-style-loader',
                        },
                        {
                            loader: 'css-loader',
                            options: {
                                modules: true,
                                importLoaders: 1,
                                localIdentName: '[hash:base64:10]',
                                sourceMap: false
                            }
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                config: {
                                    path: `${__dirname}/../postcss/postcss.config.js`,
                                }
                            }
                        }
                    ]
                }
            ],
        },
        plugins: [
            new OptimizeCssAssetsPlugin({
                cssProcessorOptions: {discardComments: {removeAll: true}}
            }),
            new StatsPlugin('stats.json', {
                chunkModules: true,
                modules: true,
                chunks: true,
                exclude: [/node_modules[\\\/]react/],
            }),
        ]
    }
];

و همینطور در فولدر ‌express یک فایل به نام production.js برای محیط production بسازید و کد‌های زیر رو داخلش کپی کنید:

const express = require('express');
const path = require('path');
const app = express();
const ClientStatsPath = path.join(__dirname, './../dist/stats.json');
const ServerRendererPath = path.join(__dirname, './../dist/server.js');
const ServerRenderer = require(ServerRendererPath).default;
const Stats = require(ClientStatsPath);

app.use('/dist', express.static(path.join(__dirname, '../dist')));
app.use(ServerRenderer(Stats));

const PORT = process.env.PORT || 3000;

app.listen(PORT, error => {
    if (error) {

        return console.error(error);

    } else {

        console.log(`Production Express server running at http://localhost:${PORT}`);
    }
});

الآن دیگه باید بتونید با دستور زیر فایل‌هایی رو بسازید که قراره برای محیط عملیاتی به سرور اعزام بشه:

NODE_ENV=production webpack -p --config ./webpack/webpack.production.config.js --progress --profile --colors

مشاهده می‌کنید که فایل‌هایی در فولدر dist ساخته شده و اگر دستور زیر رو در محیط کامندیتون اجرا کنید، شما درواقع محیط عملیاتی رو شبیه سازی کردید. یجورایی چیز نهایی که مردم می‌بینند در واقع همینه:

NODE_ENV=production node ./express/production.js 

اگر الآن روی مرورگر Google Chrome دو افزونه React Developer Tools و Wappalyzer رو داشته باشید می‌بینید که علامت React روش ظاهر شده و مخصوصا افزونه React Developer Tools آبی شده و دیگه قرمز نیست. چون قرمز برای محیط دولوپ هست و آبی یعنی شما نسخه مخصوص production رو بردی روی سرور.

جدا تبریک می‌گم. شما فوق العاده‌ای، چون روند این مقاله در عین اینکه ساده‌ترین شکل رو داشته ولی جزو طولانی‌ترین‌ها و پیچیده‌ترین‌ها بوده. وقتی تا اینجا اومدی جلو یعنی شما بهترینی، مدتی هم میگذره تا شما با معجزه‌ای که کردی بیشتر آشنا بشی و اون رو custom خودتون کنید. مثلا eslint بیارید، از Redux استفاده کنید، از Redux-Saga استفاده کنید، تست، مثل Jest رو اضافه کنید و خیلی چیز‌های باحال دیگه.


-- اجرا روی سرور واقعی

زمانی که همه کارهای بالا رو انجام دادید باید به نفر DevOps تون بگید که روی سرور به pm2 نیاز دارید. چون برنامه‌های خفن Reactی که کاربرهای زیادی داره رو با دستور بالا روی سرور اصلی اجرا نمی‌کنن، برای این کار باید روی سرور اصلی‌تون دستور زیر رو اجرا کنید:

npm install pm2 -g

و برای اجرا:

NODE_ENV=production pm2 start ./express/production.js 


البته بگما، این کار رو روی کامپیوتری که تا الآن هم استفاده کردید می‌تونید استفاده کنید، هیچ اشکالی نداره، ببینین که چه شکلیه! ولی این خروجی و نمایشتون روی مرورگر اصلا با

 node ./express/production.js

فرقی نداره، تفاوت‌هاش رو نفر DevOps میدونه که قراره مدیریت کش و لودبالانس و اینا اضافه کنه. اگر دوست داشتید می‌تونید که داک pm2 رو بخونید.

-- اختتام

برای راحتی کار دولوپ، من دستوراتی رو در قسمت scripts در فایل package.json گذاشتم که می‌تونید از آدرس گیت‌هاب که در بالای صفحه گذاشتم استفاده کنید یا همین زیر هم براتون می‌نویسم:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "NODE_ENV=development node ./express/development.js",
  "build": "NODE_ENV=production webpack -p --config ./webpack/webpack.production.config.js --progress --profile --colors",
  "prod": "NODE_ENV=production webpack -p --config ./webpack/webpack.production.config.js --progress --profile --colors && node ./express/production.js",
  "pm2": "NODE_ENV=production pm2 start ./express/production.js"
}

یه توصیه آخر هم از من داشته باشید، درسته که تا الآن داشتیم با npm کار می‌کردیم و الآن با تنظیم بالا مثلا برای شروع develop از دستور npm run dev استفاده می‌کنید ولی سرعت npm خیلی کم هست و زمانی که شما در فایل‌هاتون تغییرات ایجاد می‌کنید تقریبا ۲۷ الی ۳۰ ثانیه طول می‌کشه که براتون سازه جدید رو بسازه و با Hard Reload تغییرات رو ببینید. برای همین من توصیه می‌کنم که yarn رو نصب کنید و با دستور yarn dev شروع به develop کنید، باورتون نمیشه سازه رو در زیر ۵۰۰ میلی ثانیه میسازه.


امیدوارم که تونسته باشم کمکتون کنم، همیشه موفق و شاد باشید.