Modern Web AppããŒãããæ§ç¯ããåã«ãModern Webã¢ããªã±ãŒã·ã§ã³ãšã¯äœããç解ããå¿
èŠããããŸããïŒ
Modern Web AppïŒMWAïŒã¯ãææ°ã®ãã¹ãŠã®Webæšæºã«æºæ ããã¢ããªã±ãŒã·ã§ã³ã§ãã ãã®äžã§ããããã°ã¬ãã·ãWebã¢ããªã¯ãã¢ãã€ã«ãã©ãŠã¶ãŒããŒãžã§ã³ãæºåž¯é»è©±ã«ããŠã³ããŒãããæ¬æ Œçãªã¢ããªã±ãŒã·ã§ã³ãšããŠäœ¿çšããæ©èœã§ãã ãŸããã¢ãã€ã«ããã€ã¹ãšã³ã³ãã¥ãŒã¿ãŒã®äž¡æ¹ãããµã€ãããªãã©ã€ã³ã§ã¹ã¯ããŒã«ããããšãã§ããŸãã ã¢ãã³ãªçŽ æãã¶ã€ã³ã å®å
šãªæ€çŽ¢ãšã³ãžã³æé©å; ãããŠåœç¶-é«éããŠã³ããŒãã

MWAã§èµ·ããããšã¯æ¬¡ã®ãšããã§ãïŒãã®ããã²ãŒã·ã§ã³ãèšäºã§äœ¿çšããããšããå§ãããŸãïŒã
Habréã®ãŠãŒã¶ãŒã¯ããžãã¹ã§ãããããããã«GitHubãªããžã㪠ãåéçºæ®µéã®ã¢ãŒã«ã€ã ãããã³ãã¢ãžã®ãªã³ã¯ããã£ããããŠãã ããã ãã®èšäºã¯ãnode.jsã«ç²ŸéããŠåå¿ããéçºè
ã察象ãšããŠããŸãã å¿
èŠãªãã¹ãŠã®çè«ã¯ãå¿
èŠãªããªã¥ãŒã ã«ç€ºãããŠããŸãã ãªã³ã¯ãã¯ãªãã¯ããŠãèŠéãåºããŠãã ããã
ããå§ããŸãããïŒ
1.ãŠãããŒãµã«
æšæºã¢ã¯ã·ã§ã³ïŒäœæ¥ãã£ã¬ã¯ããªãäœæããŠgit init
ãå®è¡ãgit init
ã package.jsonãéããæ°è¡ãè¿œå ããŸãã
"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
ãå®è¡ãã npm install
äžã«ç解ããŸãã
2018幎ãš2019幎ã®å€ããç®ã«ãªã£ãŠãããããWebã¢ããªã±ãŒã·ã§ã³ã¯ãŠãããŒãµã«ïŒãŸãã¯ååïŒã«ãªããè£é¢ãšè¡šé¢ã®äž¡æ¹ã«ES2017以äžã®ECMAScriptããŒãžã§ã³ããããŸãã ãããè¡ãã«ã¯ã index.js ïŒã¢ããªã±ãŒã·ã§ã³å
¥åãã¡ã€ã«ïŒãbabel / registerãæ¥ç¶ããããã«ç¶ããã¹ãŠã®ESã³ãŒããbabelããã®å Žã§JavaScriptã«å€æãããã©ãŠã¶ãŒãbabel / preset-envããã³babel / preset-reactã䜿çšããŠç解ã§ããããã«ããŸãã éçºã®å©äŸ¿æ§ã®ããã«ãéåžžã¯babel-plugin-root-importãã©ã°ã€ã³ã䜿çšããŸããããã«ãããã«ãŒããã£ã¬ã¯ããªããã®ãã¹ãŠã®ã€ã³ããŒã㯠'ã/'ããã³src /-'ïŒ/'ã®ããã«ãªããŸãã ãŸãã¯ãé·ããã¹ãæå®ãããã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/" }] }] ] }
Webpackãã»ããã¢ããããæéã§ãã webpack.config.jsãäœæããã³ãŒãã䜿çšããŸãïŒä»¥éãã³ãŒãå
ã®ã³ã¡ã³ãã«æ³šæããŠãã ããïŒã
const path = require('path'); module.exports = {
ãã®ç¬éããã楜ãã¿ãå§ãŸããŸãã ã¢ããªã±ãŒã·ã§ã³ã®ãµãŒããŒåŽãéçºããæãæ¥ãŸããã ãµãŒããŒåŽã¬ã³ããªã³ã° ïŒSSRïŒã¯ãWebã¢ããªã±ãŒã·ã§ã³ã®èªã¿èŸŒã¿ãé«éåããã·ã³ã°ã«ããŒãžã¢ããªã±ãŒã·ã§ã³ïŒSPAã®SEOïŒã®æ€çŽ¢ãšã³ãžã³æé©åã«é¢ããæ°žé ã®è°è«ã解決ããããã«èšèšãããæè¡ã§ãã ãããè¡ãã«ã¯ãHTMLãã³ãã¬ãŒããååŸãããã®äžã«ã³ã³ãã³ããå
¥ããŠãŠãŒã¶ãŒã«éä¿¡ããŸãã ãµãŒããŒã¯ãããéåžžã«è¿
éã«è¡ããŸã-ããŒãžã¯æ°ããªç§ã§æç»ãããŸãã ãã ãããµãŒããŒäžã®DOMãæäœããæ¹æ³ã¯ãªããããã¢ããªã±ãŒã·ã§ã³ã®ã¯ã©ã€ã¢ã³ãéšåã¯ããŒãžãæŽæ°ããæçµçã«ã€ã³ã¿ã©ã¯ãã£ãã«ãªããŸãã ããã§ãã éçºäžã§ãïŒ
app.js
import express from 'express' import path from 'path' import stateRoutes from './server/stateRoutes'
ãµãŒããŒ/ stateRoutes.js
import ssr from './server' export default function (app) {
server / server.jsãã¡ã€ã«ã¯ãreactã«ãã£ãŠçæãããã³ã³ãã³ããåéããHTMLãã³ãã¬ãŒã- /server/ template.jsã«æž¡ããŸã ã ããŒãäžã«ããŒãžã®URLãå€æŽããããªãã®ã§ããµãŒããŒãéçã«ãŒã¿ãŒã䜿çšããããšãæ確ã«ãã䟡å€ããããŸãã ãããŠã react-helmetã¯ãã¡ã¿ããŒã¿ïŒããã³å®éã«ã¯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) {
ãµãŒããŒ/ template.jsã®ãããã§ããã«ã¡ããããããŒã¿ãå°å·ãããã¡ãã³ã³ãéçãã£ã¬ã¯ããª/ã¢ã»ããã®ã¹ã¿ã€ã«ãæ¥ç¶ããŸãã æ¬æã§ã¯ã/ publicãã©ã«ããŒã«ããcontent.webpackãã³ãã«client.jsã§ãããéçãªã®ã§ãã«ãŒããã£ã¬ã¯ããªã®ã¢ãã¬ã¹/client.jsã«ç§»åããŸãã
ãµãŒããŒ/ template.js
ç§ãã¡ã¯åçŽãªã¯ã©ã€ã¢ã³ãåŽã«ç®ãåããŸãã src / client.jsãã¡ã€ã«ã¯ãDOMãæŽæ°ããã«ãµãŒããŒã«ãã£ãŠçæãããHTMLã埩å
ããã€ã³ã¿ã©ã¯ãã£ãã«ããŸãã ïŒè©³çŽ°ã¯ãã¡ã ïŒã æ°Žåç©åå¿æ©èœããããè¡ããŸãã ãããŠä»ã§ã¯ãéçã«ãŒã¿ãŒãšã¯äœã®é¢ä¿ããããŸããã éåžžã®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') )
ãã§ã«2ã€ã®ãã¡ã€ã«ã§ãã¢ããªã®åå¿ã³ã³ããŒãã³ããç¹ç¯ããŸããã ããã¯ãã«ãŒãã£ã³ã°ãå®è¡ãããã¹ã¯ãããã¢ããªã±ãŒã·ã§ã³ã®ã¡ã€ã³ã³ã³ããŒãã³ãã§ãã ãã®ã³ãŒãã¯éåžžã«äžè¬çã§ãïŒ
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> ) }
ããŠã src / app /Home.jsã ãã«ã¡ããã®ä»çµã¿ã«æ³šæããŠãã ãã-headã¿ã°ã®éåžžã®ã©ãããŒã§ãã
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> ) }
ããã§ãšãããããŸãïŒ MWAã®éçºã®æåã®éšåãå解ããŸããïŒ ãã®å
šäœããã¹ãããããã«ãã»ãã®æ°åã®ã¿ãããæ®ã£ãŠããŸãã çæ³çã«ã¯ã / assetsãã©ã«ããŒã«ããã³ãã¬ãŒãïŒserver / template.jsïŒã«å¿ããã°ããŒãã«ã¹ã¿ã€ã«ãã¡ã€ã«ãšãã¡ãã³ã³ãå
¥åã§ããŸãã ãŸããã¢ããªã±ãŒã·ã§ã³ã®èµ·åã³ãã³ãããããŸããã 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" }
ProdãšDevã®2ã€ã®ã«ããŽãªã®ã³ãã³ãããããŸãã ãããã¯ãwebpack v4æ§æãç°ãªããŸãã --mode
ã«ã€ããŠ--mode
ã ããã§èªã䟡å€ããããŸã ã
localhostïŒ3000ã§çµæã®ãŠãããŒãµã«ã¢ããªã±ãŒã·ã§ã³ãå¿
ãè©ŠããŠãã ãã
2.ãããªã¢ã«UI
ãã¥ãŒããªã¢ã«ã®ãã®éšåã§ã¯ãmaterial-uiã©ã€ãã©ãªã®SSRã䜿çšããŠWebã¢ããªã±ãŒã·ã§ã³ã«æ¥ç¶ããããšã«çŠç¹ãåœãŠãŸãã ãªã圌女ãªã®ãïŒ ãã¹ãŠãã·ã³ãã«ã§ã-ã©ã€ãã©ãªã¯ç©æ¥µçã«éçºãä¿å®ãããŠãããåºç¯ãªããã¥ã¡ã³ãããããŸãã ããã䜿çšããŠãã€ã°ãåãã ãã®çŸãããŠãŒã¶ãŒã€ã³ã¿ãŒãã§ã€ã¹ãæ§ç¯ã§ããŸãã
ããã§ãã¢ããªã±ãŒã·ã§ã³ã«é©ããæ¥ç¶ã¹ããŒã èªäœã«ã€ããŠèª¬æããŸã ã ããããã£ãŠã¿ãŸãããã
å¿
èŠãªäŸåé¢ä¿ãã€ã³ã¹ããŒã«ããŸãã
npm i @material-ui/core jss react-jss
次ã«ãæ¢åã®ãã¡ã€ã«ã«å€æŽãå ããå¿
èŠããããŸãã server / server.jsã§ã¯ãã¢ããªã±ãŒã·ã§ã³ãJssProviderãšMuiThemeProviderã§ã©ããããŸãããããã¯ããããªã¢ã«UIã³ã³ããŒãã³ããšãéåžžã«éèŠãªããšã«ãHTMLãã³ãã¬ãŒãã«é
眮ããå¿
èŠãããsheetsRegistryãªããžã§ã¯ã-cssãæäŸããŸãã ã¯ã©ã€ã¢ã³ãåŽã§ã¯ãMuiThemeProviderã®ã¿ã䜿çšããããŒããªããžã§ã¯ããæäŸããŸãã
ãµãŒããŒããã³ãã¬ãŒããã¯ã©ã€ã¢ã³ãserver / server.js
import React from 'react' import { renderToString } from 'react-dom/server' import { StaticRouter } from 'react-router-dom' import { Helmet } from 'react-helmet'
ãµãŒããŒ/ 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'
ããã§ãå°ãã¹ã¿ã€ãªãã·ã¥ãªãã¶ã€ã³ãããŒã ã³ã³ããŒãã³ãã«è¿œå ããããšãææ¡ããŸãã å
¬åŒWebãµã€ãã§ãã¹ãŠã®material-uiã³ã³ããŒãã³ããèŠãããšãã§ããŸããããã§ã¯ãPaperãButtonãAppBarãToolbarãTypographyã§ååã§ãã
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> ) }
ããã§æ¬¡ã®ããã«ãªããŸãïŒ

