از React تا گوگل - قسمت سوم

خوب قسمت قبل رو که یادتونه؟

https://virgool.io/JavaScript8/%D8%A7%D8%B2-react-%D8%AA%D8%A7-%DA%AF%D9%88%DA%AF%D9%84-%D9%82%D8%B3%D9%85%D8%AA-%D8%AF%D9%88%D9%85-kygfnxk17ff9

توی قسمت قبل من اومدم گفتم که از preRender استفاده کردم به اسم react-snap .

خداییش هم کارش خوبه . فقط چندتا مشکل داره .

مشکل اولش اینه که هرجا که clone کنم پروژه رو ، باید حجم زیادی chromless دانلود کنم . مشکل بعدی اینه که فرض کنین من چنین آدرسی توی سایتم دارم :‌ site.com/ایران-الفبا-ری-اکت-ویرگول-نودجی-اس

خوب این آدرس طولانی و فارسیه . وقتی که اینکد میشه ( آخه react-snap میاد برای هر آدرس شما یک فولدر میسازه و کل سایت رو کرول میکنه و استاتیک تحویل میده . اسم هر فولدر هم میشه آدرس صفحه ای که توشه ) :

%D8%A7%DB%8C%D8%B1%D8%A7%D9%86-%D8%A7%D9%84%D9%81%D8%A8%D8%A7-%D8%B1%DB%8C-%D8%A7%DA%A9%D8%AA-%D9%88%DB%8C%D8%B1%DA%AF%D9%88%D9%84-%D9%86%D9%88%D8%AF%D8%AC%DB%8C-%D8%A7%D8%B3

خوب چنین فولدری نمیتونین بسازید . حداقل Nodejs با پلاگین mkdirp نمیسازه .

تا اینکه زیر پست دومی که گذاشتم ، دوستی اومد و لینک ویرگول خودش رو داد و راهنمایی کرد .

میتونین کامنتشو بخونین . این هم صفحه مطلب ویرگول ایشون در مورد SSR

https://virgool.io/JavaScript8/%D9%85%D8%AB%D8%A7%D9%84-%DA%A9%D8%A7%D9%85%D9%84%DB%8C-%D8%A7%D8%B2-%D9%BE%DB%8C%D8%A7%D8%AF%D9%87-%D8%B3%D8%A7%D8%B2%DB%8C-%DB%8C%DA%A9-%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87-%D9%88%D8%A8%DB%8C-%D9%BE%D8%A7%DB%8C%D9%87-%D8%A8%D8%B1-react-efebuxevlbih

خوب .

رفتم داخل مطلبشون .

اما من مشکل خیلی بدی داشتم .

  • من از Material-ui استفاده کردم .
  • لوکال استوریج و window رو نمیشه خارج از componentDidMount پارس کرد . چرا؟ چون که شما داخل server دسترسی به window و یا localStorage ندارید منطقا . چون این متغیرها ، برای مرورگر تعریف میشن ، نه سرور .
  • پروژه زیاد بود حجمش و تقریبا 20 رو کد نویسی کرده بودم روش
  • کار با وب پک کمی گنگ بود
  • چجوری Deploy کنم؟

یکی یکی جواب اینارو میدم .

خوب Material-ui خودش گفته باید چی کارکنیم . این لینک رو بخونید :

https://material-ui.com/guides/server-rendering/

خوب فقط این که نیست .

کلا چند مرحله هست که باید با وب پک انجام بشه :

بارگذاری css , بارگذاری js , بارگذاری assets مثل تصاویر و فونت ها , ایجاد یک وب سرور برای ارسال درخواست ها , ایجاد routing ها خوب خیلی اتفاقات بدی میفته ،که من فقط روی کد دوست عزیزمون مینویسم .

یعنی تغییراتیه که من اعمال کردم و کار کرد . ( شاید شما اعمال نکنید هم کار میکنه )

