npm install express --save
render\client.js
let express = require('express');
let app = express();
app.get('/', (req, res) => {
res.send(`
<html>
<body>
<div id="root">hello</div>
</body>
</html>
`);
});
app.listen(8080);
client
let express = require('express');
let app = express();
app.get('/', (req, res) => {
res.send(`
<html>
<body>
<div id="root"></div>
<script>root.innerHTML = 'hello'</script>
</body>
</html>
`);
});
app.listen(8090);
npm install react react-dom --save
npm install webpack webpack-cli babel-loader @babel/preset-react --save-dev
npm install express cross-env nodemon @babel/register @babel/plugin-transform-modules-commonjs --save-dev
src\index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = document.getElementById('root');
createRoot(root).render(<App />);
src\App.js
import React from 'react';
import Header from './Header';
import User from './User';
import Footer from './Footer';
function App() {
return (
<>
<Header />
<User />
<Footer />
</>
);
}
export default App
src\Header.js
import React from 'react';
function Header() {
return (
<div onClick={() => alert('Header')}>Header</div>
);
}
export default Header;
src\User.js
import React from 'react';
function User() {
return (
<div>User</div>
);
}
export default User;
src\Footer.js
import React from 'react';
function Footer() {
return (
<div>Footer</div>
);
}
export default Footer;
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'main.js'
},
watch: true,
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: {
loader: 'source-map-loader',
}
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
}
},
exclude: /node_modules/
},
],
},
}
package.json
{
"scripts": {
"build": "webpack"
}
}
build\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>
build\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
+ <div id="root"><div>Header</div><div>User</div><div>Footer</div></div>
<script src="main.js"></script>
</body>
</html>
src\index.js
import React from 'react';
+import { createRoot, hydrateRoot } from 'react-dom/client';
import App from './App';
const root = document.getElementById('root');
-createRoot(root).render(<App />);
+hydrateRoot(root, <App />);
这个操作必须是整体性的,而水合的过程可能比较慢,会引起卡顿
node
的 require
办法,在代码里引入 @babel/register
模块后,所有通过require引入并且以.es6
、.es
、.jsx
、.mjs
和.js
为后缀名的模块都会被 Babel
转译{
"scripts": {
"build": "webpack",
+ "start": "cross-env NODE_ENV=development nodemon -- server.js"
}
}
server.js
const babelRegister = require('@babel/register');
babelRegister({
ignore: [/node_modules/],
presets: ["@babel/preset-react"],
plugins: ['@babel/plugin-transform-modules-commonjs'],
});
const webpack = require('webpack');
const express = require('express');
const static = require('serve-static');
const webpackConfig = require('./webpack.config');
const render = require('./render');
webpack(webpackConfig, (err, stats) => {
let statsJSON = stats.toJson({ assets: true });
const assets = statsJSON.assets.reduce((memo, { name }) => {
memo[name] = `/${name}`
return memo;
}, {});
const app = express();
app.get('/', async function (req, res) {
render(req, res, assets);
});
app.use(static('build'));
app.listen(8080, () => console.log('server started on port 8080'));
});
render.js
import React from 'react';
import App from "./src/App";
import { renderToString } from "react-dom/server";
function render(req, res, assets) {
const html = renderToString(<App />);
res.statusCode = 200;
res.setHeader("Content-type", "text/html;charset=utf8");
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">${html}</div>
<script src="${assets['main.js']}"></script>
</body>
</html>
`);
}
module.exports = render;
renderToString
那样一次性渲染机制res.send
改为res.socket
,这样的渲染就从单次行为转变为持续性行为renderToPipeableStream
Hydration
使用 <Suspense>
render.js
import React from 'react';
import App from "./src/App";
import { renderToPipeableStream } from "react-dom/server";
function render(req, res, assets) {
+ const { pipe } = renderToPipeableStream(
+ <App />,
+ {
+ bootstrapScripts: [assets['main.js']],
+ onShellReady() {
+ res.statusCode = 200;
+ res.setHeader("Content-type", "text/html;charset=utf8");
+ res.write(`
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+ </head>
+ <body><div id="root">`);
+ pipe(res);
+ res.write(`</div></body></html>`)
+ }
+ }
+ );
}
module.exports = render;
src\App.js
+import React, { Suspense } from 'react';
import Header from './Header';
import Footer from './Footer';
+import User from './User';
//const LazyUser = React.lazy(() => import('../User'));
function App() {
return (
<>
<Header />
+ <Suspense fallback={<div>loading User...</div>}>
+ <User />
+ </Suspense>
<Footer />
</>
);
}
export default App
<div hidden id="S:0">
<div>ID:1</div>
</div>
document.getElementById('B:0').replaceChildren(document.getElementById('S:0'));
src\User.js
import React from 'react';
+const userPromise = fetchUser(1);
+const userResource = wrapPromise(userPromise);
+function User() {
+ const user = userResource.read();
+ return <div>ID:{user.id}</div>
+}
+export default User;
+
+function fetchUser(id) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({ id });
+ }, 80000);
+ });
+}
+function wrapPromise(promise) {
+ let status = "pending";
+ let result;
+ let suspender = promise.then(
+ (r) => {
+ status = "success";
+ result = r;
+ },
+ (e) => {
+ status = "error";
+ result = e;
+ }
+ );
+ return {
+ read() {
+ if (status === "pending") {
+ throw suspender;
+ } else if (status === "error") {
+ throw result;
+ } else if (status === "success") {
+ return result;
+ }
+ }
+ };
+}
Math.random()
生成的ID在客户端、服务端不匹配useId
可以生成稳定、唯一的idsrc\Footer.js
import React, { useId } from 'react';
function Footer() {
+ const id = useId();
return (
<div>
+ <label htmlFor={id}>are you ok?</label>
+ <input type="checkbox" id={id} />
</div>
);
}
export default Footer;
npm install react-router-dom --save
src\routesConfig.js
import React from 'react';
import Home from './routes/Home';
import Counter from './routes/Counter';
export default [
{
path: '/',
element: <Home />,
index: true
},
{
path: '/counter',
element: <Counter />
}
]
src\routes\Home.js
import React from 'react';
function Home() {
return (
<div>
Home
</div>
)
}
export default Home;
src\routes\Counter.js
import React, { useState } from 'react';
function Counter() {
const [number, setNumber] = useState(0);
return (
<div>
<p>{number}</p>
<button onClick={() => setNumber(number + 1)}>+</button>
</div>
)
}
export default Counter;
src\components\Header.js
import React from 'react';
import { Link } from 'react-router-dom';
function Header() {
return (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/counter">Counter</Link></li>
</ul>
)
}
export default Header
src\App.js
import React from 'react';
+import { useRoutes } from 'react-router-dom';
+import routesConfig from './routesConfig';
+import Header from './components/Header';
function App() {
return (
+ <>
+ <Header />
+ {useRoutes(routesConfig)}
+ </>
);
}
export default App
src\index.js
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
+import { BrowserRouter } from 'react-router-dom';
const root = document.getElementById('root');
+hydrateRoot(root, <BrowserRouter><App /></BrowserRouter>);
render.js
import React from 'react';
+import { StaticRouter } from "react-router-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import App from "./src/App";
function render(req, res, assets) {
const { pipe } = renderToPipeableStream(
+ <StaticRouter location={req.url}><App /></StaticRouter>,
{
bootstrapScripts: [assets['main.js']],
onShellReady() {
res.statusCode = 200;
res.setHeader("Content-type", "text/html;charset=utf8");
res.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body><div id="root">`);
pipe(res);
res.write(`</div></body></html>`)
}
}
);
}
module.exports = render;