飞不高 发表于 2024-6-13 21:18:48

基于React的SSG静态站点渲染方案

基于React的SSG静态站点渲染方案

静态站点生成SSG - Static Site Generation是一种在构建时生成静态HTML等文件资源的方法,其可以完全不必要服务端的运行,通过预先生成静态文件,实现快速的内容加载和高度的安全性。由于其生成的是纯静态资源,便可以利用CDN等方案以更低的成本和更高的效率来构建和发布网站,在博客、知识库、API文档等场景有着广泛应用。
描述

在前段时间碰到了一个比较麻烦的题目,我们是主要做文档业务的团队,而由于对外的产品文档涉及到全球很多地域的用户,因此在CN以外地域的网站访问速率就成了比较大的题目。虽然我们有多区域部署的机房,但是每个地域机房的数据都是相互隔离的,而实际上很多产品并不会做很多特异化的定制,因此文档实际上是可以通用的,特殊是提供了多语言文档支持的情况下,各地域共用一份文档也变得公道了起来。而即使对于CN和海外地区有着特异化的定制,但在海外自己的访问也会有比较大的范围,例如假设机房部署在US,那么在SG的访问速率同样也会成为一件棘手的事情。
那么题目来了,如果我们必要做到各地域访问的高效性,那么就必须要在各个地域的主要机房部署服务,而各个地域又存在数据隔离的要求,那么在这种情况下我们可能必要手动将文档复制到各个机房部署的服务上去,这必然就是一件很低效的事情,即使某个产品的文档不会经常更新,但是这种人工处理的方式依然是会泯灭大量精力的,显然是不可取的。而且由于我们的业务是管理各个产品的文档,在加上在海外业务不停扩展的情况下,这类的反馈需求必然也会越来越多,那么解决这个题目就变成了比较紧张的事情。
那么在这种情况下,我就忽然想到了我的博客站点的构建方式,为了方便我会将博客直接通过gh-pages分支部署在GitHub Pages上,而GitHub Pages自己是不支持服务端部署的,也就是说我的博客站全部都是静态资源。由此可以想到在业务中我们的文档站也可以用类似的方式来实现,也就是在发布文档的时间通过SSG编译的方式来生成静态资源,那么在全部的内容都是静态资源的情况下,我们就可以很轻松地基于CDN来实现跨地域访问的高效性。此外除了调度CDN的分发方式,我们还可以通过将静态资源发布到业务方申请的代码堆栈中,然后业务方就可以自行部署服务与资源了,通过多机房部署同样可以解决跨地域访问的题目。
固然,由于要思量到各种题目以及现有部署方式的兼容,在我们的业务中通过SSG来单独部署实现跨地域的高效访问并不太实际,终极大概率还是要走合规的各地域数据同步方案来包管数据的同等性与高效访问。但是在思考通过SSG来作为这个题目标解决方案时,我还是很好奇如安在React的底子上来实现SSG渲染的,究竟我的博客就可以算是基于Mdx的SSG渲染。最开始我把这个题目想的特殊复杂,但是在实现的时间发现只是实现基本原理的话还是很粗暴的解决方案,在渲染的时间并没有想象中要处理得那么精细,固然实际上要做完整的方案特殊是要实现一个框架也不是那么轻易的事情,对于数据的处理与渲染要做很多方面的考量。
在我们正式开始聊SSG的基本原理前,我们可以先来看一下通过SSG实现静态站点的特点:


[*]访问速率快: 静态网站只是一组预先生成的HTML、CSS、JavaScript、Image等静态文件,没有运行在服务器上的动态语言程序,在部署于CDN的情况下,用户可以直接通过边沿节点高效获取资源,可以减少加载时间并增强用户体验。
[*]部署简单: 静态网站可以在任何托管服务上运行,例如GitHub Pages、Vercel等,我们只必要传输文件即可,无需处理服务器配置和数据库管理等,如果借助Git版本控制和CI/CD工具等,还可以比较轻松地实现自动化部署。
[*]资源占用低: 静态网站只必要非常少的服务器资源,这使得其可以在低配置的情况中运行,我们可以在较低配置的服务器上借助Nginx轻松支撑10k+的QPS网站访问。
[*]SEO优势: 静态网站通常对搜索引擎优化SEO更加友好,预渲染的页面可以拥有完整的HTML标签结构,并且通过编译可以使其尽可能符合语义化结构,这样使得搜索引擎的呆板人更轻易抓取和索引。
那么同样的,通过SSG生成的静态资源站点也有一些范围性:


