react 服务端渲染教程(基于DVA)

2023-10-18 17:48

本文主要是介绍react 服务端渲染教程(基于DVA),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

react 服务端渲染教程(基于DVA)

Github Repo 地址

dva-ssr

使用

  • npm install
  • npm run buildClient
  • npm run buildServer
  • npm run ssr
  • view localhost:3000

功能

  • 基于 Dva 的 SSR 解决方案
  • 支持 Code Splitting (不再使用Dva自带的 dva/dynamic加载组件)
  • 支持 CSS Modules

SSR实现逻辑

概览

在这里插入图片描述

上图是SSR的运行时流程图(暂时不考虑构建的问题)

图中左侧是浏览器端看到的页面源码。其中红色框标识的3个部分,是SSR需要关注的重点内容。

  • 最简单的是中间一个框,它是服务端渲染的App的内容部分。

  • 第一个是分片(splitting)代码文件。即SSR Server必须要知道,浏览器要正确展示这个页面,需要包含哪些分片的js代码。
    如果不计算并返回这个script标签,那么浏览器render这个list 组建时,会发现这个组件不存在,还需要异步加载并re-render 页面。

  • 最后一个框,是服务端返回的 window._preloadedState 即 全局状态对象。浏览器端要使用这个对象对redux的store进行初始化。

收到客户端的SSR请求后,SSR Server将依次执行如下五部操作:

  1. 对请求的路径,进行路由匹配;并 “获取/加载”(获取对应同步组件,加载对应异步组件) 所涉及的组件
  // 初始化const history = createMemoryHistory();history.push(req.path);const initialState = {};const app = dva({history, initialState});app.router(router);const App = app.start();let routes = getRoutes(app);// 匹配路由,获取需要加载的Route组件(包含Loadable组件)const matchedComponents = matchRoutes(routes, req.path).map(({route}) => {if (!route.component.preload) {// 同步组件return route.component;} else {// 异步组件return route.component.preload().then(res => res.default)}});const loadedComponents = await Promise.all(matchedComponents);
  1. 对1中组件进行初始化(如需),进行接口请求,并等待请求返回。

注: 需要进行数据初始化的组件,需要定义 static fetching 方法

const actionList = loadedComponents.map(component => {if (component.fetching) {return component.fetching({...app._store,...component.props,path: req.path});} else {return null;}});
await Promise.all(actionList);
  1. 调用 ReactDOMServer.renderString 渲染数据

//  Render Dva App。同时使用Loadable.Capture 捕捉本次渲染包含的Loadable组件集合Array<String>。
const modules = [];
const markup = renderToString(<Loadable.Capture report={module => modules.push(module)}><App location={req.path} context={{}}/></Loadable.Capture>
);//  构造需要render的 script标签。其中利用了react-loadable的webpack插件在构建过程中生成的module字典
let bundles = getBundles(moduleDict, modules);
let scripts = bundles.filter(bundle => bundle.file.endsWith('.js'));
let scriptMarkups = scripts.map(bundle => {return `<script src="/public/${bundle.file}"></script>`
}).join('\n');

Loadable 的相关概念和用法,请参考 github: react-loadable

Code Splitting
  1. 获取preloadedState
const preloadedState = app._store.getState();
  1. 拼装Html,并返回
res.send(`
<!DOCTYPE html>
<html><head><title>React Server Side Demo With Dva</title><link href="/public/style.css" rel="stylesheet"></head><body><div id="app">${markup}</div><script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\\u003c')}</script><script src="/public/main.js"></script>${scriptMarkups}</body>
</html>
`);
如何支持Dva

本节分几个部分:

  1. 如何既支持 dva/dynamic 又支持 SSR
  2. SSR Server 端如何支持 Dva
  3. SSR Client 端如何支持 Dva
如何既支持 dva/dynamic 又支持 SSR

之前使用dva的Code Splitting功能时,用的是 dva/dynamic。示例代码如下:

import dynamic from 'dva/dynamic';const UserPageComponent = dynamic({app,models: () => [import('./models/users'),],component: () => import('./routes/UserPage'),
});

它的问题是不支持SSR。解决方法是使用 react-loadable 代替 dva/dynamic。为了不影响dva的功能,
我们需要了解 dva/dynamic 除了实现了加载组件之外还实现了哪些功能。

通过查阅dva源码,发现 dva/dynamic 额外实现的功能比较纯粹,就是 register model

// packages/dva/src/dynamic.jsconst cached = {};function registerModel(app, model) {model = model.default || model;if (!cached[model.namespace]) {app.model(model);cached[model.namespace] = 1;}
}// ..... 省略部分代码export default function dynamic(config) {const { app, models: resolveModels, component: resolveComponent } = config;return asyncComponent({resolve: config.resolve || function () {const models = typeof resolveModels === 'function' ? resolveModels() : [];const component = resolveComponent();return new Promise((resolve) => {Promise.all([...models, component]).then((ret) => {if (!models || !models.length) {return resolve(ret[0]);} else {const len = models.length;ret.slice(0, len).forEach((m) => {m = m.default || m;if (!Array.isArray(m)) {m = [m];}// 注册 modelm.map(_ => registerModel(app, _));});resolve(ret[len]);}});});},...config,});
}

