S14-04 SSR-React18 SSR
[TOC]
原生实现
概述
React和Vue一样,除了支持开发SPA应用之外,其实也是支持开发SSR应用的。
在React中创建SSR应用,需要调用 ReactDOM.hydrateRoot() 函数,而不是ReactDOM.createRoot
- createRoot :创建一个Root,接着调用其 render 函数将App直接过载到页面上
- hydrateRoot() :创建水合Root ,是在激活的模式下渲染 App
服务端可用 ReactDOM.renderToString() 来进行渲染
搭建Node Server
依赖包:
- express
- 安装:
pnpm i express
- 安装:
- nodemon
- 安装:
pnpm i nodemon -g
- 安装:
- webpack
- webpack-cli
- webpack-node-externals
- 安装:
pnpm i webpack webpack-cli webpack-node-externals -D
- 安装:
搭建过程:
1、运行pnpm init
初始化package.json
2、创建一个express服务器
3、在package.json
中编写npm脚本,运行node服务器
4、打包/src/server/index.js
文件
打包配置
/config/webpack.server.js
。target打包命令,
--watch
表示内容变化时会重新打包。
5、优化打包:使用 webpack-node-externals 在打包时排除node_modules
中的包。
此时打包的js文件有900kb大小,需要优化。
6、测试打包后的js文件是否可以运行(OK)
搭建React18 SSR Server
依赖包:
- react
- 安装:
pnpm i react
- 安装:
- react-dom
- 安装:
pnpm i react-dom
- 安装:
- webpack-merge
- 安装:
pnpm i webpack-merge -D
- 安装:
- babel-loader
- @babel/preset-react
- @babel/preset-env
- 安装:
pnpm i babel-loader @babel/preset-react @babel/preset-env -D
- 安装:
搭建过程:
注意: react中不用调用SSR专用的方法(如vue中的createSSRApp()
)生成app,可以直接返回App。其他流程和vue基本一致。
1、编写app.jsx
2、在src/server/index.js
中通过 renderToString() 方法将app实例转化为HTML字符串。并返回给前端。
6、打包项目:pnpm run build:server
7、运行打包后的项目:pnpm run start
8、此时页面可以展示,但是不能互动,页面中的按钮不起作用。
Hydration
安装的依赖项同前面一样
服务器端渲染页面 + 客户端激活页面,是页面有交互效果(这个过程称为:Hydration 水合)
Hydration的具体步骤如下:
1、在src/client/index.js
中通过 hydrateRoot() 创建一个App实例并挂载到#root
元素上。
2、创建配置文件webpack.client.js
,并配置打包项。
3、创建打包脚本
4、打包项目:pnpm run build:client
5、在src/server/index.js
的HTML模板中,引入client_bundle.js
。JS文件部署在静态服务器中。
注意: 在HTML模板中引入${AppHtmlString}
时,起前后不能有空格或换行,否则报错
6、运行项目:pnpm run start
。此时页面中的JS代码已经激活。
合并配置
依赖包:
- webpack-merge:合并webpack配置。
- 安装:
pnpm i webpack-merge -D
- 安装:
base.config.js
server.config.js
client.config.js
集成Router
依赖包:
- react-router-dom
- 安装:
npm i react-router-dom
( 默认会自动安装 react-router )
- 安装:
实现过程:
1、在src/router/index.js
中创建一个路由实例。
2、在src/app.jsx
中将routes转化为组件形式的路由并添加路由占位。
3、在src/server/index.js
中挂载路由到app上。<App />
需要用<StatciRouter>
包裹。
4、在src/client/index.js
中也挂载一遍路由。<App />
需要用<BrowserRouter>
包裹。
5、效果
集成ReduxToolkit
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。
并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;
在很多地方为了称呼方便,也将之称为“RTK”;
安装
npm install @reduxjs/toolkit react-redux
API
- configureStore({ reducer, middleware, devTools, ... }):
返回:store
,包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。- 参数
- reducer:``,Redux store 的根 reducer
- middleware:``,要使用的中间件数组
- devTools:``,是否启用开发工具(如 Redux DevTools),默认true
- preloadedState:``,初始状态
- enhancers:``,其他 store 增强器
- 返回
- store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
- createSlice({ name, initialState, reducers,... }):
返回:reducerSlice
,用于创建一个Redux reducer和action creator的集合- 参数
- name:
String
,用于标识这个reducer的名称,action.type
会根据name生成 - initialState:
any
,表示这个reducer的初始状态 - reducers:
{ reducer,... }
,用于定义这个reducer的action creator和对应的reducer函数- reducer:
(state, action) => void
,相当于之前的reducer函数
- reducer:
- 返回
- reducerSlice:``,返回一个reducer片段
- createAsyncThunk(typePrefix, payloadCreator, options? ):
返回:
,用于创建一个异步action creator- 参数
- typePrefix:
String
,用于标识这个异步action creator的类型前缀 - payloadCreator:
(arg, thunkAPI) => Promise
,用于处理异步操作并返回一个Promise对象 - options?:
Object
,用于配置异步action creator的一些选项- fulfilled:用于指定异步操作成功时的处理函数。
- rejected:用于指定异步操作失败时的处理函数。
- pending:用于指定异步操作进行中时的处理函数。
- dispatchCondition:用于指定在什么条件下才会dispatch这个action的函数。
- condition:用于指定在什么条件下才会调用
payloadCreator
函数的函数。 - typeSuffixes:用于指定异步action creator的类型后缀的对象。
- serializeError:用于指定如何序列化异步操作的错误信息的函数。
集成过程
1、创建store:configureStore()
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'
import homeReducer from './features/home'
+ const store = configureStore({
+ reducer: {
+ counter: counterReducer,
+ home: homeReducer
+ }
+ })
export default store
2、创建reducer片段:createSlice()
import { createSlice } from "@reduxjs/toolkit";
+ const counterSlice = createSlice({
+ name: 'counter',
+ initialState: {
+ counter: 100
+ },
+ reducers: {
+ addCounter(state, action) {
+ state.counter += action.payload
+ },
+ subCounter(state, action) {
+ state.counter -= action.payload
+ }
+ }
+ })
+ export default counterSlice.reducer
3、在client中结合redux和react组件
4、在server中结合redux和react组件
5、在组件中使用 useSelector() 获取store中的数据
6、在组件中使用 useDispatch() 修改store中的数据
7、效果
异步操作
1、在home.ts
中发送异步请求
+ export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
const res = await axios.get('http://123.207.32.32:8000/home/multidata')
return res.data.data
})
const homeSlice = createSlice({
name: 'home',
initialState: {
banners: [],
recommends: []
},
+ extraReducers(builder) {
+ builder
+ .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
state.banners = payload.banner.list
state.recommends = payload.recommend.list
})
+ .addCase(fetchHomeMultidataAction.rejected, (state) => {
console.log('fetchHomeMultidataAction.rejected')
})
}
})
2、在组件中调用 fetchHomeMultidataAction()