[*]实时性不强: 由于静态站点必要提前生成,因此就无法像动态网站一样根据实时的请求生成对应的内容,例如当我们发布了新文档之后,就必须要重新进行增量编译乃至是全站全量编译,那么在编译期间就无法访问到最新的内容。
[*]不支持动态交互: 静态站点通常只是静态资源的集合,因此在一些动态交互的场景下就无法实现,例如用户登录、批评等功能,固然这些功能可以通过客户端渲染时动态支持,那么这种情况就不再是纯粹的静态站点,通常是借助SSG来实现更好的首屏和SEO结果。
综上所述,SSG更适用于生成内容较为固定、不必要频繁更新、且对于数据延迟敏感较低的的项目,并且实际上我们可能也只是选取部分本领来优化首屏等场景,终极还是会落到CSR来实现服务本领。因此当我们要选择渲染方式的时间,还是要充实思量到业务场景,由此来确定究竟是CSR - Client Side Render、SSR - Server Side Render、SSG - Static Site Generation更适合我们的业务场景,乃至在一些必要额外优化的场景下,ISR - Incremental Static Regeneration、DPR - Distributed Persistent Rendering、ESR - Edge Side Rendering等也可以思量作为业务上的选择。
固然,回到最初我们提到的题目上,如果我们只是为了静态资源的同步,通过CDN来解决全球跨地域访问的题目,那么实际上并不是肯定必要完全的SSG来解决题目。将CSR完全变化为SSR究竟是一件改造范围比较大的事情,而我们的目标仅仅是一处生产、多处消费,因此我们可以转过来想一想实际上JSON文件也是属于静态资源的一种类型,我们可以直接在前端发起请求将JSON文件作为静态资源请求到浏览器并且借助SDK渲染即可,至于一些交互行为例如点赞等功能的速率题目我们也是可以接受的,文档站最的主要行为还是阅读文档。此外对于md文件我们同样可以如此处理,例如docsify就是通过动态请求,但是同样的对于搜索引擎来说这些必要执行Js来动态请求的内容并没有那么轻易抓取,所以如果想比较好地实现这部分本领还是必要不停优化迭代。
那么接下来我们就从基本原理开始,优化组件编译的方式,进而基于模版渲染生成SSG,文中相干API的调用基于React的17.0.2版本实现,内容相干的DEMO地址为https://github.com/WindrunnerMax/webpack-simple-environment/tree/master/packages/react-render-ssg。
基本原理

通常当我们使用React进行客户端渲染CSR时,只必要在入口的index.html文件中置入<div id="root"></div>的独立DOM节点,然后在引入的xxx.js文件中通过ReactDOM.render方法将React组件渲染到这个DOM节点上即可。将内容渲染完成之后,我们就会在某些生命周期大概Hooks中发起请求,用以动态请求数据并且渲染到页面上,此时便完成了组件的渲染流程。
那么在前边我们已经聊了比较多的SSG内容,那么可以明确对于渲染的主要内容而言我们必要将其离线化,因此在这里就必要先解决第一个题目,怎样将数据离线化,而不是在浏览器渲染页面之后再动态获取。很明显在前边我们提到的将数据从数据库请求出来之后写入json文件就是个可选的方式,我们可以在代码构建的时间请求数据,在此时将其写入文件,在最后一并上传到CDN即可。
在我们的离线数据请求题目解决后,我们就必要来看渲染题目了,前边也提到了类似的题目,如果依旧按照之前的渲染思路,而仅仅是将数据请求的地址从服务端接口更换成了静态资源地址,那么我们就无法做到SEO以及更快的首屏体验。实在说到这里还有一个比较有趣的事情,当我们用SSR的时间,如果我们的组件是dynamic引用的,那么Next在输出HTML的时间会将数据打到HTML的<script />标签里,在这种情况下实际上首屏的效率还是不错的,并且Google进行索引的时间是能够正常将动态执行Js渲染后的数据抓取,对于我们来说也可以算作一种离线化的渲染方案。
那么这种方式虽然可行但是并不是很好的方案,我们依然必要继续解决题目,那么接下来我们必要正常地来渲染完整的HTML结构。在ReactDOM的Server API中存在存在两个相干的API,分别是renderToStaticMarkup与renderToString,这两个API都可以将React组件输出HTML标签的结构,只是区别是renderToStaticMarkup渲染的是不带data-reactid的纯HTML结构,当客户端进行React渲染时会完全重建DOM结构,因此可能会存在闪烁的情况,renderToString则渲染了带标志的HTML结构,React在客户端不会重新渲染DOM结构,那么在我们的场景下时必要通过renderToString来输出HTML结构的。
// packages/react-render-ssg/src/basic/index.ts
import ReactDOMServer from "react-dom/server";

