Avant vous - React Modern Web App

Avant de commencer à créer à partir de zéro Modern Web App, vous devez comprendre ce qu'est une application Web moderne?


Modern Web App (MWA) est une application qui respecte toutes les normes Web modernes. Parmi eux, l'application Web progressive est la possibilité de télécharger une version de navigateur mobile sur votre téléphone et de l'utiliser comme une application à part entière. C'est aussi l'occasion de faire défiler le site hors ligne à la fois depuis un appareil mobile et depuis un ordinateur; conception de matériaux modernes; optimisation parfaite des moteurs de recherche; et naturellement - une vitesse de téléchargement élevée.



Voici ce qui se passera dans notre MWA (je vous conseille d'utiliser cette navigation sur l'article):



Les gens sur Habré sont des entreprises, alors attrapez immédiatement un lien vers le référentiel GitHub , une archive de chacune des étapes de développement et une démo . Cet article est destiné aux développeurs familiarisés avec node.js et réagissant. Toute la théorie nécessaire est présentée dans le volume nécessaire. Élargissez vos horizons en cliquant sur les liens.


Commençons!



1. Universel


Actions standard: créez un répertoire de travail et exécutez git init . Ouvrez package.json et ajoutez quelques lignes:


 "dependencies": { "@babel/cli": "^7.1.5", "@babel/core": "^7.1.6", "@babel/preset-env": "^7.1.6", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", "babel-loader": "^8.0.4", "babel-plugin-root-import": "^6.1.0", "express": "^4.16.4", "react": "^16.6.3", "react-dom": "^16.6.3", "react-helmet": "^5.2.0", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "webpack": "^4.26.1", "webpack-cli": "^3.1.2" } 

Nous npm install et, pendant son installation, nous comprenons.


Puisque nous sommes au début de 2018 et 2019, notre application Web sera universelle (ou isomorphe) , à la fois à l'arrière et à l'avant, il y aura une version ECMAScript non inférieure à ES2017. Pour ce faire, index.js (le fichier d'entrée de l'application) connecte babel / register, et babel à la volée transforme tout le code ES qui le suit en JavaScript convivial par navigateur en utilisant babel / preset-env et babel / preset-react. Pour faciliter le développement, j'utilise généralement le plugin babel-plugin-root-import, avec lequel toutes les importations du répertoire racine ressembleront à '~ /', et à partir de src / - '& /'. Alternativement, vous pouvez écrire de longs chemins ou utiliser un alias à partir du webpack.


index.js


 require("@babel/register")(); require("./app"); 

.babelrc


 { "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ], "@babel/preset-react" ], "plugins": [ ["babel-plugin-root-import", { "paths": [{ "rootPathPrefix": "~", "rootPathSuffix": "" }, { "rootPathPrefix": "&", "rootPathSuffix": "src/" }] }] ] } 

Il est temps de configurer Webpack . Nous créons webpack.config.js et utilisons le code (ci-après, faites attention aux commentaires dans le code).


 const path = require('path'); module.exports = { // ,      Universal web app entry: { client: './src/client.js' }, // ,      webpack' output: { path: path.resolve(__dirname, 'public'), publicPath: '/' }, module: { //  babel-loader     ECMAScript    // JavaScript.       /public rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] } } 

A partir de ce moment, le plaisir commence. Il est temps de développer le côté serveur de l'application. Le rendu côté serveur (SSR) est une technologie conçue pour accélérer le chargement d'une application Web à certains moments et résoudre le débat éternel sur l'optimisation des moteurs de recherche dans les applications à page unique (SEO in SPA). Pour ce faire, nous prenons le modèle HTML, y mettons le contenu et l'envoyons à l'utilisateur. Le serveur le fait très rapidement - la page est dessinée en quelques millisecondes. Cependant, il n'y a aucun moyen de manipuler le DOM sur le serveur, donc la partie client de l'application rafraîchit la page et elle devient finalement interactive. D'accord? Nous nous développons!


app.js


 import express from 'express' import path from 'path' import stateRoutes from './server/stateRoutes' //   Express     Node.js const app = express() //    app.use(express.static('public')) app.use('/assets', express.static(path.resolve(__dirname, 'assets'))) //    3000 ,      const PORT = process.env.PORT || 3000 app.listen(PORT, '0.0.0.0', () => { console.log(`The app is running in PORT ${PORT}`) }) //   -  GET-   state  -  //    ,     . stateRoutes(app) 

server / stateRoutes.js


 import ssr from './server' export default function (app) { //        // ssr - ,   HTML app.get('*', (req, res) => { const response = ssr(req.url) res.send(response) }) } 

Le fichier server / server.js collecte le contenu généré par react et le transmet au modèle HTML - /server/template.js . Il convient de préciser que le serveur utilise un routeur statique, car nous ne voulons pas modifier l'url de la page lors du chargement. Et react-casque est une bibliothèque qui simplifie considérablement le travail avec les métadonnées (et en effet avec la balise head).


server / server.js


 import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet' import App from '&/app/App' import template from './template' export default function render(url) { //      const reactRouterContext = {} //     HTML let content = renderToString( <StaticRouter location={url} context={reactRouterContext}> <App/> </StaticRouter> ) //  <head>  HTML- const helmet = Helmet.renderStatic() //   HTML-     return template(helmet, content) } 

Dans server / template.js, dans la tête, nous imprimons les données du casque, connectons le favicon, les styles du répertoire statique / actifs. Dans le corps se trouvent le contenu et le bundle webpack client.js situés dans le dossier / public, mais comme il est statique, nous allons à l'adresse du répertoire racine - /client.js.


serveur / template.js


 // HTML- export default function template(helmet, content = '') { const scripts = `<script src="/client.js"></script>` const page = `<!DOCTYPE html> <html lang="en"> <head> ${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()} <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="theme-color" content="#810051"> <link rel="shortcut icon" href="/assets/logos/favicon.ico" type="image/x-icon"> <link rel="icon" href="/assets/logos/favicon.ico" type="image/x-icon"> <link rel="stylesheet" href="/assets/global.css"> </head> <body> <div class="content"> <div id="app" class="wrap-inner"> <!--- magic happens here --> ${content} </div> </div> ${scripts} </body> ` return page } 

Nous nous tournons vers le simple - le côté client. Le fichier src / client.js restaure le code HTML généré par le serveur sans mettre à jour le DOM et le rend interactif. (Plus d'informations ici ). La fonction de réaction d' hydrate le fait. Et maintenant, nous n'avons plus rien à voir avec un routeur statique. Nous utilisons celui habituel - BrowserRouter.


src / client.js


 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.querySelector('#app') ) 

Déjà dans deux fichiers, le composant React de l'application a réussi à s'allumer. Il s'agit du composant principal de l'application de bureau qui effectue le routage. Son code est très courant:


src / app / App.js


 import React from 'react' import { Switch, Route } from 'react-router' import Home from './Home' export default function App() { return( <Switch> <Route exact path="/" component={Home}/> </Switch> ) } 

Eh bien, src / app / Home.js. Remarquez comment fonctionne Helmet - le wrapper de balise head habituel.


 import React from 'react' import { Helmet } from 'react-helmet' export default function Home() { return( <div> <Helmet> <title>Universal Page</title> <meta name="description" content="Modern Web App - Home Page" /> </Helmet> <h1> Welcome to the page of Universal Web App </h1> </div> ) } 

Félicitations! Nous avons démonté la première partie du développement de MWA! Il ne restait que quelques touches pour tester le tout. Idéalement, vous pouvez remplir le dossier / assets avec des fichiers de style global et un favicon selon le modèle - server / template.js. Nous n'avons pas non plus de commandes de lancement d'application. Retour à package.json :


 "scripts": { "start": "npm run pack && npm run startProd", "startProd": "NODE_ENV=production node index.js", "pack": "webpack --mode production --config webpack.config.js", "startDev": "npm run packDev && node index.js", "packDev": "webpack --mode development --config webpack.config.js" } 

Vous pouvez remarquer deux catégories de commandes - Prod et Dev. Ils diffèrent dans la configuration de webpack v4. À propos de --mode mérite d'être lu ici .
Assurez-vous d'essayer l'application universelle résultante sur localhost: 3000



2. Material-ui


Cette partie du didacticiel se concentrera sur la connexion à l'application Web avec le SSR de la bibliothèque material-ui. Pourquoi exactement elle? Tout est simple - la bibliothèque se développe activement, est maintenue et possède une documentation complète. Avec lui, vous pouvez créer une belle interface utilisateur juste pour cracher.


Le schéma de connexion lui-même, adapté à notre application, est décrit ici . Eh bien, faisons-le.


Installez les dépendances nécessaires:


 npm i @material-ui/core jss react-jss 

Ensuite, nous devons apporter des modifications aux fichiers existants. Dans server / server.js, nous encapsulons notre application dans JssProvider et MuiThemeProvider, qui fourniront des composants material-ui et, très important, l'objet sheetsRegistry - css, qui doit être placé dans le modèle HTML. Côté client, nous utilisons uniquement MuiThemeProvider, en lui fournissant un objet thème.


serveur, modèle et client

server / server.js


 import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet' //     material-ui import { SheetsRegistry } from 'react-jss/lib/jss' import JssProvider from 'react-jss/lib/JssProvider' import { MuiThemeProvider, createMuiTheme, createGenerateClassName, } from '@material-ui/core/styles' import purple from '@material-ui/core/colors/purple' import App from '&/app/App' import template from './template' export default function render(url) { const reactRouterContext = {} //  sheetsRegistry -    const sheetsRegistry = new SheetsRegistry() const sheetsManager = new Map() //   -        const theme = createMuiTheme({ palette: { primary: purple, secondary: { main: '#f44336', }, }, //      3.*.*.   v4 -  typography: { useNextVariants: true, }, }) const generateClassName = createGenerateClassName() //     let content = renderToString( <StaticRouter location={url} context={reactRouterContext}> <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}> <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}> <App/> </MuiThemeProvider> </JssProvider> </StaticRouter> ) const helmet = Helmet.renderStatic() //  sheetsRegistry        html return template(helmet, content, sheetsRegistry) } 

serveur / template.js


 export default function template(helmet, content = '', sheetsRegistry) { const css = sheetsRegistry.toString() const scripts = `<script src="/client.js"></script>` const page = `<!DOCTYPE html> <html lang="en"> <head> ... </head> <body> <div class="content">...</div> <style id="jss-server-side">${css}</style> ${scripts} </body> ` return page } 

src / client.js


 ... import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider' import createMuiTheme from '@material-ui/core/styles/createMuiTheme' import purple from '@material-ui/core/colors/purple' //       ,     //         const theme = createMuiTheme({ palette: { primary: purple, secondary: { main: '#f44336', }, }, typography: { useNextVariants: true, }, }) //     hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) 

Maintenant, je propose d'ajouter un petit design élégant au composant Home. Vous pouvez consulter tous les composants Material-UI sur leur site officiel, ici Paper, Button, AppBar, Toolbar et Typography suffisent.


src / app / Home.js


 import React from 'react' import { Helmet } from 'react-helmet' import Paper from '@material-ui/core/Paper' import Typography from '@material-ui/core/Typography' import Button from '@material-ui/core/Button' import Header from './Header' // Inline styles -       css  react const styles = { paper: { margin: "auto", marginTop: 200, width: "40%", padding: 15 }, btn: { marginRight: 20 } } export default function Home() { return( <div> <Helmet> <title>Universal Material Page</title> </Helmet> <Header/> <Paper elevation={4} style={styles.paper} align="center"> <Typography variant="h5">Universal Web App with Material-ui</Typography> <br/> <Button variant="contained" color="primary" style={styles.btn}>I like it!</Button> </Paper> </div> ) } 

src / app / Header.js


 import React from 'react' import AppBar from '@material-ui/core/AppBar' import Toolbar from '@material-ui/core/Toolbar' import Typography from '@material-ui/core/Typography' export default function Header() { return ( <AppBar position="static"> <Toolbar> <Typography variant="h5" color="inherit"> Modern Web App </Typography> </Toolbar> </AppBar> ) } 

Maintenant, quelque chose comme ça devrait se révéler:




3. Fractionnement du code


Si vous prévoyez d'écrire quelque chose de plus qu'une liste TODO, votre application augmentera proportionnellement au bundle client.js. Pour éviter un long chargement de pages chez l'utilisateur, le fractionnement de code a été inventé depuis longtemps. Cependant, une fois que Ryan Florence, l'un des créateurs de React-router, a effrayé les développeurs potentiels avec sa phrase:


Godspeed ceux qui tentent les applications de partage de code rendues par le serveur.

Bonne chance à tous ceux qui décident de créer des applications SSR avec fractionnement de code


Nous sommes repoussés - nous le ferons! Installez le nécessaire:


 npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable 

Le problème n'est qu'une fonction - l'importation. Webpack prend en charge cette fonction d'importation dynamique asynchrone, mais la compilation de babel sera un énorme problème. Heureusement, en 2018, les bibliothèques sont arrivées pour aider à résoudre ce problème. babel / plugin-syntax-dynamic-import et babel-plugin-dynamic-import-node nous sauveront de l'erreur "Unexpected token when using import()" . Pourquoi deux bibliothèques pour une tâche? dynamic-import-node est nécessaire spécifiquement pour le rendu du serveur et récupérera les importations sur le serveur à la volée:


index.js


 require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app"); 

En mĂŞme temps, nous modifions le fichier de configuration global babel .babelrc


 "plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ] 

Ici semblait être réactif . Cette bibliothèque avec une excellente documentation rassemblera tous les modules cassés par l'importation du webpack sur le serveur, et le client les récupérera tout aussi facilement. Pour ce faire, le serveur doit télécharger tous les modules:


app.js


 import Loadable from 'react-loadable' ... Loadable.preloadAll().then(() => app.listen(PORT, '0.0.0.0', () => { console.log(`The app is running in PORT ${PORT}`) })) ... 

Les modules eux-mêmes sont très faciles à connecter. Jetez un œil au code:


src / app / App.js


 import React from 'react' import { Switch, Route } from 'react-router' import Loadable from 'react-loadable' import Loading from '&/Loading' const AsyncHome = Loadable({ loader: () => import(/* webpackChunkName: "Home" */ './Home'), loading: Loading, delay: 300, }) export default function App() { return( <Switch> <Route exact path="/" component={AsyncHome}/> </Switch> ) } 

React-loadable charge de manière asynchrone le composant Home, indiquant clairement au webpack qu'il doit être appelé Home (oui, c'est un cas rare lorsque les commentaires ont un sens). delay: 300 signifie que si après 300 ms le composant ne se charge toujours pas, vous devez montrer que le téléchargement est toujours en cours. Il traite du chargement:


src / Loading.js


 import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress' //        .   const styles = { div: { width: '20%', margin: 'auto', transition: 'margin 1s', backgroundColor: 'lightgreen', color: 'white', cursor: 'pointer', borderRadius: '3px' } } export default function Loading(props) { if (props.error) { //      (  PWA  ),  //  ,      return <div style={styles.div} onClick={ () => window.location.reload(true) } align="center"> <h3> Please, click here or reload the page. New content is ready. </h3> </div> } else if (props.pastDelay) { //     300,    return <CircularProgress color="primary"/> } else { //    Loading  return null } } 

Pour indiquer clairement au serveur les modules que nous importons, nous devons enregistrer:


 Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], }); 

