1.渲染模式 #

1.1 服务器渲染 #

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

1.2 客户端渲染 #

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

2. 什么是同构 #

3. 客户端渲染(CSR) #

3.1 安装 #

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

3.2 src\index.js #

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 />);

3.3 App.js #

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

3.4 Header.js #

src\Header.js

import React from 'react';
function Header() {
  return (
    <div onClick={() => alert('Header')}>Header</div>
  );
}
export default Header;

3.5 User.js #

src\User.js

import React from 'react';
function User() {
  return (
    <div>User</div>
  );
}
export default User;

3.6 Footer.js #

src\Footer.js

import React from 'react';
function Footer() {
  return (
    <div>Footer</div>
  );
}
export default Footer;

3.7 webpack.config.js #

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/
      },
    ],
  },
}

3.8 package.json #

package.json

{
  "scripts": {
    "build": "webpack"
  }
}

3.9 index.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>
  <script src="main.js"></script>
</body>
</html>

4. 水合 #




4.1 index.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>

4.2 src\index.js #

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 />);

5. React18之前的服务器端渲染(SSR) #

5.1 package.json #

{
  "scripts": {
    "build": "webpack",
+   "start": "cross-env NODE_ENV=development nodemon -- server.js"
  }
}

5.2 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'));
});

5.3 render.js #

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;

6. React18之后的服务器端渲染(SSR) #

6.1 render.js #

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;

6.2 App.js #

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'));

6.3 User.js #

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;
+      }
+    }
+  };
+}

7. useId #

7.1 src\Footer.js #

src\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;

8. 支持路由 #

8.1 安装 #

npm install react-router-dom --save

8.2 routesConfig.js #

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

8.3 Home.js #

src\routes\Home.js

import React from 'react';
function Home() {
  return (
    <div>
      Home
    </div>
  )
}
export default Home;

8.4 Counter.js #

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;

8.5 Header.js #

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

8.6 src\App.js #

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

8.7 src\index.js #

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

8.8 render.js #

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;