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 = {
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'
server / stateRoutes.js
import ssr from './server' export default function (app) {
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) {
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
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 clientserver / server.js
import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet'
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'
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'
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( './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'
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 = []
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 clientserver / stateRoutes.js :
import ssr from './server'
server / server.js :
import { Provider } from 'react-redux' import configureStore from '&/redux/configureStore' ... export default function render(url, initialState) {
serveur / template.js
export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { ...
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' ...
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 icisrc / redux / configureStore.js
import { createStore } from 'redux' import rootReducer from './reducers' export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState ) }
src / redux / actions.js
src / redux / reducers.js
import { INCREASE, DECREASE } from './actions' export default function count(state, action) { switch (action.type) { case INCREASE:
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) }
:

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) => {
— :
server/server.js
... import App from '&/app/App' import MobileApp from '&/mobileApp/App' export default function render(url, initialState, mobile) {
src/client.js
... const state = window.__STATE__ const store = configureStore(state)
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
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