const App = React.createElement(
React.Fragment,
null,
React.createElement("div", null, "React HTML Render"),
React.createElement(
    "button",
    {
      onClick: () => alert("On Click"),
    },
    "Button"
)
);

const HTML = ReactDOMServer.renderToString(App);
// <div data-reactroot="">React HTML Render</div><button data-reactroot="">Button</button>
当前我们已经得到组件渲染过后的完整HTML结构,紧接着从输出的内容我们可以看出来一个题目,我们界说的onClick函数并没有在渲染过后的HTML结构中体现出来,此时在我们的HTML结构中只是一些完整的标签,并没有任何变乱的处理。固然这也是很公道的情况,我们是用React框架实现的变乱处理,其并不太可能直接完整地映射到输出的HTML中,特殊是在复杂应用中我们还是必要通过React来做后续变乱交互处理的,那么很显然我们依旧必要在客户端处理相干的变乱。
那么在React中我们常用的处理客户端渲染函数就是ReactDOM.render,那么当前我们实际上已经处理好了HTML结构,而并不必要再次将内容完整地渲染出来,大概换句话说我们现在必要的是将变乱挂在相干DOM上来处理交互行为,将React附加到在服务端情况中已经由React渲染的现有HTML上,由React来接管有关的DOM的处理。那么对于我们来说,我们必要将同样的React组件在客户端一并界说,然后将其输出到页面的Js中,也就是说这部分内容是必要在客户端中执行的。
// packages/react-render-ssg/src/basic/index.ts
const PRESET = `
const App = React.createElement(
React.Fragment,
null,
React.createElement("div", null, "React HTML Render"),
React.createElement(
    "button",
    {
      onClick: () => alert("On Click"),
    },
    "Button"
)
);
const _default = App;
ReactDOM.hydrate(_default, document.getElementById("root"));
`;

await fs.writeFile(`dist/${jsPathName}`, PRESET);
实际上这部分代码都是在服务端生成的,我们此时并没有在客户端运行的内容,大概说这是我们的编译过程,还没有到达运行时,所以我们生成的一系列内容都是在服务端执行的,那么很明显我们是必要拼装HTML等静态资源文件的。因此在这里我们可以通过预先界说一个HTML文件的模版,然后将构建过程中产生的内容放到模版以及新生成的文件里,产生的所有内容都将随着构建一并上传到CDN上并分发。
<!-- packages/react-render-ssg/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... Meta -->
    <title>Template</title>
    <!-- INJECT STYLE -->
</head>
<body>
    <div id="root">
      <!-- INJECT HTML -->
    </div>
    <!-- ... React Library-->
    <!-- INJECT SCRIPT -->
</body>
</html>
// packages/react-render-ssg/src/basic/index.ts
const template = await fs.readFile("./public/index.html", "utf-8");
await fs.mkdir("dist", { recursive: true });
const random = Math.random().toString(16).substring(7);
const jsPathName = `${random}.js`;
const html = template
.replace(/<!-- INJECT HTML -->/, HTML)
.replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsPathName}"></script>`);
await fs.writeFile(`dist/${jsPathName}`, PRESET);
await fs.writeFile(`dist/index.html`, html);
至此我们完成了最基本的SSG构建流程,接下来就可以通过静态服务器访问资源了,在这部分DEMO可以直接通过ts-node构建以及anywhere预览静态资源地址。实际被骗前很多开源的静态站点搭建框架例如VitePress、RsPress等等都是采用类似的原理,都是在服务端生成HTML、Js、CSS等等静态文件,然后在客户端由各自的框架重新接管DOM的行为,固然这些框架的集成度很高,对于相干库的复用程度也更高。而针对于更复杂的应用场景,还可以思量Next、Gatsby等框架实现,这些框架在SSG的底子上还提供了更多的本领,对于更复杂的应用场景也有着更好的支持。
组件编译

