Sebelum Anda - Bereaksi Aplikasi Web Modern

Sebelum Anda mulai membangun dari Aplikasi Web Modern, Anda perlu mencari tahu apa itu Aplikasi Web Modern?


Aplikasi Web Modern (MWA) adalah aplikasi yang mematuhi semua standar web modern. Di antara mereka, Aplikasi Web Progresif adalah kemampuan untuk mengunduh versi peramban seluler ke telepon Anda dan menggunakannya sebagai aplikasi yang lengkap. Ini juga merupakan kesempatan untuk menggulir situs secara offline baik dari perangkat seluler maupun dari komputer; desain material modern; optimisasi mesin pencari yang sempurna; dan tentu saja - kecepatan unduh yang tinggi.



Inilah yang akan terjadi di MWA kami (saya sarankan Anda menggunakan navigasi ini pada artikel):



Orang-orang di Habré adalah bisnis, jadi segera tangkap tautan ke repositori GitHub , arsip dari setiap tahap pengembangan dan demo . Artikel ini ditujukan untuk pengembang yang akrab dengan node.js dan bereaksi. Semua teori yang diperlukan disajikan dalam volume yang diperlukan. Perluas wawasan Anda dengan mengklik tautan.


Ayo mulai!



1. Universal


Tindakan standar: buat direktori kerja dan jalankan git init . Buka package.json dan tambahkan beberapa baris:


 "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" } 

Kami mengeksekusi npm install dan, ketika diinstal, kami mengerti.


Karena kita berada di pergantian tahun 2018 dan 2019, aplikasi web kita akan bersifat universal (atau isomorfis) , baik di bagian belakang maupun di bagian depan akan ada versi ECMAScript tidak lebih rendah dari ES2017. Untuk melakukan ini, index.js (file input aplikasi) menghubungkan babel / register, dan babel on-the-fly mengubah semua kode ES yang mengikutinya menjadi JavaScript yang ramah-browser menggunakan babel / preset-env dan babel / preset-react. Untuk kenyamanan pengembangan, saya biasanya menggunakan plugin babel-plugin-root-import, yang semua impor dari direktori root akan terlihat seperti '~ /', dan dari src / - '&' & / '. Atau, Anda dapat menulis jalur panjang atau menggunakan alias dari 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/" }] }] ] } 

Saatnya menyiapkan Webpack . Kami membuat webpack.config.js dan menggunakan kode (selanjutnya, perhatikan komentar dalam kode).


 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" } ] } } 

Mulai saat ini, kesenangan dimulai. Saatnya untuk mengembangkan sisi server dari aplikasi. Server-side Rendering (SSR) adalah teknologi yang dirancang untuk mempercepat pemuatan aplikasi web di kali dan memecahkan perdebatan abadi tentang optimasi mesin pencari dalam Aplikasi Halaman Tunggal (SEO di SPA). Untuk melakukan ini, kami mengambil templat HTML, memasukkan konten ke dalamnya dan mengirimkannya kepada pengguna. Server melakukan ini dengan sangat cepat - halaman diambil dalam hitungan milidetik. Namun, tidak ada cara untuk memanipulasi DOM di server, sehingga bagian klien dari aplikasi menyegarkan halaman, dan akhirnya menjadi interaktif. Oke Kami berkembang!


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) }) } 

File server / server.js mengumpulkan konten yang dihasilkan dengan bereaksi dan meneruskannya ke templat HTML - /server/template.js . Perlu diperjelas bahwa server menggunakan router statis, karena kami tidak ingin mengubah url halaman saat memuat. Dan reaksi helm adalah perpustakaan yang sangat menyederhanakan pekerjaan dengan metadata (dan memang dengan tag kepala).


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) } 

Di server / template.js, di kepala kita mencetak data dari helm, menghubungkan favicon, gaya dari direktori statis / aset. Di isi adalah bundel konten dan webpack client.js yang terletak di folder / publik, tetapi karena itu statis, kita pergi ke alamat direktori root - /client.js.


server / 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 } 

Kami beralih ke yang sederhana - sisi klien. File src / client.js mengembalikan HTML yang dihasilkan oleh server tanpa memperbarui DOM, dan menjadikannya interaktif. (Lebih lanjut tentang ini di sini ). Fungsi reaksi hidrat melakukan ini. Dan sekarang kita tidak ada hubungannya dengan router statis. Kami menggunakan yang biasa - 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') ) 