因此,我们需要在 react-loadable 的基础上,增加 registerModel 功能,且需要自己维护 cached model 这个对象。

为什么选择 react-loadable ?

通过翻阅若干个支持SSR Code Splitting的Repo,只有 react-loadable 比较好的支持 “多个文件加载”。

下面是react-loadable 的基本用法:


Loadable({loader: () => import('./components/Bar'),loading: Loading,timeout: 10000, // 10 seconds
});

不难发现, 这是不能够完全匹配 dva/dynamic 的能力的。因为在Dva里,有model这个概念。
我们不仅需要加载UI组件本身,还需要加载它所依赖的model文件。而react-loadable 可以很好的支持这个特性。

下面是 react-loadable 的 Loadable.Map 用法

Loadable.Map({loader: {Bar: () => import('./Bar'),i18n: () => fetch('./i18n/bar.json').then(res => res.json()),},render(loaded, props) {let Bar = loaded.Bar.default;let i18n = loaded.i18n;return <Bar {...props} i18n={i18n}/>;},
});

经过修改,我们可以得到兼容dva的dynamic方案。
例如,有一个页面叫做 Grid。它依赖2个model,分别是 grid 和 user。

Loadable.Map({loader: {Grid: () => import('./routes/Grid.js'),grid: () => import('./models/grid.js'),user: () => import('./models/user.js'),},delay: 200,timeout: 1000,loading: Loading,render(loaded, props) {let Grid = loaded["Grid"].default;let grid = loaded["grid"].default;let user = loaded["grid"].default;registerModel(app, grid);registerModel(app, user);return <Grid {...props} />;},});

对于复杂的项目,可能有很多route配置,写上面这个配置项代码较多。我们可以考虑对其进行封装。
基于此,我们可以考虑实现 dynamicLoader 方法。


const dynamicLoader = (app, modelNameList, componentName) => {let loader = {};let models = [];let fn = (path, prefix) => {return () => import(`./${prefix}/${path}`);};if (modelNameList && modelNameList.length > 0) {for (let i in modelNameList) {if (modelNameList.hasOwnProperty(i)) {let model = modelNameList[i];if (loader[model] === undefined) {loader[model] = fn(model, 'models');models.push(model);}}}}loader[componentName] = fn(componentName, 'routes');return Loadable.Map({loader: loader,loading: Loading,render(loaded, props) {let C = loaded[componentName].default;for (let i in models) {if (models.hasOwnProperty(i)) {let model = models[i];if (loaded[model] && getApp()) {registerModel(app, loaded[model]);}}}return <C {...props}/>;},});
};// 使用const routes = [{path: '/popular/:id',component: dynamicLoader(app, ['grid'], 'Grid')
}];

但是,上述代码在 SSR Server端是无法工作的。

首先,react-loadable 需要在webpack打包过程中生成Loadable组件的数据字典。
SSR Server 需要利用这个字典的信息生成 分片js代码的 script 标签。

字典文件示例:

// react-loadable.json{"./routes/Grid.js": [{"id": 141,"name": "./src/routes/Grid.js","file": "0.js","publicPath": "/public/0.js"}],"lodash/isArray": [{"id": 296,"name": "./node_modules/lodash/isArray.js","file": "0.js","publicPath": "/public/0.js"},{"id": 296,"name": "./node_modules/lodash/isArray.js","file": "1.js","publicPath": "/public/1.js"}]//... 以下省略
}

实际使用发现,上述代码 dynamicLoader 无法生成正确的字典。

后经过Debug发现,问题根源是代码中使用了带参数的 import。即 import(./${prefix}/${path})
而webpack 在构建过程中无法静态获取Loadable组件的路径。因此,不能使用带参数的 import。

最终的方案是,定义路由配置文件 routes.json。然后编写一个路由生成器,生成需要的路由文件。

示例的routes.json 文件如下:


[{"path": "/","exact": true,"dva_route": "./routes/Home.js","dva_models": []},{"path": "/popular/:id","dva_route": "./routes/Grid.js","dva_models": ["./models/grid.js"]},{"path": "/topic","dva_route": "./routes/Topic.js","dva_models": []}
]

到此,我们就完成了对于dva/dynamic 和 SSR 的支持。

SSR Server 端如何支持 Dva
  1. app.start

默认情况下:

app.start('#root');

server 端应该不加参数

// 官方示例
import { IntlProvider } from 'react-intl';
...
const App = app.start();
ReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement);// 本实现的示例const App = app.start();
const markup = renderToString(<Loadable.Capture report={module => modules.push(module)}><App location={req.path} context={{}}/></Loadable.Capture>);
  1. model register