خوب این از Webpack

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: [
                {
                    loader: 'babel-loader',
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules\/)/,
                    query: {
                        presets: ['react', 'es2015'],
                        plugins: ['transform-class-properties'],
                        "env": {
                            // only enable it when process.env.NODE_ENV is 'development' or undefined
                            "development": {
                                "plugins": [["react-transform", {
                                    "transforms": [{
                                        "transform": "react-transform-hmr",
                                        // if you use React Native, pass "react-native" instead:
                                        "imports": ["react"],
                                        // this is important for Webpack HMR:
                                        "locals": ["module"]
                                    }]
                                    // note: you can put more transforms into array
                                    // this is just one of them!
                                }]]
                            }
                        }
                    }
                },
                {
                    test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
                    use: [{
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }]
                },
                {
                    test: /\.(pdf|jpg|png|gif|svg|ico)$/,
                    use: [
                        {
                            loader: 'url-loader'
                        },
                    ]
                }
                ,
                {
                    test: /\.css$/,
                    use: ['style-loader', 'css-loader']
                }
            ],
        },
        plugins: [
            new ExtractTextPlugin({
                filename: 'style.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: [
                {
                    loader: 'babel-loader',
                    test: /\.(js|jsx)$/,
                    exclude: /(node_modules\/)/,
                    query: {
                        presets: ['react', 'es2015'],
                        plugins: ['transform-class-properties'],
                        "env": {
                            // only enable it when process.env.NODE_ENV is 'development' or undefined
                            "development": {
                                "plugins": [["react-transform", {
                                    "transforms": [{
                                        "transform": "react-transform-hmr",
                                        // if you use React Native, pass "react-native" instead:
                                        "imports": ["react"],
                                        // this is important for Webpack HMR:
                                        "locals": ["module"]
                                    }]
                                    // note: you can put more transforms into array
                                    // this is just one of them!
                                }]]
                            }
                        }
                    }
                },
                {
                    test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
                    use: [{
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }]
                },
                {
                    test: /\.(pdf|jpg|png|gif|svg|ico)$/,
                    use: [
                        {
                            loader: 'url-loader'
                        },
                    ]
                },
                {
                    test: /\.css$/,
                    use: ['style-loader', 'css-loader']
                }
            ],
        },
        plugins: [
            new ExtractTextPlugin({
                filename: 'style.css',
                allChunks: true
            })
        ]
    }
];

خیلی از کدها رو دوست عزیزمون نوشتن . من تغییراتی دادم داخل بارگذاری تصاویر و فونت ها و محیط production در بخش js .

حالا میرسیم به رفع مشکلات window و localstorage.

من توی بخش production.js تغییرات زیادی دادم تا تونستم استفاده کنم . یک سری پلاگین ها نصب کردم و مشکلم رفع شد . البته تا پیدا کردم و فهمیدم چی به چیه ، زمان برد .

const express = require('express');
const path = require('path');
const app = express();
const localStorage=require('localStorage')
const Window = require('window');
const { document } = new Window();

global.window = new Window();
global.document = document;
global.navigator=window.navigator;
global.localStorage=localStorage;


