北鸟南游的博客 北鸟南游的博客
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
  • JavaScript

    • 原生js
    • vue
    • react
      • react的fiber架构
      • 从0实现vite+react的ssr服务端渲染
        • 0 首先创建项目
          • 在src下创建pages目录
          • 安装一些依赖
        • 1.本地开发阶段
          • 在App.jsx中添加路由
          • 在main.jsx中配置BrowserRouter
          • 创建server服务文件
          • 新建server-entry.jsx文件
          • 最后,给index.html标签添加标记并测试
        • 2.编译之后正式发布
          • 修改启动命令
          • 在server.js文件中添加production时的渲染
          • 测试项目
      • 基于React搭建threejs[最流行的3D库]环境
    • node
    • nextjs
    • 其它框架
  • 界面

  • front
  • javascript
  • react
北鸟南游
2024-04-27
目录

从0实现vite+react的ssr服务端渲染

在vite创建项目时,有支持创建ssr的命令。为了能够深入学习ssr实现原理,决定从零搭建服务端渲染的项目,了解了内部原理后,就可以切换使用不同的语言应用来启动服务。

# 0 首先创建项目

npm init vite@latest 选择react、javascript基本版本。 初始化完成,项目结构如下

├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
│   ├── App.css
│   ├── App.jsx
│   ├── assets
│   ├── index.css
│   └── main.jsx
└── vite.config.js
1
2
3
4
5
6
7
8
9
10
11
12

# 在src下创建pages目录

创建aaa.jsx和bbb.jsx文件

export default function AAA() {
  return <div>this is aaa;</div>;
}
1
2
3
export default function BBB() {
  return <div>this is bbb;</div>;
}
1
2
3

# 安装一些依赖

  • 路由react-router-dom
  • 创建服务器 express
  • 设置启动时的环境变量 cross-env
$ yarn add -D cross-env
$ yarn add react-router-dom express
1
2

最后,移除不需要的css文件,精简代码。

# 1.本地开发阶段

# 在App.jsx中添加路由

import { useState } from "react";
import { Link, Route, Routes } from "react-router-dom";
// 获取pages目录下的2个页面,用来做路由跳转
const pages = import.meta.globEager("./pages/*.jsx");
//生成 routes 对象
const routes = Object.keys(pages).map((path) => {
  const name = path.match(/\.\/pages\/(.*)\.jsx$/)[1];
  return {
    name,
    path: name === "Home" ? "/" : `/${name.toLowerCase()}`,
    component: pages[path].default,
  };
});
console.log(routes, "Routes");
function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <h1>Vite + React + ssr</h1>
      {/* ul列表,点击发生路由跳转 */}
      <ul>
        {routes.map(({ name, path }) => {
          return (
            <li key={path}>
              <Link to={path}>{name}</Link>
            </li>
          );
        })}
      </ul>
      {/* 显示路由对应的页面 */}
      <Routes>
        {routes.map(({ path, component: RouteComp }) => {
          return <Route key={path} path={path} element={<RouteComp />}></Route>;
        })}
      </Routes>
    </div>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 在main.jsx中配置BrowserRouter

BrowserRouter是前端显示的路由标签

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </BrowserRouter>
);
1
2
3
4
5
6
7
8
9
10
11
12

执行npm run dev命令启动项目后,可以看到 image.png

# 创建server服务文件

在项目下创建server.js文件

# 1:引入express,创建express实例

import express from "express";
const app = express();
app.listen(4000);
1
2
3

# 2:引入vite,使用createServer创建viteServer

// 通过vite创建server服务
const { createServer: createViteServer } = await import("vite");
//创建vite服务实例e
let vite = await createViteServer({
  server: { middlewareMode: true },
  appType: "custom",
  base: "/",
});
//使用vite的中间件
app.use(vite.middlewares);
1
2
3
4
5
6
7
8
9
10

# 3:监听所有路由