虽然在前边我们已经实现了最基本的SSG原理,但是很明显我们为了最简化地实现原理人工处理了很多方面的内容,例如在上述我们输出到Js文件的代码中是通过PRESET变量界说的纯字符串实现的代码,而且我们对于同一个组件界说了两遍,相当于在服务端和客户端分开界说了运行的代码,那么很明显这样的方式并不太公道,接下来我们就必要解决这个题目。
那么我们首先必要界说一个公共的App组件,在该组件的代码实现中与前边的基本原理中同等,这个组件会共享在服务端的HTML生成和客户端的React Hydrate,而且为了方便外部的模块导入组件,我们通常都是通过export default的方式默认导出整个组件。
// packages/react-render-ssg/src/rollup/app.tsx
import React from "react";

const App = () => (
<React.Fragment>
    <div>React Render SSG</div>
    <button onClick={() => alert("On Click")}>Button</button>
</React.Fragment>
);

export default App;
紧接着我们先来处理客户端的React Hydrate,在先前我们是通过人工维护的编辑的字符串来界说的,而实际上我们同样可以打包工具在Node端将组建编译出来,以此来输出Js代码文件。在这里我们选择使用Rollup来打包Hydrate内容,我们以app.tsx作为入口,将整个组件作为iife打包,然后将输出的内容写入APP_NAME,然后将实际的hydrate置入footer,就可以完成在客户端的React接管DOM执行了。
// packages/react-render-ssg/rollup.config.js
const APP_NAME = "ReactSSG";
const random = Math.random().toString(16).substring(7);

export default async () => {
return {
    input: "./src/rollup/app.tsx",
    output: {
      name: APP_NAME,
      file: `./dist/${random}.js`,
      format: "iife",
      globals: {
      "react": "React",
      "react-dom": "ReactDOM",
      },
      footer: `ReactDOM.hydrate(React.createElement(${APP_NAME}), document.getElementById("root"));`,
    },
    plugins: [
      // ...
    ],
    external: ["react", "react-dom"],
};
};

接下来我们来处理服务端的HTML文件生成与资源的引用,这里的逻辑与先前的基本原理中服务端生成逻辑差别并不大,只是多了通过终端调用Rollup打包的逻辑,同样也是将HTML输出,并且将Js文件引入到HTML中,这里必要特殊关注的是我们的Rollup打包时的输出文件路径是在这里由--file参数覆盖原来的rollup.config.js内置的配置。
// packages/react-render-ssg/src/rollup/index.ts
const exec = promisify(child.exec);

(async () => {
const HTML = ReactDOMServer.renderToString(React.createElement(App));
const template = await fs.readFile("./public/index.html", "utf-8");

const random = Math.random().toString(16).substring(7);
const path = "./dist/";
const { stdout } = await exec(`npx rollup -c --file=${path + random}.js`);
console.log("Client Compile Complete", stdout);

const jsFileName = `${random}.js`;
const html = template
    .replace(/<!-- INJECT HTML -->/, HTML)
    .replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
await fs.writeFile(`${path}index.html`, html);
})();
模版渲染