const matchedComponents = matchRoutes(routes, req.path).map(({route}) => {if (!route.component.preload) {return route.component;} else {// 加载Loadable组件return route.component.preload().then(res => {if (res.default) {// Loadable 组件return res.default;} else {// Loadable.Map 组件let result;for (let i in res) {if (res.hasOwnProperty(i)) {if (res[i].default.hasOwnProperty('namespace')) {// model 组件registerModel(app, res[i]);} else {// route 组件result = res[i].default;}}}return result;}})}});
  1. 调用组件初始化方法fetching时,需要传入 dispatch。而全局的dispatch对象在 app._store 里

const actionsList = loadedComponents.map(component => {if (component.fetching) {return component.fetching({...app._store,...component.props,path: req.path});} else {return null;}});// 示例 fetching 方法static fetching({dispatch, path}) {let language = path.substr("/popular/".length);return [dispatch({type: 'grid/init', payload: {language}}),];
}
客户端如何支持 Dva
  1. render

Loadable.preloadReady().then(() => {const App = app.start();hydrate(<App/>,document.getElementById('app'));
});
  1. 组件的初始化数据方法 fetching

由于一个route 可能需要依赖多个model作为数据源。故返回一个dispatch 的数组。这样server就可以通过多个接口拿数据。


static fetching({dispatch, path, params}) {let language = path.substr("/popular/".length);return [dispatch({type: 'grid/init', payload: {language}}),dispatch({type: 'user/fetch', payload: {userId: params.userId}})];
}

源码Github Repo 地址

dva-ssr

这篇关于react 服务端渲染教程(基于DVA)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/234183

相关文章

C#实现访问远程硬盘的图文教程

《C#实现访问远程硬盘的图文教程》在现实场景中,我们经常用到远程桌面功能,而在某些场景下,我们需要使用类似的远程硬盘功能,这样能非常方便地操作对方电脑磁盘的目录、以及传送文件,这次我们将给出一个完整的... 目录引言一. 远程硬盘功能展示二. 远程硬盘代码实现1. 底层业务通信实现2. UI 实现三. De

ubuntu20.0.4系统中安装Anaconda的超详细图文教程

《ubuntu20.0.4系统中安装Anaconda的超详细图文教程》:本文主要介绍了在Ubuntu系统中如何下载和安装Anaconda,提供了两种方法,详细内容请阅读本文,希望能对你有所帮助... 本文介绍了在Ubuntu系统中如何下载和安装Anaconda。提供了两种方法,包括通过网页手动下载和使用wg

如何在Ubuntu上安装NVIDIA显卡驱动? Ubuntu安装英伟达显卡驱动教程

《如何在Ubuntu上安装NVIDIA显卡驱动?Ubuntu安装英伟达显卡驱动教程》Windows系统不同,Linux系统通常不会自动安装专有显卡驱动,今天我们就来看看Ubuntu系统安装英伟达显卡... 对于使用NVIDIA显卡的Ubuntu用户来说,正确安装显卡驱动是获得最佳图形性能的关键。与Windo

SpringBoot快速搭建TCP服务端和客户端全过程

《SpringBoot快速搭建TCP服务端和客户端全过程》:本文主要介绍SpringBoot快速搭建TCP服务端和客户端全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录TCPServerTCPClient总结由于工作需要,研究了SpringBoot搭建TCP通信的过程

Python Selenium动态渲染页面和抓取的使用指南

《PythonSelenium动态渲染页面和抓取的使用指南》在Web数据采集领域,动态渲染页面已成为现代网站的主流形式,本文将从技术原理,环境配置,核心功能系统讲解Selenium在Python动态... 目录一、Selenium技术架构解析二、环境搭建与基础配置1. 组件安装2. 驱动配置3. 基础操作模

Nginx部署React项目时重定向循环问题的解决方案

《Nginx部署React项目时重定向循环问题的解决方案》Nginx在处理React项目请求时出现重定向循环,通常是由于`try_files`配置错误或`root`路径配置不当导致的,本文给大家详细介... 目录问题原因1. try_files 配置错误2. root 路径错误解决方法1. 检查 try_f

在React聊天应用中实现图片上传功能

《在React聊天应用中实现图片上传功能》在现代聊天应用中,除了文字和表情,图片分享也是一个重要的功能,本文将详细介绍如何在基于React的聊天应用中实现图片上传和预览功能,感兴趣的小伙伴跟着小编一起... 目录技术栈实现步骤1. 消息组件改造2. 图片预览组件3. 聊天输入组件改造功能特点使用说明注意事项

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

如何为Yarn配置国内源的详细教程

《如何为Yarn配置国内源的详细教程》在使用Yarn进行项目开发时,由于网络原因,直接使用官方源可能会导致下载速度慢或连接失败,配置国内源可以显著提高包的下载速度和稳定性,本文将详细介绍如何为Yarn... 目录一、查询当前使用的镜像源二、设置国内源1. 设置为淘宝镜像源2. 设置为其他国内源三、还原为官方

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H