{ res.send("
赞
踩
我们的页面DOM结构由服务端产生的,就是服务端渲染。
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("<html><body><h2>hello</h2></body></html>");
});
app.listen(8300, () => {
console.log("程序运行在8300接口");
});
同构渲染的项目支持服务端渲染和客户端渲染。
第一次访问是服务端渲染(ssr),后边的路由切换访问是客户端渲染(SPA),可以支持爬虫(SEO)。
客户端和服务端同构可以复用一部分代码。
npm install react react-dom webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react --save
const path = require("path"); module.exports = { mode: "development", devtool: false, entry: "./src/index.js", output: { filename: "main.js", path: path.resolve(__dirname, "build"), }, watch: true, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], }, }, ], }, };
"scripts": {"build": "webpack"}
此过程类似于银耳,长成后晒干,然后加入水再泡发。
import React from "react";
import App from "./page/App/index.jsx";
import { hydrateRoot } from "react-dom/client";
const root = document.getElementById("root");
const element = <App />
hydrateRoot(root, element);
const express = require("express"); const register = require("@babel/register"); register({ ignore: [/node_modules/], presets: ["@babel/preset-env", "@babel/preset-react"], plugins: ["@babel/plugin-transform-modules-commonjs"], }); const static = require('serve-static'); const webpack = require("webpack"); const render = require("./oldRender"); const webpackConfig = require("./webpack.config"); webpack(webpackConfig, (error, status) => { const statusJson = status.toJson({ assets: true }); const assets = statusJson.assets.reduce((item, { name }) => { item[name] = `/${name}`; return item; }, {}); console.log(assets, 'assets') const app = express(); app.get("/", (req, res) => { render(req, res, assets); }); app.use(static('build')); app.listen(8100, () => { console.log("运行在8100端口"); }); });
import React from "react"; import App from "./src/page/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=utf-8"); 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>ssr</title> </head> <body> <div id="root">${html}</div> <script src="${assets["main.js"]}"></script> </body> </html>` ); } module.exports = render;
注意: 由于render函数中即使用了commonjs,又使用了es的代码,所以,servicejs文件中使用了@babel/register插件进行转换,将es转换成commonjs进行执行。
选择性水合,可以在局部进行水合。
像流水一样,打造一个从服务端到客户端的持续不断的渲染管线。而不是renderToString那样一次性渲染。
服务端渲染把简单的res.send改为res.socket
这样就把一次渲染转变为持续性行为。
打破了以前串行的限制,优化前端的加载速度和可交互所需等待时间。
服务器端的流式HTML使用 renderToPipeableStream
客户端的水合使用 hydrateRoot ,需要调用接口组件使用<Suspense/>
包裹。
需要请求的组件会先返回一个<template></template>
占位,然后再替换。
import React from "react"; import { renderToPipeableStream } from "react-dom/server"; import AppPage from "./src/page/AppPage/index.jsx"; import { StaticRouter } from "react-router-dom/server"; function newRender(req, res, assets) { const { pipe } = renderToPipeableStream( <StaticRouter location={req.url}> <AppPage /> </StaticRouter>, { bootstrapScripts: [assets["main.js"]], onShellReady() { res.statusCode = 200; res.setHeader("Content-Type", "text/html; charset=utf-8"); 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>ssr</title> </head> <body> <div id="root">`); pipe(res); res.write(`</div> </body> </html>`); }, } ); } module.exports = newRender;
import React from "react";
import AppPage from "./src/page/AppPage/index.jsx";
import { hydrateRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
const root = document.getElementById("root");
const element = (
<BrowserRouter>
<AppPage />
</BrowserRouter>
);
hydrateRoot(root, element);
import React, { Suspense } from "react";
import NavList from "../NavList/index.jsx";
import routerConfig from "../../route/index.js";
import { useRoutes } from "react-router-dom";
export default function AppPage() {
return (
<div>
<NavList />
<Suspense fallback={<div>加载中。。。</div>}>
{useRoutes(routerConfig)}
</Suspense>
</div>
);
}
import React from "react"; import Header from "../page/Header/index.jsx"; import Footer from "../page/Footer/index.jsx"; import User from "../page/User/index.jsx"; const routerConfig = [ { path: "/", element: <Header />, index: true, }, { path: "/footer", element: <Footer />, }, { path: "/user", element: <User />, }, ]; export default routerConfig;
import React, { startTransition } from "react"; import { useNavigate } from "react-router-dom"; export default function NavList() { const navigate = useNavigate(); function handleHistory(url) { startTransition(() => { navigate(url); }); } return ( <ul> <li onClick={() => handleHistory("/")}>主页</li> <li onClick={() => handleHistory("/footer")}>底部</li> <li onClick={() => handleHistory("/user")}>用户页</li> </ul> ); }
注意: 此处没有使用Link,原因是由于如果跳转过于频繁,Suspense中内容还没有渲染结束,会导致报错。需要使用startTransition来降低优先级。
npm install @babel/core @babel/preset-env @babel/preset-react babel-loader express react-router-dom webpack webpack-cli @babel/plugin-transform-modules-commonjs @babel/register cross-env nodemon react react-dom @babel/plugin-transform-runtime --save
const path = require("path"); module.exports = { mode: "development", devtool: 'source-map', entry: "./index.js", output: { filename: "main.js", path: path.resolve(__dirname, "build"), }, watch: true, module: { rules: [ { test: /\.(jsx|js)$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], plugins: ["@babel/plugin-transform-runtime"], }, }, }, ], }, };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。