Bevor Sie mit dem Erstellen einer modernen Webanwendung von Grund auf beginnen , müssen Sie herausfinden, was eine moderne Webanwendung ist.
Modern Web App (MWA) ist eine Anwendung, die allen modernen Webstandards entspricht. Darunter die Progressive Web App - die Möglichkeit, eine mobile Browserversion auf Ihr Telefon herunterzuladen und als vollwertige Anwendung zu verwenden. Es ist auch eine Möglichkeit, die Site sowohl von einem mobilen Gerät als auch von einem Computer aus offline zu scrollen. modernes Materialdesign; perfekte Suchmaschinenoptimierung; und natürlich - hohe Download-Geschwindigkeit.

Folgendes wird in unserer MWA passieren (ich rate Ihnen, diesen Navigationsartikel zu verwenden):
Die Leute auf Habré sind geschäftlich, also holen Sie sich sofort einen Link zum GitHub-Repository , ein Archiv aus jeder Entwicklungsphase und eine Demo . Dieser Artikel richtet sich an Entwickler, die mit node.js vertraut sind und reagieren. Alle notwendigen Theorien werden im notwendigen Band vorgestellt. Erweitern Sie Ihren Horizont, indem Sie auf die Links klicken.
Fangen wir an!
1. Universal
Standardaktionen: Erstellen Sie ein Arbeitsverzeichnis und führen Sie git init
. Öffnen Sie package.json und fügen Sie einige Zeilen hinzu:
"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" }
Wir führen npm install
und verstehen es, während es installiert ist.
Da wir uns um die Wende von 2018 und 2019 befinden, wird unsere Webanwendung universell (oder isomorph) sein . Sowohl auf der Rückseite als auch auf der Vorderseite wird es eine ECMAScript- Version geben, die nicht niedriger als ES2017 ist. Zu diesem Zweck verbindet index.js (die Anwendungseingabedatei) babel / register, und babel on-the-fly wandelt den gesamten darauf folgenden ES-Code mithilfe von babel / preset-env und babel / preset-react in browserfreundliches JavaScript um. Zur Vereinfachung der Entwicklung verwende ich normalerweise das Babel-Plugin-Root-Import-Plugin, mit dem alle Importe aus dem Root-Verzeichnis wie '~ /' und von src / - '& /' aussehen. Alternativ können Sie lange Pfade schreiben oder einen Alias aus dem Webpack verwenden.
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/" }] }] ] }
Zeit, Webpack einzurichten . Wir erstellen webpack.config.js und verwenden den Code (im Folgenden beachten Sie die Kommentare im Code).
const path = require('path'); module.exports = {
Ab diesem Moment beginnt der Spaß. Es ist Zeit, die Serverseite der Anwendung zu entwickeln. Server-Side Rendering (SSR) ist eine Technologie, die entwickelt wurde, um das Laden von Webanwendungen zeitweise zu beschleunigen und die ewige Debatte über die Suchmaschinenoptimierung in Single Page Application (SEO in SPA) zu lösen. Dazu nehmen wir die HTML-Vorlage, fügen den Inhalt ein und senden ihn an den Benutzer. Der Server erledigt dies sehr schnell - die Seite wird in wenigen Millisekunden gezeichnet. Es gibt jedoch keine Möglichkeit, das DOM auf dem Server zu bearbeiten, sodass der Client-Teil der Anwendung die Seite aktualisiert und sie schließlich interaktiv wird. Okay? Wir entwickeln uns!
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) {
Die Datei server / server.js sammelt den durch react generierten Inhalt und übergibt ihn an die HTML-Vorlage - /server/template.js . Es sollte klargestellt werden, dass der Server einen statischen Router verwendet, da wir die URL der Seite beim Laden nicht ändern möchten. Und React-Helm ist eine Bibliothek, die die Arbeit mit Metadaten (und tatsächlich mit dem Head-Tag) erheblich vereinfacht.
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) {
In server / template.js drucken wir im Kopf die Daten vom Helm, verbinden das Favicon und die Stile aus dem statischen Verzeichnis / Assets. Im Body-Content- und Webpack-Bundle client.js , das sich im Ordner / public befindet, aber da es statisch ist, gehen wir zur Adresse des Stammverzeichnisses - /client.js.
server / template.js
Wir wenden uns dem Einfachen zu - der Kundenseite. Die Datei src / client.js stellt den vom Server generierten HTML- Code ohne Aktualisierung des DOM wieder her und macht ihn interaktiv. (Mehr dazu hier ). Die Hydratreaktionsfunktion übernimmt dies. Und jetzt haben wir nichts mehr mit einem statischen Router zu tun. Wir verwenden den üblichen 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') )
Bereits in zwei Dateien konnte die Reaktionskomponente der App aufleuchten. Dies ist die Hauptkomponente der Desktopanwendung, die das Routing ausführt. Sein Code ist sehr alltäglich:
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> ) }
Nun, src / app / Home.js. Beachten Sie, wie der Helm funktioniert - der übliche Head Tag Wrapper.
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> ) }
Glückwunsch! Wir haben den ersten Teil der Entwicklung von MWA auseinander genommen! Es blieben nur ein paar Berührungen, um das Ganze zu testen. Idealerweise können Sie den Ordner / assets mit Dateien im globalen Stil und einem Favicon gemäß der Vorlage - server / template.js - füllen. Wir haben auch keine Befehle zum Starten von Anwendungen. Zurück zu 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" }
Möglicherweise stellen Sie zwei Kategorien von Befehlen fest - Prod und Dev. Sie unterscheiden sich in der Webpack v4-Konfiguration. Über --mode
lohnt es sich hier zu lesen.
Probieren Sie unbedingt die resultierende universelle Anwendung bei localhost: 3000 aus
2. Material-UI
Dieser Teil des Tutorials konzentriert sich auf die Verbindung mit der Webanwendung mit dem SSR der Material-UI-Bibliothek. Warum genau sie? Alles ist einfach - die Bibliothek entwickelt sich aktiv, wird gepflegt und verfügt über eine umfangreiche Dokumentation. Mit ihm können Sie eine schöne Benutzeroberfläche erstellen, nur um zu spucken.
Das für unsere Anwendung geeignete Verbindungsschema selbst wird hier beschrieben. Nun, lass es uns tun.
Installieren Sie die erforderlichen Abhängigkeiten:
npm i @material-ui/core jss react-jss
Als nächstes müssen wir Änderungen an vorhandenen Dateien vornehmen. In server / server.js verpacken wir unsere Anwendung in JssProvider und MuiThemeProvider, die Material-UI-Komponenten und vor allem das sheetRegistry-Objekt - css - bereitstellen, das in der HTML-Vorlage platziert werden muss. Auf der Client-Seite verwenden wir nur MuiThemeProvider und liefern ihm ein Themenobjekt.
Server, Vorlage und Clientserver / server.js
import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet'
server / 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'
Jetzt schlage ich vor, der Home-Komponente ein wenig stilvolles Design hinzuzufügen. Sie können sich alle Material-UI-Komponenten auf ihrer offiziellen Website ansehen. Hier reichen Papier, Button, AppBar, Symbolleiste und Typografie aus.
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> ) }
Nun sollte sich so etwas herausstellen:

3. Code-Aufteilung
Wenn Sie mehr als eine TODO-Liste schreiben möchten, wird Ihre Anwendung proportional zum Paket client.js erhöht. Um ein langes Laden von Seiten beim Benutzer zu vermeiden, wurde die Codeaufteilung seit langem erfunden. Als Ryan Florence, einer der Entwickler von React-Router, potenzielle Entwickler mit seinem Satz erschreckte:
Godspeed diejenigen, die versuchen, die vom Server gerenderten, Code-Split-Apps.
Viel Glück für alle, die sich entscheiden, SSR-Anwendungen mit Code-Aufteilung zu erstellen
Wir sind abgestoßen - wir werden es tun! Installieren Sie die erforderlichen:
npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable
Das Problem ist nur eine Funktion - Import. Webpack unterstützt diese asynchrone dynamische Importfunktion, aber die Babel-Kompilierung wird ein großes Problem sein. Glücklicherweise kamen bis 2018 Bibliotheken an, um dies zu bewältigen. babel / plugin-syntax-dynamic-import und babel-plugin-dynamic-import-node bewahren uns vor dem Fehler "Unexpected token when using import()"
. Warum zwei Bibliotheken für eine Aufgabe? Der dynamische Importknoten wird speziell für das Rendern von Servern benötigt und nimmt Importe auf dem Server im laufenden Betrieb auf:
index.js
require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app");
Gleichzeitig ändern wir die globale Babel-Konfigurationsdatei .babelrc
"plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ]
Hier erschien reaktionsladbar . Diese Bibliothek mit hervorragender Dokumentation sammelt alle Module, die durch den Webpack-Import auf dem Server beschädigt wurden, und der Client kann sie genauso einfach abrufen. Dazu muss der Server alle Module herunterladen:
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}`) })) ...
Die Module selbst sind sehr einfach anzuschließen. Schauen Sie sich den Code an:
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 lädt die Home-Komponente asynchron und macht dem Webpack klar, dass sie Home heißen sollte (ja, dies ist ein seltener Fall, wenn Kommentare sinnvoll sind). delay: 300
bedeutet, dass wenn nach delay: 300
ms die Komponente immer noch nicht geladen wird, Sie zeigen müssen, dass der Download noch läuft. Es befasst sich mit Laden:
src / Loading.js
import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress'
Um dem Server klar zu machen, welche Module wir importieren, müssten wir uns registrieren:
Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], });
Um jedoch nicht denselben Code zu wiederholen, gibt es ein reaktionsladbares / babel-Plugin, das wir bereits erfolgreich mit .babelrc verbunden haben . Nachdem der Server weiß, was importiert werden soll, müssen Sie herausfinden, was gerendert wird. Der Workflow ist ein bisschen wie beim Helm:
server / server.js
import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = []
Um sicherzustellen, dass der Client alle auf dem Server gerenderten Module lädt, müssen sie mit den von webpack erstellten Bundles korreliert werden. Nehmen Sie dazu Änderungen an der Konfiguration des Kollektors vor. Das React-Load / Webpack-Plugin schreibt alle Module in eine separate Datei. Wir sollten Webpack auch anweisen, Module nach dem dynamischen Import korrekt zu speichern - im Ausgabeobjekt.
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', }) ]
Wir schreiben die Module in die Vorlage und laden sie nacheinander:
server / 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 }
Es bleibt nur die Bearbeitung des Client-Teils. Die Loadable.preloadReady()
-Methode lädt alle Module, die der Server dem Benutzer im Voraus gegeben hat.
src / client.js
import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) })
Fertig! Wir beginnen und schauen uns das Ergebnis an - im letzten Teil war das Bundle nur eine Datei - client.js mit einem Gewicht von 265 KB, und jetzt gibt es 3 Dateien, von denen die größte 215 KB wiegt. Es ist unnötig zu erwähnen, dass die Geschwindigkeit beim Laden von Seiten beim Skalieren eines Projekts erheblich zunimmt.

4. Reduxzähler
Jetzt werden wir beginnen, praktische Probleme zu lösen. Um das Dilemma zu lösen, wenn der Server über Daten verfügt (z. B. aus einer Datenbank), müssen Sie diese anzeigen, damit Such-Bots den Inhalt finden und diese Daten dann auf dem Client verwenden können.
Es gibt eine Lösung. Es wird in fast jedem SSR-Artikel verwendet, aber die Art und Weise, wie es dort implementiert wird, ist bei weitem nicht immer für eine gute Skalierbarkeit zugänglich. Mit einfachen Worten, nach den meisten Tutorials können Sie mit SSR keine echte Site nach dem Prinzip "Eins, Zwei und Produktion" erstellen. Jetzt werde ich versuchen, das i zu punktieren.
Wir brauchen nur Redux . Tatsache ist, dass Redux einen globalen Speicher hat, den wir mit einem Fingerklick vom Server auf den Client übertragen können.
Nun das Wichtige (!): Wir haben einen Grund, eine Server / StateRoutes-Datei zu haben . Es verwaltet das dort generierte initialState- Objekt, erstellt daraus einen Speicher und übergibt ihn an die HTML-Vorlage. Der Client ruft dieses Objekt aus dem window.__STATE__
, window.__STATE__
Speicher neu und window.__STATE__
. Es scheint einfach.
Installieren:
npm i redux react-redux
Befolgen Sie die obigen Schritte. Hier zum größten Teil Wiederholung des zuvor verwendeten Codes.
Server- und Client-Verarbeitungszählerserver / 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) {
server / template.js
export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { ...
Wir bekommen Geschäft auf dem Kunden. src / client.js
import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...
Die Redux-Logik im SSR ist beendet. Die übliche Arbeit mit Redux besteht nun darin, ein Geschäft, Aktionen, Reduzierungen, Verbindungen und mehr zu erstellen. Ich hoffe, dass dies ohne viel Erklärung klar wird. Wenn nicht, lesen Sie die Dokumentation .
Ganzer Redux hiersrc / redux / configureStore.js
import { createStore } from 'redux' import rootReducer from './reducers' export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState ) }
src / redux / action.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 }
! https, , gif demo .
7.
MWA. , , . , SSR Code Splitting, PWA .
, MWA - web.dev :

, — . , , — .
, MWA — opensource . , , !
Viel Glück