各大网站域名大全,网站产品优化,网站做网站做任务,90设计网好吗一、背景
目前主流的前端架构分为SSR、CSR、SSG#xff0c;比较适合首屏直出的方案除了CSR都还不错#xff0c;因为服务端会直接返回路由对应的html css#xff0c;浏览器直接解析DOM即可#xff0c;而水合的作用是什么#xff1f;服务端首次返回的是静态页面#xff0…一、背景
目前主流的前端架构分为SSR、CSR、SSG比较适合首屏直出的方案除了CSR都还不错因为服务端会直接返回路由对应的html css浏览器直接解析DOM即可而水合的作用是什么服务端首次返回的是静态页面页面需要”动“起来的话则需要水合即将页面所需的JS引入并加载、给DOM绑定交互等等核心的API即ReactDOM.hydrateRoot。
二、同构渲染
这样服务端渲染一次、客户端渲染一次的流程也称为同构渲染一般情况是通过Nodejs实现文件服务基于组件文件通过React Server API——renderToString就像这样
import { renderToString } from react-dom/server;
import App from ./App;console.log(renderToString(App/));而在客户端接收到服务端渲染所生成的html string后基于客户端水合API——react DOM API——hydrateRoot进行组件与静态标签的关联就像这样
import React from react;
import { hydrateRoot } from react-dom/client;
import ./index.css;
import App from ./App;hydrateRoot(document.getElementById(root), App/);三、手写一个SSR
基于同构渲染的秘密我们解开了那手写一个基础的SSR服务其实很简单我们可以把整个加载链路拆解成三步
起一个本地express服务用于按路由返回html前端构建时约定式路由/pages解析每个路由下的根组件生成html模板webpack构建工程代码产出bundle.js前端项目中引入bundle.js进行同构渲染水合逻辑
整体的项目工程如下
├── .next // 生成文件服务html、构建结果
├── src
│ ├── pages // 页面
│ │ ├── a.jsx // 页面A
│ │ ├── b.jsx // 页面B
│ │ ├── c.jsx // 页面C
│ ├── client.js // 水合
├── generateHtml.js // 路由转换html能力
├── server.js // 文件服务
├── teamplate.html // html基础模板
├── webpack.config.js
└── package.json3.1、文件服务搭建
我们起一个简单的工程项目并安装基础依赖。
mkdir ssr-demo
cd ssr-demo
npm init -y
npm i express react react-dom webpack webpack-cli fs-extraexpress服务代码如下
const express require(express);
const path require(path);// 定义根目录
const rootDir path.resolve(__dirname, ./);
const outputDir path.join(rootDir, .next);const app express();
const PORT process.env.PORT || 3000;// 提供 .next 目录的静态文件服务
app.use(express.static(outputDir));// 启动服务器
app.listen(PORT, () {console.log(Server is running on http://localhost:${PORT});
});
代码中启用了静态文件服务在路由解析服务实现后每次项目构建阶段都会在.nest路由生成所有页面的html用于在服务端直接返回。
3.2、路由解析服务
接下来我们实现服务端核心部分将所有路由的组件解析成html模板在此之前需要有一个基础的html模板用于动态插入组件部分的标签html模板如下
!DOCTYPE html
html langenheadmeta charsetUTF-8 /titleMy App/title/headbody data-component{{componentName}}{{content}}script src/client.bundle.js defer/script/body
/html其中content是实际渲染的组件标签、componentName用于在后续水合阶段定位组件、script用于执行水合逻辑。
遍历生成所有html文件的代码如下
// src/generate.js
require(babel/register)({presets: [babel/preset-env, babel/preset-react],
});const fs require(fs-extra); // 使用 fs-extra 方便处理文件
const path require(path);
const React require(react);
const ReactDOMServer require(react-dom/server);// 定义根目录
const rootDir path.resolve(__dirname, ./);
const pagesDir path.join(rootDir, src/pages);
const outputDir path.join(rootDir, .next);// 生成 HTML 文件
const generateHtmlFiles () {return new Promise((resolve, reject) {fs.readdir(pagesDir, (err, files) {if (err) {reject(Error reading pages directory);return;}const promises files.map((file) {const filePath path.join(pagesDir, file);const fileExt path.extname(file);// 只处理以 .jsx 结尾的文件if (fileExt .jsx) {const pageName path.basename(file, fileExt);const PageComponent require(filePath).default; // 导入组件const renderedContent ReactDOMServer.renderToString(React.createElement(PageComponent));const templatePath path.resolve(__dirname, template.html); // Load HTML templatereturn new Promise((resolve, reject) {fs.readFile(templatePath, utf8, (err, template) {if (err) {console.error(Error loading template:, err);reject(err);return;}const html template.replace({{content}}, renderedContent).replaceAll({{componentName}}, pageName); // 注入组件名称// 构造输出路径放在以页面名字为名的文件夹中const outputDirForPage path.join(outputDir, pageName);const outputFilePath path.join(outputDirForPage, index.html);fs.ensureDirSync(outputDirForPage); // 确保页面目录存在fs.outputFileSync(outputFilePath, html, utf8);console.log(Generated HTML for ${pageName}: ${outputFilePath});resolve();});});}return Promise.resolve(); // 对于不是 .jsx 文件的情况});Promise.all(promises).then(() resolve(HTML generation completed!)).catch(reject); // 处理生成过程中的异常});});
};// 直接调用生成函数并输出结果
generateHtmlFiles().then((message) {console.log(message);process.exit(0); // 结束进程}).catch((err) {console.error(err);process.exit(1); // 发生错误结束进程});
3.3、webpack打包前端工程
这里比较好理解整个前端客户端代码也需要打包包括水合的代码我们基于webpack构建初始化一个基础的打包配置
const path require(path);module.exports {entry: path.resolve(__dirname, src, client.js),output: {path: path.resolve(__dirname, .next), // 输出到 .next 目录filename: client.bundle.js, // 根据入口名称生成文件名publicPath: /, // 公开路径},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: {loader: babel-loader,options: {presets: [babel/preset-env, babel/preset-react],},},},],},resolve: {extensions: [.js, .jsx],},mode: production, // 可以根据需要设置为 development
};配置完webpack之后我们前三步的流程可以串起来了这个效果和webpack dev server比较类似我们基于流程顺序配置对应的package.json scripts scripts: {build: webpack --config webpack.config.js,generate: node generateHtml.js,server: node server.js,start: npm run build npm run generate npm run server},build负责构建前端工程generate负责生成所有页面的htmlserver负责创建最终的文件服务
3.4、渲染的终点——水合
这段代码运行时服务端已经返回html文件此时是同构渲染的终点通过hydrateRoot API将服务端的标签在浏览器水合让页面组件动起来即可。
实现代码
// // src/client.js
import React from react;
import ReactDOM from react-dom/client;console.log(React);
const hydrateComponent (componentName) {// 动态 import 组件import(./pages/${componentName}.jsx).then(({ default: Component }) {const domContainer document.getElementById(componentName);if (domContainer) {ReactDOM.hydrateRoot(Component /, domContainer);} else {console.error(No container found for component ${componentName});}}).catch((error) {console.error(Error loading component:, error);});
};// 从 HTML 中提取组件名称然后进行水合
document.addEventListener(DOMContentLoaded, () {const componentName document.body.getAttribute(data-component);hydrateComponent(componentName);
});
至此整个demo就完成了。
大致的效果如下 结尾
本文希望对于原本不是很了解SSR、SSG方案的同学可以快速的理解与传统单页应用的区别并且可以基于这个demo了解到服务端渲染、webpack dev server的工作原理。
对于实现同构渲染的方案有建议的同学欢迎评论探讨。