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 = {
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'
server / stateRoutes.js
import ssr from './server' export default function (app) {
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) {
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
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 klienserver / 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'
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'
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( './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'
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 = []
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 klienserver / 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 = {}) { ...
Kami mendapatkan toko di klien. src / client.js
import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...
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 sinisrc / 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 }
Selesai! https, , gif demo .
7.
MWA. , , . , SSR Code Splitting, PWA .
, MWA - web.dev :

, — . , , — .
, MWA — opensource . , , !
Semoga beruntung