Antes de começar a criar do zero o Modern Web App, você precisa descobrir o que é um Aplicativo Web Moderno?
O Modern Web App (MWA) é um aplicativo que cumpre todos os padrões da web modernos. Entre eles, o Progressive Web App é a capacidade de baixar uma versão do navegador móvel para o seu telefone e usá-la como um aplicativo completo. É também uma oportunidade de rolar o site offline, tanto em um dispositivo móvel quanto em um computador; design moderno de materiais; otimização perfeita de mecanismos de pesquisa; e naturalmente - alta velocidade de download.

Aqui está o que acontecerá em nossa MWA (aconselho você a usar esta navegação no artigo):
As pessoas no Habré são profissionais, então pegue imediatamente um link para o repositório do GitHub , um arquivo de cada estágio de desenvolvimento e uma demonstração . Este artigo é destinado a desenvolvedores familiarizados com o node.js e reage. Toda a teoria necessária é apresentada no volume necessário. Amplie seus horizontes clicando nos links.
Vamos começar!
1. Universal
Ações padrão: crie um diretório de trabalho e execute git init . Abra package.json e adicione algumas linhas:
 "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 e, enquanto está instalado, entendemos.
Como estamos na virada de 2018 e 2019, nosso aplicativo da Web será universal (ou isomórfico) , tanto na parte traseira quanto na parte frontal haverá uma versão do ECMAScript não inferior ao ES2017. Para fazer isso, o index.js (o arquivo de entrada do aplicativo) conecta o babel / register, e o babel on-the-fly transforma todo o código ES seguindo o JavaScript compatível com o navegador usando babel / preset-env e babel / preset-react. Para maior comodidade do desenvolvimento, eu costumo usar o plugin babel-plugin-root-import, com o qual todas as importações do diretório raiz terão a aparência de '~ /' e de src / - '& /'. Como alternativa, você pode escrever caminhos longos ou usar o alias do 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/" }] }] ] } 
Hora de configurar o Webpack . Criamos o webpack.config.js e usamos o código (a seguir, preste atenção aos comentários no código).
 const path = require('path'); module.exports = {  
A partir deste momento, a diversão começa. É hora de desenvolver o lado do servidor do aplicativo. A renderização do lado do servidor (SSR) é uma tecnologia projetada para acelerar o carregamento de aplicativos da Web às vezes e resolver o eterno debate sobre a otimização de mecanismos de pesquisa no aplicativo de página única (SEO no SPA). Para fazer isso, pegamos o modelo HTML, colocamos o conteúdo nele e o enviamos ao usuário. O servidor faz isso muito rapidamente - a página é desenhada em questão de milissegundos. No entanto, não há como manipular o DOM no servidor, portanto, a parte cliente do aplicativo atualiza a página e ela se torna interativa. Ta bom Estamos desenvolvendo!
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) {  
O arquivo server / server.js coleta o conteúdo gerado pelo react e o passa para o modelo HTML - /server/template.js . Vale esclarecer que o servidor usa um roteador estático, porque não queremos alterar o URL da página durante o carregamento. E react-helmet é uma biblioteca que simplifica bastante o trabalho com metadados (e de fato com a tag 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) {  
No server / template.js, na cabeça, imprimimos os dados do capacete, conecte o favicon, estilos do diretório / ativos estáticos. No corpo - pacote de conteúdo e webpack client.js , localizado na pasta / public, mas como é estático - vamos para o endereço do diretório raiz - /client.js.
server / template.js
 
Nos voltamos para o simples - o lado do cliente. O arquivo src / client.js restaura o HTML gerado pelo servidor sem atualizar o DOM e o torna interativo. (Mais sobre isso aqui ). A função de reação de hidrato faz isso. E agora não temos nada a ver com um roteador estático. Usamos o usual - 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') ) 
Já em dois arquivos, o componente de reação do aplicativo conseguiu acender. Este é o principal componente do aplicativo de desktop que executa o roteamento. Seu código é muito comum:
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> ) } 
Bem, src / app / Home.js. Observe como o Helmet funciona - o invólucro de etiqueta de cabeça usual.
 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> ) } 
Parabéns! Desmontamos a primeira parte do desenvolvimento do MWA! Apenas alguns toques permaneceram para testar a coisa toda. Idealmente, você pode preencher a pasta / assets com arquivos de estilo global e um favicon de acordo com o template - server / template.js. Também não temos comandos de inicialização de aplicativos. Voltar para 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" } 
Você pode perceber duas categorias de comandos - Prod e Dev. Eles diferem na configuração do webpack v4. Sobre --mode vale a pena ler aqui .
Não deixe de experimentar o aplicativo universal resultante em localhost: 3000
2. Material-ui
Esta parte do tutorial se concentrará na conexão com o aplicativo Web com o SSR da biblioteca material-ui. Por que exatamente ela? Tudo é simples - a biblioteca está ativamente desenvolvendo, mantida e possui extensa documentação. Com ele, você pode criar uma interface de usuário bonita apenas para cuspir.
O próprio esquema de conexão, adequado para nossa aplicação, é descrito aqui . Bem, vamos lá.
Instale as dependências necessárias:
 npm i @material-ui/core jss react-jss 
Em seguida, temos que fazer alterações nos arquivos existentes. No server / server.js , agrupamos nosso aplicativo no JssProvider e no MuiThemeProvider, que fornecerão componentes de material-ui e, muito importante, o objeto sheetsRegistry - css, que deve ser colocado no modelo HTML. No lado do cliente, usamos apenas o MuiThemeProvider, fornecendo um objeto de tema.
servidor, modelo e 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'  
 Agora, proponho adicionar um pouco de design elegante ao componente Home. Você pode ver todos os componentes da interface do usuário no site oficial, aqui papel, botão, AppBar, barra de ferramentas e tipografia são 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> ) } 
