Antes de comenzar a construir desde cero la aplicación web moderna, debe descubrir qué es una aplicación web moderna.
Modern Web App (MWA) es una aplicación que se adhiere a todos los estándares web modernos. Entre ellos, la aplicación web progresiva es la capacidad de descargar una versión de navegador móvil a su teléfono y usarla como una aplicación completa. También es una oportunidad para desplazar el sitio fuera de línea tanto desde un dispositivo móvil como desde una computadora; diseño moderno de materiales; perfecta optimización de motores de búsqueda; y, naturalmente, alta velocidad de descarga.

Esto es lo que sucederá en nuestro MWA (le aconsejo que use esta navegación en el artículo):
Las personas en Habré son negocios, por lo que de inmediato pueden ver un enlace al repositorio de GitHub , un archivo de cada una de las etapas de desarrollo y una demostración . Este artículo está destinado a desarrolladores familiarizados con node.js y que reaccionen. Toda la teoría necesaria se presenta en el volumen necesario. Amplíe sus horizontes haciendo clic en los enlaces.
¡Empecemos!
1. Universal
Acciones estándar: crea un directorio de trabajo y ejecuta git init
. Abra package.json y agregue un par de líneas:
"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" }
npm install
y, mientras está instalado, entendemos.
Dado que estamos a finales de 2018 y 2019, nuestra aplicación web será universal (o isomórfica) , tanto en la parte posterior como frontal habrá una versión ECMAScript no inferior a ES2017. Para hacer esto, index.js (el archivo de entrada de la aplicación) conecta babel / register, y babel sobre la marcha convierte todo el código ES que lo sigue en JavaScript amigable para el navegador usando babel / preset-env y babel / preset-react. Por conveniencia del desarrollo, generalmente uso el complemento babel-plugin-root-import, con el cual todas las importaciones desde el directorio raíz se verán como '~ /', y desde src / - '& /'. Alternativamente, puede escribir rutas largas o usar alias desde 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/" }] }] ] }
Es hora de configurar Webpack . Creamos webpack.config.js y usamos el código (en adelante, preste atención a los comentarios en el código).
const path = require('path'); module.exports = {
A partir de este momento, comienza la diversión. Es hora de desarrollar el lado del servidor de la aplicación. El renderizado del lado del servidor (SSR) es una tecnología diseñada para acelerar la carga de una aplicación web en ocasiones y resolver el eterno debate sobre la optimización de motores de búsqueda en la Aplicación de página única (SEO en SPA). Para hacer esto, tomamos la plantilla HTML, colocamos el contenido y la enviamos al usuario. El servidor hace esto muy rápidamente: la página se dibuja en cuestión de milisegundos. Sin embargo, no hay forma de manipular el DOM en el servidor, por lo que la parte del cliente de la aplicación actualiza la página y finalmente se vuelve interactiva. Ok? Estamos desarrollando!
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) {
El archivo server / server.js recopila el contenido generado por react y lo pasa a la plantilla HTML: /server/template.js . Vale la pena aclarar que el servidor usa un enrutador estático, porque no queremos cambiar la URL de la página durante la carga. Y react-helmet es una biblioteca que simplifica enormemente el trabajo con metadatos (y de hecho con la etiqueta de la cabeza).
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) {
En server / template.js, en la cabeza imprimimos los datos del casco, conectamos el favicon, los estilos del directorio / activos estáticos. En el cuerpo se encuentran el paquete de contenido y paquete web client.js ubicado en la carpeta / public, pero como es estático, vamos a la dirección del directorio raíz: /client.js.
server / template.js
Pasamos a lo simple: el lado del cliente. El archivo src / client.js restaura el HTML generado por el servidor sin actualizar el DOM y lo hace interactivo. (Más sobre esto aquí ). La función de reacción de hidrato hace esto. Y ahora no tenemos nada que ver con un enrutador estático. Usamos el habitual: 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') )
Ya en dos archivos, el componente de reacción de la aplicación logró iluminarse. Este es el componente principal de la aplicación de escritorio que realiza el enrutamiento. Su código es muy común:
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> ) }
Bueno, src / app / Home.js. Observe cómo funciona Helmet: el envoltorio habitual de etiquetas de cabeza.
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> ) }
Felicidades ¡Desmontamos la primera parte del desarrollo de MWA! Solo quedaban un par de toques para probar todo. Idealmente, puede llenar la carpeta / assets con archivos de estilo global y un favicon de acuerdo con la plantilla: server / template.js. Tampoco tenemos comandos de inicio de aplicaciones. Volver a 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" }
Puede notar dos categorías de comandos: Prod y Dev. Difieren en la configuración de webpack v4. Acerca de --mode
vale la pena leer aquí .
Asegúrese de probar la aplicación universal resultante en localhost: 3000
2. Material-ui
Esta parte del tutorial se centrará en conectarse a la aplicación web con el SSR de la biblioteca material-ui. ¿Por qué exactamente ella? Todo es simple: la biblioteca se desarrolla, mantiene y cuenta con una extensa documentación. Con él, puedes construir una hermosa interfaz de usuario solo para escupir.
El esquema de conexión en sí, adecuado para nuestra aplicación, se describe aquí . Bueno, hagámoslo.
Instale las dependencias necesarias:
npm i @material-ui/core jss react-jss
Luego tenemos que hacer cambios a los archivos existentes. En server / server.js, envolvemos nuestra aplicación en JssProvider y MuiThemeProvider, que proporcionará componentes material-ui y, muy importante, el objeto sheetRegistry - css, que debe colocarse en la plantilla HTML. En el lado del cliente, usamos solo MuiThemeProvider, proporcionándole un objeto de tema.
servidor, plantilla y clienteserver / 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'
Ahora propongo agregar un pequeño diseño elegante al componente Home. Puede ver todos los componentes de material-ui en su sitio web oficial, aquí Paper, Button, AppBar, Toolbar y Typography son suficientes.
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> ) }
Ahora algo como esto debería resultar:

3. División de código
Si planea escribir algo más que una lista TODO, su aplicación aumentará en proporción al paquete client.js. Para evitar la carga prolongada de páginas al usuario, se inventó la división de código durante mucho tiempo. Sin embargo, una vez que Ryan Florence, uno de los creadores de React-router, asustó a los posibles desarrolladores con su frase:
Apresure a aquellos que intentan las aplicaciones divididas en código divididas en servidor.
Buena suerte a todos los que decidan crear aplicaciones ssr con división de código.
Estamos rechazados, ¡lo haremos! Instala lo necesario:
npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable
El problema es solo una función: importar. Webpack admite esta función de importación dinámica asincrónica, pero la compilación de babel será un gran problema. Afortunadamente, para 2018, llegaron bibliotecas para ayudar a lidiar con esto. babel / plugin-syntax-dynamic-import y babel-plugin-dynamic-import-node nos salvarán del error "Unexpected token when using import()"
. ¿Por qué dos bibliotecas para una tarea? Dynamic-import-node se necesita específicamente para la representación del servidor, y recogerá las importaciones en el servidor sobre la marcha:
index.js
require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app");
Al mismo tiempo, modificamos el archivo de configuración global de babel .babelrc
"plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ]
Aquí apareció reaccionar-cargable . Esta biblioteca con excelente documentación recopilará todos los módulos rotos por la importación del paquete web en el servidor, y el cliente los recogerá con la misma facilidad. Para hacer esto, el servidor necesita descargar todos los módulos:
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}`) })) ...
Los módulos en sí son muy fáciles de conectar. Echa un vistazo al código:
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> ) }
El componente Home carga de forma asíncrona cargando React, dejando en claro al paquete web que debe llamarse Home (sí, este es un caso raro cuando los comentarios tienen sentido). delay: 300
significa que si después de delay: 300
ms el componente aún no se carga, debe mostrar que la descarga aún continúa. Se trata de carga:
src / Loading.js
import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress'
Para dejar en claro al servidor qué módulos estamos importando, necesitaríamos registrarnos:
Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], });
Pero para no repetir el mismo código, hay un complemento reaccionable / babel que ya hemos conectado con éxito a .babelrc . Ahora que el servidor sabe qué importar, debe averiguar qué se representará. El flujo de trabajo es un poco como Helmet:
server / server.js
import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = []
Para asegurarnos de que el cliente carga todos los módulos representados en el servidor, necesitamos correlacionarlos con los paquetes creados por webpack. Para hacer esto, realice cambios en la configuración del recopilador. El complemento react-loadable / webpack escribe todos los módulos en un archivo separado. También deberíamos decirle a webpack que guarde correctamente los módulos después de la importación dinámica, en el objeto de salida.
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', }) ]
Escribimos los módulos en la plantilla, cargándolos a su vez:
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 }
Solo queda procesar la parte del cliente. El método Loadable.preloadReady()
carga todos los módulos que el servidor le dio al usuario por adelantado.
src / client.js
import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) })
Hecho Comenzamos y observamos el resultado: en la última parte, el paquete era solo un archivo: client.js pesaba 265 kb, y ahora hay 3 archivos, el más grande de los cuales pesa 215 kb. Huelga decir que la velocidad de carga de la página aumentará significativamente al escalar un proyecto.

4. Contador de Redux
Ahora comenzaremos a resolver problemas prácticos. Para resolver el dilema cuando el servidor tiene datos (por ejemplo, de una base de datos), debe mostrarlos para que los robots de búsqueda puedan encontrar el contenido y luego usar estos datos en el cliente.
Hay una solución Se usa en casi todos los artículos de SSR, pero la forma en que se implementa está lejos de ser siempre susceptible de una buena escalabilidad. En palabras simples, siguiendo la mayoría de los tutoriales, no podrá crear un sitio real con SSR según el principio de "Uno, Dos y Producción". Ahora intentaré puntear la i.
Solo necesitamos redux . El hecho es que redux tiene una tienda global, que podemos transferir del servidor al cliente con el clic de un dedo.
Ahora lo importante (!): Tenemos una razón para tener un archivo server / stateRoutes . Gestiona el objeto initialState que se genera allí, se crea una tienda a partir de él y luego se pasa a la plantilla HTML. El cliente recupera este objeto de la window.__STATE__
, window.__STATE__
tienda y eso es todo. Parece facil.
Instalar:
npm i redux react-redux
Sigue los pasos anteriores. Aquí, en su mayor parte, la repetición del código utilizado anteriormente.
Contador de procesamiento de servidor y clienteserver / 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 = {}) { ...
Tenemos tienda en el cliente. src / client.js
import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...
La lógica redux en el SSR ha terminado. Ahora, el trabajo habitual con redux es crear una tienda, acciones, reductores, conectar y más. Espero que esto quede claro sin mucha explicación. Si no, lea la documentación .
Todo redux aquí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
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 . , , !
Buena suerte