3.ã³ãŒãåå²
TODOãªã¹ã以å€ã®ãã®ãäœæããå Žåã¯ãclient.jsãã³ãã«ã«æ¯äŸããŠã¢ããªã±ãŒã·ã§ã³ãå¢å ããŸãã ãŠãŒã¶ãŒã«ããããŒãžã®é·æéã®ããŒããåé¿ããããã«ãé·ãéã³ãŒãåå²ãçºæãããŠããŸããã ãã ããReact-routerã®äœæè
ã®1人ã§ããRyan Florenceã¯ã次ã®ãã¬ãŒãºã§æœåšçãªéçºè
ãæããããŠããŸããã
ãµãŒããŒã¬ã³ããªã³ã°ãããã³ãŒãåå²ãããã¢ããªãè©Šã人ã¯ãGodspeedã䜿çšããŠãã ããã
ã³ãŒãåââå²ã䜿çšããŠssrã¢ããªã±ãŒã·ã§ã³ãäœæããããšã«ãããã¹ãŠã®äººã«å¹žéã
ç§ãã¡ã¯æéãããŸãã-ç§ãã¡ã¯ãããããŸãïŒ å¿
èŠãªãã®ãã€ã³ã¹ããŒã«ããŸãã
npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable
åé¡ã¯1ã€ã®é¢æ°ã ãã§ã-ã€ã³ããŒãã Webpackã¯ãã®éåæåçã€ã³ããŒãæ©èœããµããŒãããŠããŸãããããã«ã®ã³ã³ãã€ã«ã¯å€§ããªåé¡ã«ãªããŸãã 幞ããªããšã«ã2018幎ãŸã§ã«ãããã«å¯ŸåŠããããã®å³æžé€šãå°çããŸããã babel / plugin-syntax-dynamic-importããã³babel-plugin-dynamic-import-nodeã¯ã "Unexpected token when using import()"
ãšã©ãŒããç§ãã¡ãæããŸãã 1ã€ã®ã¿ã¹ã¯ã«2ã€ã®ã©ã€ãã©ãªãããã®ã¯ãªãã§ããïŒ dynamic-import-nodeã¯ç¹ã«ãµãŒããŒã¬ã³ããªã³ã°ã«å¿
èŠã§ããããµãŒããŒäžã®ã€ã³ããŒãããã®å Žã§ååŸããŸãã
index.js
require("@babel/register")({ plugins: ["@babel/plugin-syntax-dynamic-import", "dynamic-import-node"] }); require("./app");
åæã«ãã°ããŒãã«babelæ§æãã¡ã€ã«.babelrcãå€æŽããŸã
"plugins": [ "@babel/plugin-syntax-dynamic-import", "react-loadable/babel", ... ]
ããã«åå¿ããŒãå¯èœãç»å ŽããŸãã ã åªããããã¥ã¡ã³ããåãããã®ã©ã€ãã©ãªã¯ããµãŒããŒäžã®webpackã€ã³ããŒãã«ãã£ãŠç Žæãããã¹ãŠã®ã¢ãžã¥ãŒã«ãåéããã¯ã©ã€ã¢ã³ãã¯ããããç°¡åã«éžæããŸãã ãããè¡ãã«ã¯ããµãŒããŒã¯ãã¹ãŠã®ã¢ãžã¥ãŒã«ãããŠã³ããŒãããå¿
èŠããããŸãã
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}`) })) ...
ã¢ãžã¥ãŒã«èªäœã¯éåžžã«ç°¡åã«æ¥ç¶ã§ããŸãã ã³ãŒããèŠãŠãã ããïŒ
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ã¯ãHomeã³ã³ããŒãã³ããéåæã«ããŒãããHomeãšåŒã°ããã¹ãã§ããããšãwebpackã«æããã«ããŸãïŒã¯ããããã¯ã³ã¡ã³ããæå³ãæããŸããªã±ãŒã¹ã§ãïŒã delay: 300
ã¯ã delay: 300
ããªç§åŸã«ã³ã³ããŒãã³ãããŸã ããŒããããªãå ŽåãããŠã³ããŒãããŸã é²è¡äžã§ããããšã瀺ãå¿
èŠãããããšãæå³ããŸãã ããŒããæ±ããŸãïŒ
src / Loading.js
import React from 'react' import CircularProgress from '@material-ui/core/CircularProgress'
ã€ã³ããŒãããã¢ãžã¥ãŒã«ããµãŒããŒã«æ確ã«ããã«ã¯ãç»é²ããå¿
èŠããããŸãã
Loadable({ loader: () => import('./Bar'), modules: ['./Bar'], webpack: () => [require.resolveWeak('./Bar')], });
ããããåãã³ãŒããç¹°ãè¿ããªãããã«ãæ¢ã«.babelrcã«æ£åžžã«æ¥ç¶ããåå¿ããŒãå¯èœãª/ babelãã©ã°ã€ã³ããããŸãã ãµãŒããŒãã€ã³ããŒããããã®ãèªèããã®ã§ãã¬ã³ããªã³ã°ããããã®ãèŠã€ããå¿
èŠããããŸãã ã¯ãŒã¯ãããŒã¯ãã«ã¡ããã«å°ã䌌ãŠããŸãïŒ
server / server.js
import Loadable from 'react-loadable' import { getBundles } from 'react-loadable/webpack' import stats from '~/public/react-loadable.json' ... let modules = []
ã¯ã©ã€ã¢ã³ãããµãŒããŒã§ã¬ã³ããªã³ã°ããããã¹ãŠã®ã¢ãžã¥ãŒã«ãããŒãããããšã確èªããã«ã¯ãwebpackã«ãã£ãŠäœæããããã³ãã«ãšããããçžé¢ãããå¿
èŠããããŸãã ãããè¡ãã«ã¯ãã³ã¬ã¯ã¿ãŒã®æ§æãå€æŽããŸãã react-loadable / webpackãã©ã°ã€ã³ã¯ããã¹ãŠã®ã¢ãžã¥ãŒã«ãåå¥ã®ãã¡ã€ã«ã«æžã蟌ã¿ãŸãã ãŸããåçãªããžã§ã¯ããã€ã³ããŒããªããžã§ã¯ãã«åºåããåŸãã¢ãžã¥ãŒã«ãæ£ããä¿åããããwebpackã«æ瀺ããå¿
èŠããããŸãã
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', }) ]
ãã³ãã¬ãŒãã«ã¢ãžã¥ãŒã«ãèšè¿°ããé çªã«ããŒãããŸãã
ãµãŒããŒ/ 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 }
ã¯ã©ã€ã¢ã³ãéšåãåŠçããããã ãã«æ®ããŸãã Loadable.preloadReady()
ã¡ãœããã¯ããµãŒããŒãäºåã«ãŠãŒã¶ãŒã«æäŸãããã¹ãŠã®ã¢ãžã¥ãŒã«ãããŒãããŸãã
src / client.js
import Loadable from 'react-loadable' Loadable.preloadReady().then(() => { hydrate( <MuiThemeProvider theme={theme}> <BrowserRouter> <App/> </BrowserRouter> </MuiThemeProvider>, document.querySelector('#app') ) })
ã§ããïŒ éå§ããŠçµæã確èªããŸã-æåŸã®éšåã§ã¯ããã³ãã«ã¯1ã€ã®ãã¡ã€ã«ã®ã¿ã§ãã-client.jsã¯265kbã®éãã§ãçŸåšã¯3ã€ã®ãã¡ã€ã«ãããããã®ãã¡æ倧ã®éãã¯215kbã§ãã èšããŸã§ããªãããããžã§ã¯ãã®ã¹ã±ãŒãªã³ã°æã«ããŒãžã®èªã¿èŸŒã¿é床ã倧å¹
ã«åäžããŸããïŒ

4. Reduxã«ãŠã³ã¿ãŒ
ä»ãç§ãã¡ã¯å®éçãªåé¡ã解決ãå§ããŸãã ãµãŒããŒã«ããŒã¿ãããå ŽåïŒããšãã°ãããŒã¿ããŒã¹ããïŒã®ãžã¬ã³ãã解決ããæ¹æ³ã¯ãæ€çŽ¢ããããã³ã³ãã³ããèŠã€ããããããã«è¡šç€ºããå¿
èŠããããã¯ã©ã€ã¢ã³ãã§ãã®ããŒã¿ã䜿çšããŸãã
解決çããããŸãã ã»ãšãã©ãã¹ãŠã®SSRèšäºã§äœ¿çšãããŠããŸããããã®å®è£
æ¹æ³ã¯ãåžžã«åªããæ¡åŒµæ§ãåãå
¥ããããŸããã ç°¡åãªèšèã§èšãã°ãã»ãšãã©ã®ãã¥ãŒããªã¢ã«ã«åŸã£ãŠãã1ã2ãããã³æ¬çªãã®ååã«åºã¥ããŠSSRã§å®éã®ãµã€ããäœæããããšã¯ã§ããŸããã ä»ãç§ã¯ãããããããããããšããŸãã
reduxã®ã¿ãå¿
èŠã§ãã å®éãreduxã«ã¯ã°ããŒãã«ã¹ãã¢ããããæã§ã¯ãªãã¯ããã ãã§ãµãŒããŒããã¯ã©ã€ã¢ã³ãã«è»¢éã§ããŸãã
ä»éèŠïŒïŒïŒïŒ server / stateRoutesãã¡ã€ã«ãæã€çç±ããããŸã ã ããã§çæãããinitialStateãªããžã§ã¯ãã管çããããããã¹ãã¢ãäœæããŠãããHTMLãã³ãã¬ãŒãã«æž¡ããŸãã ã¯ã©ã€ã¢ã³ãã¯ãã®ãªããžã§ã¯ããwindow.__STATE__
ããååŸãwindow.__STATE__
ã¹ãã¢ãåwindow.__STATE__
ããŸãã ç°¡åããã§ãã
ã€ã³ã¹ããŒã«ïŒ
npm i redux react-redux
äžèšã®æé ã«åŸã£ãŠãã ããã ããã§ãã»ãšãã©ã®å Žåã以åã«äœ¿çšãããã³ãŒãã®ç¹°ãè¿ãã
ãµãŒããŒããã³ã¯ã©ã€ã¢ã³ãåŠçã«ãŠã³ã¿ãŒserver / 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) {
ãµãŒããŒ/ template.js
export default function template(helmet, content = '', sheetsRegistry, bundles, initialState = {}) { ...
ã¯ã©ã€ã¢ã³ãã«ã¹ãã¢ãååŸããŸãã src / client.js
import Loadable from 'react-loadable' import { Provider } from 'react-redux' import configureStore from './redux/configureStore' ...
SSRã®reduxããžãã¯ã¯çµäºããŸããã çŸåšãreduxã®éåžžã®äœæ¥ã¯ãã¹ãã¢ãã¢ã¯ã·ã§ã³ãã¬ãã¥ãŒãµãŒãæ¥ç¶ãªã©ãäœæããããšã§ãã ç§ã¯ãããå€ãã®èª¬æãªãã§æ確ã«ãªãããšãé¡ã£ãŠããŸãã ããã§ãªãå Žåã¯ã ããã¥ã¡ã³ãããèªã¿ãã ããã
ããã§ã®å
šäœã®éå
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ãšã³ãŒãåå²ã®æ¥ç¶æ¹æ³ã2ã¹ãããã§PWAãäœæããæ¹æ³ããµãŒããŒã¬ã³ããªã³ã°ã䜿çšããŠãµãŒããŒããã¯ã©ã€ã¢ã³ãã«ããŒã¿ã転éããæ¹æ³ãGoogleã§æ€çŽ¢ããå¿
èŠããªããªããŸããã
ãšããã§ããããã¯æè¿äœæãããweb.devãŠã§ããµã€ãã«ãã£ãŠçæãããMWAã®çµ±èšã§ãïŒ

ãã®èšäºã泚ææ·±ãèªãã ããããªãã¯ã¢ã³ã¹ã¿ãŒã§ããããã«å ããŠãgithubã®ã¢ã¹ã¿ãªã¹ã¯ã§ãµããŒãããããšãã§ããŸãããæè¯ã®ãµããŒãã¯ç§ã®ã³ãŒãã®æ倧ã®æäœã§ãã
ãšããã§ãMWAã¯ãªãŒãã³ãœãŒã¹ãããžã§ã¯ãã§ãã䜿çšãé
åžãæ¹åïŒ
é 匵ã£ãŠ