Agora, algo assim deve acontecer:

3. Divisão de Código
Se você planeja escrever algo mais que uma lista TODO, seu aplicativo aumentará proporcionalmente ao pacote client.js. Para evitar o carregamento longo de páginas no usuário, a divisão de código foi inventada por um longo tempo. No entanto, uma vez que Ryan Florence, um dos criadores do React-router, assustou os desenvolvedores em potencial com sua frase:
Acelere quem tenta os aplicativos renderizados pelo servidor e divididos por código.
Boa sorte a todos que decidem criar aplicativos ssr com divisão de código
Estamos com repulsa - vamos fazê-lo! Instale o necessário: 
 npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable 
O problema é apenas uma função - importação. O Webpack suporta essa função de importação dinâmica assíncrona, mas a compilação do babel será um grande problema. Felizmente, em 2018, as bibliotecas chegaram para ajudar a lidar com isso. O babel / plugin-syntax-dynamic-import e o babel-plugin-dynamic-import-node nos salvam do erro "Unexpected token when using import()" . Por que duas bibliotecas para uma tarefa? O nó de importação dinâmica é necessário especificamente para a renderização do servidor e selecionará as importações no servidor rapidamente:
index.js
 require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app"); 
Ao mesmo tempo, modificamos o arquivo de configuração global do babel .babelrc
 "plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ] 
Aqui apareceu carregável por reação . Esta biblioteca com excelente documentação coletará todos os módulos quebrados pela importação do webpack no servidor, e o cliente os buscará com a mesma facilidade. Para fazer isso, o servidor precisa fazer o download de todos os 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}`) })) ... 
Os módulos em si são muito fáceis de conectar. Dê uma olhada no 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> ) } 
O React-loadable carrega assincronamente o componente Home, deixando claro para o webpack que ele deve ser chamado de Home (sim, esse é um caso raro quando os comentários fazem algum sentido). delay: 300 significa que, se após 300 ms o componente ainda não carregar, será necessário mostrar que o download ainda está em andamento. Ele lida com o carregamento:
src / Loading.js
 import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress'  
Para deixar claro para o servidor quais módulos estamos importando, precisaríamos registrar:
 Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], }); 
Mas, para não repetir o mesmo código, existe um plugin react - loadable / babel ao qual já nos conectamos com sucesso .babelrc . Agora que o servidor sabe o que importar, você precisa descobrir o que será renderizado. O fluxo de trabalho é um pouco como o capacete:
server / server.js
 import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = []  
Para garantir que o cliente carregue todos os módulos renderizados no servidor, precisamos correlacioná-los com os pacotes configuráveis criados pelo webpack. Para fazer isso, faça alterações na configuração do coletor. O plug-in react-loadable / webpack grava todos os módulos em um arquivo separado. Também devemos dizer ao webpack para salvar corretamente os módulos após a importação dinâmica - no objeto de saída.
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', }) ] 
Escrevemos os módulos no modelo, carregando-os por sua 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 } 
Resta apenas processar a parte do cliente. O método Loadable.preloadReady() carrega todos os módulos que o servidor forneceu ao usuário com antecedência.
src / client.js
 import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) }) 
Feito! Começamos e analisamos o resultado - na última parte, o pacote configurável era apenas um arquivo - client.js pesando 265kb e agora existem três arquivos, o maior dos quais pesa 215kb. Desnecessário dizer que a velocidade de carregamento da página aumentará significativamente ao dimensionar um projeto?

4. contador Redux
Agora vamos começar a resolver problemas práticos. Como resolver o dilema quando o servidor possui dados (digamos, de um banco de dados), é necessário exibi-lo para que os robôs de pesquisa possam encontrar conteúdo e, em seguida, usar esses dados no cliente.
Existe uma solução. É usado em quase todos os artigos de SSR, mas a maneira como é implementada lá está longe de ser sempre passível de boa escalabilidade. Em palavras simples, seguindo a maioria dos tutoriais, você não poderá criar um site real com SSR com o princípio "Um, Dois e Produção". Agora vou tentar pontilhar o i.
Precisamos apenas de redux . O fato é que o redux possui uma loja global, que podemos transferir do servidor para o cliente com o clique de um dedo.
Agora o importante (!): Temos um motivo para ter um arquivo server / stateRoutes . Ele gerencia o objeto initialState que é gerado lá, uma loja é criada a partir dele e depois passada para o modelo HTML. O cliente recupera esse objeto da window.__STATE__ , window.__STATE__ loja e é isso. Parece fácil.
Instalar:
 npm i redux react-redux 
Siga os passos acima. Aqui, na maioria das vezes, repetição de código usado anteriormente.
Contador de processamento de servidor e 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 = {}) { ...  
Temos loja no cliente. src / client.js
 import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...  
 A lógica de redux no SSR acabou. Agora, o trabalho usual com o redux é criar uma loja, ações, redutores, conexão e muito mais. Espero que isso fique claro sem muita explicação. Caso contrário, leia a documentação .
Redux inteiro aquisrc / 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 , .
 , — . . , , 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.
 . : 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 } 
Feito! https, , gif demo .
7.
MWA. , , . , SSR Code Splitting, PWA .
, MWA - web.dev :

, — . , , — .
, MWA — opensource . , , !
Boa sorte