از 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 ها خوب خیلی اتفاقات بدی میفته ،که من فقط روی کد دوست عزیزمون مینویسم . یعنی تغییراتیه که من اعمال کردم و کار کرد . ( شاید شما اعمال نکنید هم کار میکنه ) خوب این از Webpackconst 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 باقی بخش ها هم تنظیمات متریال هست که داخل سایتش گفته حتما با راه کارهای بهتر و اتفاقات جالبتر میام و مجدد تجربیاتم رو به اشتراک میزارم . در آخر اگر مطلبم مفید بود یک فنجون قهوه مهمونم کن . حرف زیاد داریم بزنیم .