在从头开始构建Modern Web App之前,您需要弄清楚什么是Modern Web Application?
现代Web应用程序(MWA)是遵循所有现代Web标准的应用程序。 其中包括Progressive Web App-能够将移动浏览器版本下载到手机并用作完整的应用程序。 这也是从移动设备和计算机脱机滚动网站的机会。 现代材料设计; 完善的搜索引擎优化; 自然-高下载速度。

这是我们的MWA中将发生的事情(我建议您在文章中使用此导航):
Habré上的人都是商务人士,因此请立即获取GitHub存储库的链接,每个开发阶段的存档和演示 。 本文适用于熟悉node.js并做出反应的开发人员。 所有必要的理论都在必要的卷中列出。 通过单击链接拓宽您的视野。
让我们开始吧!
1.通用
标准操作:创建一个工作目录并执行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
,并且在安装时了解。
由于我们处于2018年和2019年之交,因此我们的Web应用程序将是通用的(或同构的) ,无论在背面还是在正面,都有不低于ES2017的ECMAScript版本。 为此, index.js (应用程序输入文件)连接babel / register,然后babel使用babel / preset-env和babel / preset-react即时将其后的所有ES代码转换为浏览器友好的JavaScript。 为了方便开发,我通常使用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.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) {
在server / template.js的头部,我们从头盔打印数据,连接图标,并从静态目录/资产中打印样式。 正文中是位于/ public文件夹中的content和webpack client.js捆绑包,但是由于它是静态的,因此我们转到根目录的地址-/client.js。
服务器/ template.js
我们转向简单的-客户端。 src / client.js文件可还原服务器生成的HTML,而无需更新DOM,并使其具有交互性。 (有关此更多信息 )。 水合物反应功能可以做到这一点。 现在,我们与静态路由器无关。 我们使用通常的一种-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') )
在两个文件中,应用程序的react组件已成功点亮。 这是执行路由的桌面应用程序的主要组件。 它的代码很普通:
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。 注意头盔的工作原理-常用的头部标签包装器。
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开发的第一部分! 为了测试整个过程,仅需触摸几下即可。 理想情况下,您可以根据模板-server / template.js用全局样式文件和图标图标填充/ assets文件夹。 我们也没有应用程序启动命令。 返回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。 它们在webpack v4配置中有所不同。 关于--mode
值得在这里阅读。
确保在本地主机上尝试生成的通用应用程序:3000
2. Material-ui
本教程的这一部分将重点介绍使用material-ui库的SSR连接到Web应用程序。 为什么是她? 一切都很简单-该库正在积极地开发,维护和提供大量文档。 有了它,您可以构建一个漂亮的用户界面来吐痰。
这里描述了适合我们应用的连接方案本身。 好吧,让我们做吧。
安装必要的依赖项:
npm i @material-ui/core jss react-jss
接下来,我们必须对现有文件进行更改。 在server / server.js中,我们将应用程序包装在JssProvider和MuiThemeProvider中,它们将提供Material-ui组件,并且非常重要的是,必须将sheetRegistry对象-css放置在HTML模板中。 在客户端,我们仅使用MuiThemeProvider,并为其提供主题对象。
服务器,模板和客户端服务器/ 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'
现在,我建议为Home组件添加一些时尚的设计。 您可以在其官方网站上查看所有material-ui组件,在这里Paper,Button,AppBar,Toolbar和Typography就足够了。
src /应用程序/ 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 /应用程序/ 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的创建者之一的Ryan Florence用他的话吓到了潜在的开发人员:
那些尝试使用服务器渲染的,代码分割的应用程序的人。
祝所有决定通过代码拆分创建ssr应用程序的人都好运
我们被排斥了-我们会做到的! 安装必要的:
npm i @babel/plugin-syntax-dynamic-import babel-plugin-dynamic-import-node react-loadable
问题只是一个功能-导入。 Webpack支持此异步动态导入功能,但是babel编译将是一个巨大的问题。 幸运的是,到2018年,图书馆已经来帮助解决这一问题。 babel / plugin-syntax-dynamic-import和babel-plugin-dynamic-import-node将使我们免于"Unexpected token when using import()"
错误。 为什么需要两个库来完成一项任务? 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组件,使Webpack清楚应将其称为Home(是的,当注释有意义时,这种情况很少见)。 delay: 300
表示如果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的react-loadable / babel插件。 现在服务器知道要导入的内容,您需要找出要渲染的内容。 工作流程有点像头盔:
服务器/ 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') ) })
做完了! 我们开始看一下结果-在最后一部分中,捆绑包只有一个文件-client.js重265kb,现在-3个文件,最大的文件重215kb。 不用说,在扩展项目时页面加载速度会大大提高吗?

4. Redux计数器
现在我们将开始解决实际问题。 当服务器具有数据(例如,来自数据库)时,如何解决难题,您需要显示它,以便搜索机器人可以找到内容,然后在客户端上使用此数据。
有一个解决方案。 几乎所有SSR文章都使用了它,但是在那里实现它的方式远非总是可取到良好的可伸缩性。 简而言之,按照大多数教程进行操作,您将无法使用“一,二和生产”的原理使用SSR创建真实的网站。 现在,我将尝试加点i。
我们只需要redux 。 事实是,redux有一个全局存储,我们可以通过单击将其从服务器转移到客户端。
现在很重要(!):我们有理由拥有一个server / stateRoutes文件 。 它管理在那里生成的initialState对象,从中创建一个存储,然后传递给HTML模板。 客户从window.__STATE__
检索该对象,重新window.__STATE__
商店,仅此而已。 看起来很容易。
安装:
npm i redux react-redux
请按照上述步骤。 在这里,大部分是重复使用以前的代码。
服务器和客户端处理计数器服务器/ stateRoutes.js :
import ssr from './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的通常工作是创建商店,动作,化简器,连接等。 我希望在没有太多解释的情况下将清楚这一点。 如果不是,请阅读文档 。
整个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 /应用程序/ 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.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 . , , !
祝你好运