Sudah dalam dua file komponen reaksi Aplikasi berhasil menyala. Ini adalah komponen utama dari aplikasi desktop yang melakukan routing. Kode ini sangat umum:


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> ) } 

Nah, src / app / Home.js. Perhatikan cara kerja Helm - pembungkus tag kepala biasa.


 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> ) } 

Selamat! Kami memisahkan bagian pertama dari pengembangan MWA! Hanya beberapa sentuhan yang tersisa untuk menguji semuanya. Idealnya, Anda dapat mengisi folder / assets dengan file gaya global dan favicon sesuai dengan template - server / template.js. Kami juga tidak memiliki perintah peluncuran aplikasi. Kembali ke 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" } 

Anda mungkin memperhatikan dua kategori perintah - Prod dan Dev. Mereka berbeda dalam konfigurasi webpack v4. Tentang --mode layak dibaca di sini .
Pastikan untuk mencoba aplikasi universal yang dihasilkan di localhost: 3000



2. Bahan-ui


Bagian tutorial ini akan fokus pada koneksi ke aplikasi web dengan SSR dari pustaka material-ui. Kenapa dia? Semuanya sederhana - perpustakaan secara aktif mengembangkan, memelihara, dan memiliki dokumentasi yang luas. Dengan itu, Anda dapat membangun antarmuka pengguna yang indah hanya untuk meludah.


Skema koneksi itu sendiri, cocok untuk aplikasi kita, dijelaskan di sini . Baiklah, mari kita lakukan.


Instal dependensi yang diperlukan:


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

Selanjutnya kita harus melakukan perubahan pada file yang ada. Di server / server.js, kami membungkus aplikasi kami dalam JssProvider dan MuiThemeProvider, yang akan menyediakan komponen material-ui dan, yang sangat penting, objek sheetRegistry - css, yang harus ditempatkan dalam template HTML. Di sisi klien, kami hanya menggunakan MuiThemeProvider, menyediakannya dengan objek tema.


server, templat, dan klien

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) } 

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' //       ,     //         const theme = createMuiTheme({ palette: { primary: purple, secondary: { main: '#f44336', }, }, typography: { useNextVariants: true, }, }) //     hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) 

Sekarang saya mengusulkan untuk menambahkan sedikit desain yang stylish ke komponen Home. Anda dapat melihat semua komponen material-ui di situs web resmi mereka, di sini Kertas, Tombol, AppBar, Bilah Alat, dan Tipografi sudah cukup.


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> ) } 

Sekarang sesuatu seperti ini akan berubah:




3. Pemecahan Kode


Jika Anda berencana untuk menulis sesuatu lebih dari daftar TODO, maka aplikasi Anda akan meningkat secara proporsional ke bundel client.js. Untuk menghindari pemuatan halaman yang lama pada pengguna, pemisahan kode diciptakan untuk waktu yang lama. Namun, pernah Ryan Florence, salah satu pencipta React-router, takut pengembang potensial dengan kalimatnya:


Godspeed mereka yang mencoba aplikasi pemecah kode yang diberikan server.

Selamat mencoba semua orang yang memutuskan untuk membuat aplikasi ssr dengan pemecahan kode


Kami jijik - kami akan melakukannya! Instal yang diperlukan:


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

Masalahnya hanya satu fungsi - impor. Webpack mendukung fungsi impor dinamis asinkron ini, tetapi kompilasi babel akan menjadi masalah besar. Untungnya, pada tahun 2018, perpustakaan datang untuk membantu menangani hal ini. babel / plugin-syntax-dynamic-import dan babel-plugin-dynamic-import-node akan menyelamatkan kita dari kesalahan "Unexpected token when using import()" . Mengapa dua perpustakaan untuk satu tugas? dynamic-import-node diperlukan khusus untuk rendering server, dan akan mengambil impor pada server dengan cepat:


index.js


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

Pada saat yang sama, kami memodifikasi file konfigurasi babel global .babelrc


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