Mais pour ne pas répéter le même code, il existe un plugin / babel réactif que nous avons déjà connecté avec succès à .babelrc . Maintenant que le serveur sait quoi importer, vous devez savoir ce qui sera rendu. Le workflow est un peu comme un casque:


server / server.js


 import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = [] //      modules let content = renderToString( <StaticRouter location={url} context={reactRouterContext}> <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}> <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}> <Loadable.Capture report={moduleName => modules.push(moduleName)}> <App/> </Loadable.Capture> </MuiThemeProvider> </JssProvider> </StaticRouter> ) ... //     ( ) let bundles = getBundles(stats, modules) //    HTML- return template(helmet, content, sheetsRegistry, bundles) 

Pour vous assurer que le client charge tous les modules rendus sur le serveur, nous devons les corréler avec les bundles créés par webpack. Pour ce faire, apportez des modifications à la configuration du collecteur. Le plugin react-loadable / webpack écrit tous les modules dans un fichier séparé. Nous devons également dire à webpack de sauvegarder correctement les modules après l'importation dynamique - dans l'objet de sortie.


webpack.config.js


 const ReactLoadablePlugin = require('react-loadable/webpack').ReactLoadablePlugin; ... output: { path: path.resolve(__dirname, 'public'), publicPath: '/', chunkFilename: '[name].bundle.js', filename: "[name].js" }, plugins: [ new ReactLoadablePlugin({ filename: './public/react-loadable.json', }) ] 