app.get("*", async (req, res) => {
  let template;
  let render;
  //   console.log("isProduction", isProduction);
  template = fs.readFileSync("index.html", "utf8");
  template = await vite.transformIndexHtml(req.url, template);
  render = (await vite.ssrLoadModule("/src/server-entry.jsx")).render;
  //   console.log(req.url, template,"render");
  const html = await render(req.url, ssrManifest);
  if (ssrManifest.url) {
    res.redirect(301, ssrManifest.url);
    return;
  }
  const responseHtml = template.replace("<!--APP_HTML-->", html);
  res.status(200).set({ "Content-Type": "text/html" }).end(responseHtml);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4: 完整的server.js代码

import fs from "fs";
import express from "express";
const app = express();

// 通过vite创建server服务
const { createServer: createViteServer } = await import("vite");
//创建vite服务实例e
let vite = await createViteServer({
  server: { middlewareMode: true },
  appType: "custom",
  base: "/",
});
//使用vite的中间件
app.use(vite.middlewares);

app.get("*", async (req, res) => {
  let template;
  let render;
  //   console.log("isProduction", isProduction);
  template = fs.readFileSync("index.html", "utf8");
  template = await vite.transformIndexHtml(req.url, template);
  // 在src下创建 server-entry.jsx 文件,做为ssr的入口文件,前端启动的入口文件为main.js,已经添加到了index.html中
  render = (await vite.ssrLoadModule("/src/server-entry.jsx")).render;
  //   console.log(req.url, template,"render");
  const html = await render(req.url);
  // 给index.html的id为root标签中添加 <!--APP_HTML-->,做为后边要替换的标志
  const responseHtml = template.replace("<!--APP_HTML-->", html);
  res.status(200).set({ "Content-Type": "text/html" }).end(responseHtml);
});
app.listen(4000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 新建server-entry.jsx文件

  • server-entry.jsx 作为服务端渲染的入口文件
  • main.jsx是前端渲染时的入口文件

StaticRouter从react-router-dom/server中导入,用于服务端渲染的路由

//服务端渲染的入口
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import App from "./App.jsx";

export function render(url, context) {
  return ReactDOMServer.renderToString(
    <StaticRouter location={url} context={context}>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </StaticRouter>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 最后,给index.html标签添加标记并测试

<div id="root"><!--APP_HTML--></div>
1

执行node server.js启动服务,可以在4000端口看到页面。 image.png 显示网页源代码,可以看到已经添加了所有html代码 image.png

# 2.编译之后正式发布

在上面只完成了本地开发时的ssr服务端渲染。要想正式上线,需要先build出来静态文件,然后经过server解析静态文件。

# 修改启动命令

"scripts": {
  "dev": "vite",
  "build": "npm run build:client && npm run build:server",
  "build:client": "vite build --ssrManifest --outDir dist/client",
  "build:server": "vite build --ssr src/server-entry.jsx --outDir dist/server",
  "server": "cross-env NODE_ENV=production node server.js",
  "preview": "vite preview"
},
1
2
3
4
5
6
7
8
  • build:client 打包前端
  • build:server 通过server-entry编译server服务
  • server 通过设置env NODE_ENV=production 测试生产环境下的ssr

# 在server.js文件中添加production时的渲染

# 1: 判断是否为production环境

// 判断是否为生产环境
const isProduction = process.env.NODE_ENV === "production";
1
2

# 2: 如果是production,则使用编译出来的文件

const templateHtml = isProduction
  ? fs.readFileSync("./dist/client/index.html", "utf-8")
  : "";
const ssrManifest = isProduction
  ? fs.readFileSync("./dist/client/ssr-manifest.json", "utf-8")
  : undefined;
1
2
3
4
5
6

# 3:如果是production,render方法直接使用dist下的server下的server-entry.js

render = (await import("./dist/server/server-entry.js")).render;
1

# 4: 修改后完整的server.js

import fs from "fs";
import express from "express";
const app = express();
// 通过vite创建server服务
const { createServer: createViteServer } = await import("vite");
// 判断是否为生产环境
const isProduction = process.env.NODE_ENV === "production";
const templateHtml = isProduction
  ? fs.readFileSync("./dist/client/index.html", "utf-8")
  : "";
const ssrManifest = isProduction
  ? fs.readFileSync("./dist/client/ssr-manifest.json", "utf-8")
  : undefined;

//创建vite服务实例
let vite;
if (!isProduction) {
  // 开发环境下
  vite = await createViteServer({
    server: { middlewareMode: true },
    appType: "custom",
    base: "/",
  });
  // 使用 vite 中间件
  app.use(vite.middlewares);
} else {
  // 生产环境下,设置静态目录
  app.use(express.static("./dist/client"));
}

app.get("*", async (req, res) => {
  let template;
  let render;
  //   console.log("isProduction", isProduction);
  if (!isProduction) {
    template = fs.readFileSync("index.html", "utf8");
    // 路由变化,更新html,更新template
    template = await vite.transformIndexHtml(req.url, template);
    // 在src下创建 server-entry.jsx 文件,做为ssr的入口文件,前端启动的入口文件为main.js,已经添加到了index.html中
    render = (await vite.ssrLoadModule("/src/server-entry.jsx")).render;
  } else {
    template = templateHtml;
    render = (await import("./dist/server/server-entry.js")).render;
  }
  //   console.log(req.url, template,"render");
  const html = await render(req.url, ssrManifest);
  if (ssrManifest.url) {
    res.redirect(301, ssrManifest.url);
    return;
  }
  // 给index.html的id为root标签中添加 <!--APP_HTML-->,做为后边要替换的标志
  const responseHtml = template.replace("<!--APP_HTML-->", html);
  res.status(200).set({ "Content-Type": "text/html" }).end(responseHtml);
});
app.listen(4000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 测试项目

  • 先执行npm run build,编译生成需要的文件
  • 然后运行 npm run server,可以在4000端口查看页面

项目完整代码 (opens new window)

编辑 (opens new window)
上次更新: 2025/04/19, 14:22:11
react的fiber架构
基于React搭建threejs[最流行的3D库]环境

← react的fiber架构 基于React搭建threejs[最流行的3D库]环境→

最近更新
01
色戒2007
04-19
02
真实real
04-19
03
Home
更多文章>
Theme by Vdoing | Copyright © 2018-2025 北鸟南游
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式