Di sini muncul reaksi yang dapat dimuat . Perpustakaan ini dengan dokumentasi yang sangat baik akan mengumpulkan semua modul yang rusak oleh impor webpack di server, dan klien akan mengambilnya dengan mudah. Untuk melakukan ini, server perlu mengunduh semua modul:


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}`) })) ... 

Modul-modul itu sendiri sangat mudah dihubungkan. Lihatlah kodenya:


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 memuat secara asinkron komponen Home, membuatnya jelas ke webpack bahwa itu harus disebut Home (ya, ini adalah kasus yang jarang terjadi ketika komentar masuk akal). delay: 300 berarti bahwa jika setelah 300ms komponen masih tidak dimuat, Anda perlu menunjukkan bahwa unduhan masih berlangsung. Ini berkaitan dengan Memuat:


src / Memuat.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 } } 

Untuk membuatnya jelas ke server modul mana yang kita impor, kita perlu mendaftar:


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

Tetapi agar tidak mengulangi kode yang sama, ada plugin react-loadable / babel yang telah berhasil kita hubungkan ke .babelrc . Sekarang server tahu apa yang harus diimpor, Anda perlu mencari tahu apa yang akan diberikan. Alur kerjanya sedikit seperti Helm:


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) 

Untuk memastikan bahwa klien memuat semua modul yang diberikan di server, kami perlu menghubungkannya dengan bundel yang dibuat oleh webpack. Untuk melakukan ini, buat perubahan pada konfigurasi kolektor. Plugin react-loadable / webpack menulis semua modul ke file terpisah. Kita juga harus memberi tahu webpack untuk menyimpan modul dengan benar setelah impor dinamis - di objek keluaran.


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', }) ] 

Kami menulis modul dalam template, memuatnya secara bergantian:


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 } 

Tetap hanya memproses bagian klien. Metode Loadable.preloadReady() memuat semua modul yang diberikan server kepada pengguna sebelumnya.


src / client.js


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

Selesai! Kami mulai dan melihat hasilnya - pada bagian terakhir bundel hanya satu file - client.js dengan berat 265kb, dan sekarang ada 3 file, yang terbesar di antaranya memiliki berat 215kb. Tidak perlu dikatakan, kecepatan memuat halaman akan meningkat secara signifikan saat menskalakan proyek?




4. Penghitung redux


Sekarang kita akan mulai memecahkan masalah praktis. Bagaimana menyelesaikan dilema ketika server memiliki data (katakanlah, dari database), Anda perlu menampilkannya sehingga bot pencarian dapat menemukan konten, dan kemudian menggunakan data ini pada klien.


Ada solusinya. Ini digunakan di hampir setiap artikel SSR, tetapi cara penerapannya di sana jauh dari selalu dapat diterima untuk skalabilitas yang baik. Dengan kata-kata sederhana, mengikuti sebagian besar tutorial, Anda tidak akan dapat membuat situs nyata dengan SSR pada prinsip "Satu, Dua, dan Produksi". Sekarang saya akan mencoba untuk dot i.


Kami hanya perlu redux . Faktanya adalah redux memiliki toko global, yang dapat kita transfer dari server ke klien dengan mengklik jari.
Sekarang yang penting (!): Kami memiliki alasan untuk memiliki file server / stateRoutes . Ia mengelola objek initialState yang dihasilkan di sana, toko dibuat darinya, dan kemudian diteruskan ke templat HTML. Klien mengambil objek ini dari window.__STATE__ , window.__STATE__ kembali toko, dan hanya itu. Sepertinya mudah.


Pasang:


 npm i redux react-redux 

Ikuti langkah-langkah di atas. Di sini, untuk sebagian besar, pengulangan kode yang digunakan sebelumnya.


Penghitung pemrosesan server dan klien

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) } 

server / 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 } 

Kami mendapatkan toko di klien. 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') ) }) 

Logika redux di SSR telah berakhir. Sekarang, pekerjaan biasa dengan redux adalah membuat toko, aksi, reduksi, terhubung, dan banyak lagi. Saya harap ini akan jelas tanpa banyak penjelasan. Jika tidak, baca dokumentasinya .


Seluruh redux di sini

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 } 

Selesai! https, , gif demo .


7.


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


, MWA - web.dev :



, — . , , — .


, MWA — opensource . , , !


Semoga beruntung

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


All Articles