Nous écrivons les modules dans le modèle, en les chargeant à leur tour:


serveur / template.js


 export default function template(helmet, content = '', sheetsRegistry, bundles) { ... const page = `<!DOCTYPE html> <html lang="en"> <head>...</head> <body> <div class="content"> <div id="app" class="wrap-inner"> <!--- magic happens here --> ${content} </div> ${bundles.map(bundle => `<script src='/${bundle.file}'></script>`).join('\n')} </div> <style id="jss-server-side">${css}</style> ${scripts} </body> ` return page } 

Il ne reste plus qu'à traiter la partie client. La méthode Loadable.preloadReady() charge à l'avance tous les modules que le serveur a donnés à l'utilisateur.


src / client.js


 import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) }) 

C'est fait! Nous commençons et regardons le résultat - dans la dernière partie, le bundle n'était qu'un seul fichier - client.js pesant 265 ko, et maintenant il y a 3 fichiers, dont le plus gros pèse 215 ko. Inutile de dire que la vitesse de chargement des pages augmentera considérablement lors de la mise à l'échelle d'un projet?




4. Compteur Redux


Nous allons maintenant commencer à résoudre des problèmes pratiques. Comment résoudre le dilemme lorsque le serveur dispose de données (par exemple, d'une base de données), vous devez les afficher afin que les robots de recherche puissent trouver le contenu, puis utiliser ces données sur le client.


Il y a une solution. Il est utilisé dans presque tous les articles SSR, mais la manière dont il est implémenté est loin d'être toujours adaptée à une bonne évolutivité. En termes simples, en suivant la plupart des tutoriels, vous ne pourrez pas faire un vrai site avec SSR sur le principe "One, Two, and Production". Maintenant, je vais essayer de parsemer le i.


Nous n'avons besoin que de redux . Le fait est que redux dispose d'un magasin global, que nous pouvons transférer du serveur au client en un seul clic.
Maintenant l'important (!): Nous avons une raison d'avoir un fichier server / stateRoutes . Il gère l'objet initialState qui y est généré, un magasin est créé à partir de celui-ci, puis transmis au modèle HTML. Le client récupère cet objet à partir de la window.__STATE__ , window.__STATE__ magasin, et c'est tout. Cela semble facile.


Installer:


 npm i redux react-redux 

Suivez les étapes ci-dessus. Ici, pour la plupart, répétition du code précédemment utilisé.


Compteur de traitement serveur et client

server / stateRoutes.js :


 import ssr from './server' //   -  = 5 const initialState = { count: 5 } export default function (app) { app.get('*', (req, res) => { //  initialState  const response = ssr(req.url, initialState) res.send(response) }) } 

server / server.js :


 import { Provider } from 'react-redux' import configureStore from '&/redux/configureStore' ... export default function render(url, initialState) { //   const store = configureStore(initialState) ... // Redux Provider    . let content = renderToString( <StaticRouter location={url} context={reactRouterContext}> <Provider store={store} > <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}> <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}> <Loadable.Capture report={moduleName => modules.push(moduleName)}> <App/> </Loadable.Capture> </MuiThemeProvider> </JssProvider> </Provider> </StaticRouter> ) ... //  initialState  HTML- return template(helmet, content, sheetsRegistry, bundles, initialState) } 

serveur / template.js


 export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { ... //   initialState       const scripts = `<script> window.__STATE__ = ${JSON.stringify(initialState)} </script> <script src="/client.js"></script>` const page = `<!DOCTYPE html> <html lang="en"> <head>...</head> <body> ... ${scripts} </body> ` return page } 

Nous obtenons le magasin sur le client. src / client.js


 import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ... //   initialState  ""     const state = window.__STATE__ const store = configureStore(state) Loadable.preloadReady().then(() => { hydrate( <Provider store={store} > <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider> </Provider>, document.querySelector('#app') ) }) 

La logique de redux dans le SSR est terminée. Maintenant, le travail habituel avec redux consiste à créer un magasin, des actions, des réducteurs, une connexion, etc. J'espère que ce sera clair sans trop d'explications. Sinon, lisez la documentation .


Redux entier ici

src / redux / configureStore.js


 import { createStore } from 'redux' import rootReducer from './reducers' export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState ) } 

src / redux / actions.js


 // actions export const INCREASE = 'INCREASE' export const DECREASE = 'DECREASE' //  action creators export function increase() { return { type: INCREASE } } export function decrease() { return { type: DECREASE } } 

src / redux / reducers.js


 import { INCREASE, DECREASE } from './actions' export default function count(state, action) { switch (action.type) { case INCREASE: //   action = INCREASE -  state.count  1 return Object.assign({}, state, { count: state.count + 1 }) case DECREASE: //  DECREASE -   1.    return Object.assign({}, state, { count: state.count - 1 }) default: //      return state } } 

src / app / Home.js


 import React from 'react' import { Helmet } from 'react-helmet' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as Actions from '&/redux/actions' import Header from './Header' import Paper from '@material-ui/core/Paper' import Typography from '@material-ui/core/Typography' import Button from '@material-ui/core/Button' const styles = { paper: { margin: 'auto', marginTop: '10%', width: '40%', padding: 15 }, btn: { marginRight: 20 } } class Home extends React.Component{ constructor(){ super() this.increase = this.increase.bind(this) this.decrease = this.decrease.bind(this) } //   dispatch   increase  decrease increase(){ this.props.actions.increase() } decrease(){ this.props.actions.decrease() } render(){ return ( <div> <Helmet> <title>MWA - Home</title> <meta name="description" content="Modern Web App - Home Page" /> </Helmet> <Header/> <Paper elevation={4} style={styles.paper} align="center"> <Typography variant="h5">Redux-Counter</Typography> <Typography variant="subtitle1">Counter: {this.props.count}</Typography> <br/> <Button variant="contained" color="primary" onClick={this.increase} style={styles.btn}>Increase</Button> <Button variant="contained" color="primary" onClick={this.decrease}>Decrease</Button> </Paper> </div> ) } } //   props  const mapStateToProps = (state) => ({ count: state.count }) //  actions  this.props const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(Actions, dispatch) }) //  react-redux connect     export default connect( mapStateToProps, mapDispatchToProps )(Home) 

:




5.


, — . . , , initialState , .


:


 npm i mobile-detect 

mobile detect user-agent, null .


:


server/stateRoutes.js


 import ssr from './server' import MobileDetect from 'mobile-detect' const initialState = { count: 5, mobile: null } export default function (app) { app.get('*', (req, res) => { // md == null,  ,    const md = new MobileDetect(req.headers['user-agent']) const response = ssr(req.url, initialState, md.mobile()) res.send(response) }) } 

— :


server/server.js


 ... import App from '&/app/App' import MobileApp from '&/mobileApp/App' export default function render(url, initialState, mobile) { //    -    let content = renderToString( <StaticRouter location={url} context={reactRouterContext}> <Provider store={store} > <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}> <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}> <Loadable.Capture report={moduleName => modules.push(moduleName)}> {mobile === null ? <App/> : <MobileApp/> } </Loadable.Capture> </MuiThemeProvider> </JssProvider> </Provider> </StaticRouter> ) //       initialState.mobile = mobile return template(helmet, content, sheetsRegistry, bundles, initialState) } 

src/client.js


 ... const state = window.__STATE__ const store = configureStore(state) //       state Loadable.preloadReady().then(() => { hydrate( <Provider store={store} > <MuiThemeProvider theme={theme}> <BrowserRouter> {state.mobile === null ? <App/> : <MobileApp/> } </BrowserRouter> </MuiThemeProvider> </Provider>, document.querySelector('#app') ) }) 

react-, . , . src/mobileApp .



6.


Progressive Web App (PWA), Google — , , , .


. : Chrome, Opera Samsung Internet , . iOS Safari, . , . PWA: Windows Chrome v70, Linux v70, ChromeOS v67. PWA macOS — 2019 Chrome v72.


: PWA . , , , .


2 — manifest.json service-worker.js — . — json , , , . Service-worker : push-, .


. , :


public/manifest.json :


 { "short_name": "MWA", "name": "Modern Web App", "description": "Modern app built with React SSR, PWA, material-ui, code splitting and much more", "icons": [ { "src": "/assets/logos/yellow 192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/assets/logos/yellow 512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": ".", "display": "standalone", "theme_color": "#810051", "background_color": "#FFFFFF" } 

service-worker', . , , :


public/service-worker.js


 //   -       var CACHE = 'cache' //     self.addEventListener('install', function(evt) { evt.waitUntil(precache()) }) //   fetch  ,       self.addEventListener('fetch', function(evt) { console.log('The service worker is serving the asset.') evt.respondWith(fromCache(evt.request)) evt.waitUntil(update(evt.request)) }) // ,      function precache() { return caches.open(CACHE).then(function (cache) { return cache.addAll([ './', '/assets/MWA.png', '/assets/global.css', '/assets/logos/favicon.ico', '/assets/logos/yellow 192.png', '/assets/logos/yellow 512.png', '/robots.txt' ]) }) } //   ,      .  ,   function fromCache(request) { return caches.open(CACHE).then(function (cache) { return cache.match(request).then(function (matching) { return matching || null }) }) } //     ,    //     function update(request) { return caches.open(CACHE).then(function (cache) { return fetch(request).then(function (response) { return cache.put(request, response) }) }) } 

PWA , - html-:


server/template.js


 export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { const scripts = `... <script> //    service-worker -  if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('Service Worker is registered! '); }) .catch(err => { console.log('Registration failed ', err); }); }); } </script>` const page = `<!DOCTYPE html> <html lang="en"> <head> ... <link rel="manifest" href="/manifest.json"> </head> <body> ... ${scripts} </body> ` return page } 

C'est fait! https, , gif demo .


7.


MWA. , , . , SSR Code Splitting, PWA .


, MWA - web.dev :



, — . , , — .


, MWA — opensource . , , !


Bonne chance

Source: https://habr.com/ru/post/fr432368/


All Articles