当前我们已经复用了组件的界说,并且通过Rollup打包了必要在客户端运行的Js文件,不必要再人工维护输出到客户端的内容。那么场景再复杂一些,如果此时我们的组件有着更加复杂的内容,例如引用了组件库来构建视图,以及引用了一些CSS样式预处理器来构建样式,那么我们的服务端输出HTML的程序就会变得更加复杂。
继续沿着前边的处理思路,我们在服务端的处理程序仅仅是必要将App组件的HTML内容渲染出来,那么假设此时我们的组件引用了@arco-design组件库,并且通常我们还必要引用此中的less文件大概css文件。
import "@arco-design/web-react/dist/css/arco.css";
import { Button } from "@arco-design/web-react";
// OR
import "@arco-design/web-react/es/Button/style/index";
import { Button } from "@arco-design/web-react/es/Button";
那么必要关注的是,当前我们运行组件的时间是在服务端情况中,那么在Node情况中显然我们是不熟悉.less文件以及.css文件的,实际上先不说这些样式文件,import语法自己在Node情况中也是不支持的,只不过我们通常是使用ts-node来执行整个运行程序,临时这点不必要关注,那么对于样式文件我们在这里实际上是不必要的,所以我们就必要配置Node情况来处理这些样式文件的引用。
require.extensions[".css"] = () => undefined;
require.extensions[".less"] = () => undefined;
但是即使这样题目显然没有竣事,熟悉arco-design的打包同砚可能会清楚,当我们引入的样式文件是Button/style/index时,实际上是引入了一个js文件而不是.less文件,如果必要明确引入.less文件的话是必要明确Button/style/index.less文件指向的。那么此时如果我们是引入的.less文件,那么并不会出现什么题目,但是此时我们引用的是.js文件,而这个.js文件中内部的引用方式是import,由于此时我们是通过es而不是lib部分明确引用的,即使在tsconfig中配置了相干解析方式为commonjs也是没有用的。
{
"ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "esModuleInterop": true
    }
}
}
因此我们可以看到,如果仅仅用ts-node来解析大概说执行服务端的数据生成是不够的,会导致我们平时实现组件的时间有着诸多限制,例如我们不能随便引用es的实现而必要借助包自己的package.json声明的内容来引入内容,如果包不能处理commonjs的引用那么还会束手无策。那么在这种情况下我们还是必要引入打包工具来打包commonjs的代码,然后再通过Node来执行输出HTML。通过打包工具,我们能够做的事情就很多了,在这里我们将资源文件例如.less、.svg都通过null-loader加载,且相干的配置输出都以commonjs为基准,此时我们输出的文件为node-side-entry.js。
// packages/react-render-ssg/rspack.server.ts
const config: Configuration = {
context: __dirname,
entry: {
    index: "./src/rspack/app.tsx",
},
externals: externals,
externalsType: "commonjs",
externalsPresets: {
    node: true,
},
// ...
module: {
    rules: [
      { test: /\.svg$/, use: "null-loader" },
      { test: /\.less$/, use: "null-loader" },
    ],
},
devtool: false,
output: {
    iife: false,
    libraryTarget: "commonjs",
    publicPath: isDev ? "" : "./",
    path: path.resolve(__dirname, ".temp"),
    filename: "node-side-entry.js",
},
};
当前我们已经得到了可以在Node情况中运行的组件,那么紧接着,思量到输出SSG时我们通常都必要预置静态数据,例如我们要渲染文档的话就必要首先在数据库中将相干数据表达查询出来,然后作为静态数据传入到组件中,然后在预输出的HTML中将内容直接渲染出来,那么此时我们的App组件的界说就必要多一个getStaticProps函数声明,并且我们还引用了一些样式文件。
// packages/react-render-ssg/src/rspack/app.tsx
import "./app.less";
import { Button } from "@arco-design/web-react";
import React from "react";

const App: React.FC<{ name: string }> = props => (
<React.Fragment>
    <div>React Render SSG With {props.name}</div>
    <Button style={{ marginTop: 10 }} type="primary" onClick={() => alert("On Click")}>
      Button
    </Button>
</React.Fragment>
);

export const getStaticProps = () => {
return Promise.resolve({
    name: "Static Props",
});
};

export default App;
/* packages/react-render-ssg/src/rspack/app.less */
body {
padding: 20px;
}
同样的,我们也必要为客户端运行的Js文件打包,只不过在这里由于我们必要处理预置的静态数据,我们在打包的时间同样就必要预先生成模版代码,当我们在服务端执行打包功能的时间,就必要将从数据库查询大概从文件读取的数据放置于生成的模版文件中,然后以该文件为入口去再打包客户端执行的React Hydrate本领。在这里由于希望将模版文件看起来更加清晰,我们使用了JSON.parse来处理预置数据,实际上这里只必要将占位预留好,数据在编译的时间颠末stringify直接写入到模版文件中即可。
// packages/react-render-ssg/src/rspack/entry.tsx
/* eslint-disable @typescript-eslint/no-var-requires */

const Index = require(`<index placeholder>`);
const props = JSON.parse(`<props placeholder>`);
ReactDOM.hydrate(React.createElement(Index.default, { ...props }), document.getElementById("root"));
在模版文件生成好之后,我们就必要以这个文件作为入口调度客户端资源文件的打包了,这里由于我们还引用了组件库,输出的内容自然不光是Js文件,还必要将CSS文件一并输出,并且我们还必要配置一些通过参数名可以控制的文件名生成、externals等等。这里必要注意的是,此处我们不必要使用html-plugin将HTML文件输出,这部分调度我们会在最后统一处理。
// packages/react-render-ssg/rspack.config.ts