require('matchmedia-polyfill');
require('matchmedia-polyfill/matchMedia.addListener');

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 = 2020;

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

        return console.error(error);

    } else {

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

یک پلاگین هم نصب کردم و داخل کدهای اصلی پروژه ، هرجا window داشتم این رو گذاشتم . به این صفحه برید و ببینید چقدر باحاله :

https://www.npmjs.com/package/window-or-global

طبق گقته دوستمون برای نگه داری و Deploy از pm2 استفاده کردم و توصیه میکنم شما هم اینکارو کنید .

کار باهاش خیلی جذابه . اگر کسی با داکر و docker-machine کار کرده باشه ، بعضی از کارایی های pm2 رو به خوبی میشناسه . مثل بخش cluster کردنش .

آخرین نکنته اینکه ، ممکنه development و production با هم تفاوت داشتن باشن موقع کار . به همین دلیل من هر دو رو یکی کردم توی webpack تا خروجی یکسان داشته باشم .

اگر با pm2 کار میکنین حتما اول npm run prod رو بزنید و بعدش pm2 reload . اینجوری نیاز نیست که pm2 رو kill کنید .

با مراحل بالا من کامل تونستم ssr کنم و از نتیجه و پرفورمنس خیلی راضیم .

نسبت به مقاله اولی که در مورد ری اکت و گوگل نوشتم ، خیلی تغییر کردم و معلوماتم تغییر کرد .

صد در صد در مورد این مقاله هم همینه .

یک مورد هم یادم رفت . اینکه server.js هم باید تغییر میدادم برای متریال .


import React from 'react';
import ReactDOMServer from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from "react-helmet";
import App from './App';
import JssProvider from 'react-jss/lib/JssProvider';
import { SheetsRegistry} from 'jss';
import Template from './Template';
import {
    MuiThemeProvider,
    createMuiTheme,
    createGenerateClassName,
} from '@material-ui/core/styles';

Helmet.canUseDOM = false;

export default function serverRenderer({clientStats, serverStats}) {
    return (req, res, next) => {
        const sheetsRegistry = new SheetsRegistry();

        // Create a sheetsManager instance.
        const sheetsManager = new Map();

        // Create a theme instance.
        const theme = createMuiTheme({
            direction: 'rtl',
            typography: {
                "fontFamily": "IRYekan",
            },
            palette: {
                primary: {
                    light: '#B0BEC5',
                    main: '#78909C',
                    dark: '#212121',
                    contrastText: '#ffffff',
                },
                secondary: {
                    light: '#F8BBD0',
                    // main: '#F06292',
                    // dark: '#C2185B',
                    main: '#555555',
                    dark: '#aa2712',
                    contrastText: '#ffffff',
                }
            },
            overrides: {
                MuiBottomNavigation: {
                    root: { // Name of the rule
                        bottom: 0,
                        left: 0,
                        right: 0,
                        // position: "sticky",
                        position: "fixed",
                        boxShadow: "0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12)"
                    },
                },
                MuiBottomNavigationAction: {
                    root: { // Name of the rule
                        minWidth: "auto",
                        // left: 0,
                        // right: 0,
                        // position: "fixed",
                    },
                },
                MuiAvatar: {
                    root: {
                        marginRight: 'auto',
                        marginLeft: '16px',
                        borderRadius: '2px'
                    }
                },
                MuiCardHeader: {
                    action: {
                        marginRight: "auto",
                        marginLeft: "-16px"
                    },
                    avatar: {
                        marginRight: "auto",
                        marginLeft: "16px"
                    }
                },
                MuiExpansionPanelSummary: {
                    root: {
                        background: "rgba(0,0,0,.05)"
                    },
                    expandIcon: {
                        left: "8px",
                        right: "auto",
                    },
                    content: {
                        display: 'flex',
                        alignItems: 'center',
                        margin: 0
                    },
                    expanded: {
                        margin: "0 !important"
                    }
                },
                MuiExpansionPanelDetails: {
                    root: {
                        padding: 0
                    }
                },
                MuiList: {
                    padding: {
                        padding: '0!important'
                    }
                },
                MuiListItem: {
                    root: {
                        textAlign: "right"
                    }
                },
                MuiListItemText: {
                    root: {
                        padding: 0
                    }
                },
                MuiTableCell: {
                    root: {
                        padding: "2px",
                        textAlign: 'center'
                    }
                },
                MuiListItemIcon: {
                    root: {
                        marginRight: 'auto',
                        marginLeft: '16px'
                    }
                },
                MuiFormControlLabel: {
                    root: {
                        marginLeft: '16px',
                        marginRight: '-14px'
                    }
                }
            }
        });
        // Create a new class name generator.
        const generateClassName = createGenerateClassName();

        const context = {};
        const markup = ReactDOMServer.renderToString(
            <StaticRouter location={req.url} context={context}>
                <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
                    <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}>
                        <App/>
                    </MuiThemeProvider>
                </JssProvider>
            </StaticRouter>
        );
        const helmet = Helmet.renderStatic();
        const css = sheetsRegistry.toString();

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

بخشی که نوشتم canUseDom=false مرتبط با این ایشو هست :

https://github.com/nfl/react-helmet/issues/195

باقی بخش ها هم تنظیمات متریال هست که داخل سایتش گفته

حتما با راه کارهای بهتر و اتفاقات جالبتر میام و مجدد تجربیاتم رو به اشتراک میزارم .

در آخر اگر مطلبم مفید بود یک فنجون قهوه مهمونم کن . حرف زیاد داریم بزنیم .