const args = process.argv.slice(2);
const map = args.reduce((acc, arg) => {
const = arg.split("=");
acc = value || "";
return acc;
}, {} as Record<string, string>);
const outputFileName = map["--output-filename"];

const config: Configuration = {
context: __dirname,
entry: {
    index: "./.temp/client-side-entry.tsx",
},
externals: {
    "react": "React",
    "react-dom": "ReactDOM",
},
// ...
builtins: {
    // ...
    pluginImport: [
      {
      libraryName: "@arco-design/web-react",
      customName: "@arco-design/web-react/es/{{ member }}",
      style: true,
      },
      {
      libraryName: "@arco-design/web-react/icon",
      customName: "@arco-design/web-react/icon/react-icon/{{ member }}",
      style: false,
      },
    ],
},
// ...
output: {
    chunkLoading: "jsonp",
    chunkFormat: "array-push",
    publicPath: isDev ? "" : "./",
    path: path.resolve(__dirname, "dist"),
    filename: isDev
      ? ".bundle.js"
      : outputFileName
      ? outputFileName + ".js"
      : "..js",
    // ...
},
};
那么此时我们就必要调度所有文件的打包过程了,首先我们必要创建必要的输出和临时文件夹,然后启动服务端commonjs打包的流程,输出node-side-entry.js文件,并且读取此中界说的App组件以及预设数据读取方法,紧接着我们必要创建客户端入口的模版文件,并且通过调度预设数据读取方法将数据写入到入口模版文件中,此时我们就可以通过打包的commonjs组件执行并且输出HTML了,并且客户端运行的React Hydrate代码也可以在这里一并打包出来,最后将各类资源文件的引入一并在HTML中更换并且写入到输出文件中就可以了。至此当我们打包完成输出文件后,就可以使用静态资源服务器启动SSG的页面预览了。
const appPath = path.resolve(__dirname, "./app.tsx");
const entryPath = path.resolve(__dirname, "./entry.tsx");
require.extensions[".less"] = () => undefined;

(async () => {
const distPath = path.resolve("./dist");
const tempPath = path.resolve("./.temp");
await fs.mkdir(distPath, { recursive: true });
await fs.mkdir(tempPath, { recursive: true });

const { stdout: serverStdout } = await exec(`npx rspack -c ./rspack.server.ts`);
console.log("Server Compile", serverStdout);
const nodeSideAppPath = path.resolve(tempPath, "node-side-entry.js");
const nodeSideApp = require(nodeSideAppPath);
const App = nodeSideApp.default;
const getStaticProps = nodeSideApp.getStaticProps;
let defaultProps = {};
if (getStaticProps) {
    defaultProps = await getStaticProps();
}

const entry = await fs.readFile(entryPath, "utf-8");
const tempEntry = entry
    .replace("<props placeholder>", JSON.stringify(defaultProps))
    .replace("<index placeholder>", appPath);
await fs.writeFile(path.resolve(tempPath, "client-side-entry.tsx"), tempEntry);

const HTML = ReactDOMServer.renderToString(React.createElement(App, defaultProps));
const template = await fs.readFile("./public/index.html", "utf-8");
const random = Math.random().toString(16).substring(7);
const { stdout: clientStdout } = await exec(`npx rspack build -- --output-filename=${random}`);
console.log("Client Compile", clientStdout);

const jsFileName = `${random}.js`;
const html = template
    .replace(/<!-- INJECT HTML -->/, HTML)
    .replace(/<!-- INJECT STYLE -->/, `<link rel="stylesheet" href="${random}.css">`)
    .replace(/<!-- INJECT SCRIPT -->/, `<script src="${jsFileName}"></script>`);
await fs.writeFile(path.resolve(distPath, "index.html"), html);
})();
逐日一题

https://github.com/WindrunnerMax/EveryDay
参考

https://www.sanity.io/ssr-vs-ssg-guide
https://react.docschina.org/reference/react-dom
https://www.theanshuman.dev/articles/what-the-heck-is-ssg-static-site-generation-explained-with-nextjs-5cja

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 基于React的SSG静态站点渲染方案