React 服务端渲染

news2024/9/28 9:20:30

React 服务器端渲染概念回顾

  • 什么是客户端渲染CSR(Client Side Rendering)

服务器端只返回json数据,Data和Html的拼接在客户端进行(渲染)。

  • 什么是服务器端渲染SSR(Server Side Rendering)

服务器端返回数据拼接过后的HTML,Data和Html的拼接在服务器端进行(渲染)。

使用react实现的应用都是单页应用(Singe Page Application),都属于是客户端渲染。

  • 为什么要实现服务器端渲染,SPA的服务器端渲染旨在解决哪些问题呢。

  • 客户端首页渲染时间长,大部分时间处于页面外链的加载等待状态,用户体验差。

  • 页面结构是空的,不利于网站的SEO

  • SSR同构,同构指的是代码复用,即实现客户端和服务器端最大程度的的代码复用。

原生实现

项目结构

react-ssr

- src 源代码文件夹

- client 客户端代码

- server 服务器端代码

- share 同构代码

三步快速实现React SSR

  1. 引入要渲染的React组件

  1. 通过renderToString方法将React 组件转换为HTML 字符串

renderToString方法用于将React组件转换成HTML字符串,可通过react-dom/server 导入

  1. 将结果HTML字符串响应到客户端

首先我们需要使用express快速搭建一个server服务器

// http.js
import express from 'express'

const app = express()

app.listen(3000, () => console.log('app is running on port 3000'))

export default app

将同构代码,主要为页面组件代码,放到share 文件夹下

webpack打包配置

这时候我们就像通过node来运行我们的express服务器是会报错的:

$ node src/server/index.js
(node:8130) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/huiquandeng/projects/lg-fed-lp/my-module/react-ssr/src/server/index.js:1
import Home from '../share/pages/Home'
^^^^^^
SyntaxError: Cannot use import statement outside a module

问题:nodejs环境不支持ESModule模块系统,不支持JSX语法。

所以需要webpack工具使用babel插件对其进行转换。

const path = require('path')

module.exports = {
  mode: 'development',
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  }
}

在 package.json中配置运行命令:

"scripts": {
    "dev:server-build": "webpack --config webpack.server.js"
}

此时即可运行打包命令生成bundle.js 文件到build 目录下,

这时使用node命令允许该服务器就不会报错了。

$ node build/bundle.js
app is running on port 3000

尝试访问应用页面,发现报错React模块未定义。

$ node build/bundle.js
app is running on port 3000
ReferenceError: React is not defined
at eval (webpack://react-ssr/./src/server/index.js?:9:96)

回到server 的index.js 文件中,我们引入React即可。

此时服务器已经可以正常渲染React 组件中的内容。

改进: 这时候我们没改动一次代码要想页面内容生效,都需重新运行一下打包命令,这个操作可以交给我们的程序自主监测运行,减少我们的重复的手工操作,提升开发效率。

配置项目启动命令

  • 配置服务器端webpack打包命令:

"dev:server-build": "webpack --config webapck.server.js --watch"

打包命令加入--watch 参数,可以让webpack监控文件的更改重新执行build打包命令生成build目录下多的文件。

  • 配置服务端启动命令:

"dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\""

该命令使用了nodemon进行node程序的监控,通过--watch 监控指定文件夹中文件的变化,当有变化时重新运行服务器启动命令:node build/bundle.js。

此时开两个终端分别运行以上两个命令即可实现文件变化的监控。

为组件元素附加事件的方式

思路: 在客户端对组件进行二次渲染,为组件元素附加事件。

客户端二次渲染的方法:hydrate

使用hydrate 方法对组件进行渲染,为组件元素附加事件。

该方法在实现渲染的时候,会复用原本已经存在的DOM节点,减少重新生成节点以及删除原本DOM节点的开销

可以通过react-dom模块引入。

import ReactDOM from 'react-dom'
ReactDOM.hydrate(<App />, document.querySelector('#root'))

在src/client 文件夹中新建index.js文件

import React from 'react'
import ReactDOM from 'react-dom'
import Home from '../share/pages/Home'

ReactDOM.hydrate(<Home />, document.querySelector('#root'))

客户端React打包配置

  1. webpack打包配置 webpack.client.js

  • 打包目的:转换JSX语法,转换浏览器不识别的高级JavaScript 语法

  • 输出目标位置:public 目录

const path = require('path')

module.exports = {
  mode: 'development',
  // target: 'es5',
  entry: './src/client/index.js',
  output: {
    path: path.join(__dirname, 'public'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  }
}
  1. 打包启动命令配置

"dev:client-build": "webpack --config webpack.client.js --watch"

添加客户端包文件请求链接

在响应客户端的HTML代码中添加script 标签,请求客户端JavaScript打包文件main.js

`<html>
    <head>
      <title>React SSR Base</title>
      
    </head>
    <body>
      <div id='root'>${content}</div>
      <script src='main.js'></script>
    </body>
  </html>`

在server端的服务器中设置静态资源文件目录

在对网页进行访问之前还需要做一步工作,就是为服务器设置静态资源文件目录。

import express from 'express'

const app = express()
// 设置静态资源请求目录
app.use(express.static('public'))

app.listen(3000, () => console.log('app is running on port 3000'))

export default app

此时就可以为组件元素添加事件了:

import React from 'react'

export default function Home () {
  return (
    <div onClick={() => console.log('Hello onClick trigger')}>
      Home works run with nodemon cli
    </div>
  )
}

此时就已经可以成功为组件附加上点击事件:

注意在V18版本的的react中hydrate 方法不再使用,改用hydrateRoot代替:

// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);

// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.

优化:合并webpack配置

服务器端和客户端的webpack配置文件存在重复,可将重复的部分配置抽象到webpack.base.js 配置文件中。

接下来在利用webpack-merge 对文件差异部分以及重复的base部分进行合并。

// webpack.base.js
module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  }
}
// webpack.client.js
const path = require('path')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')

const config = {
  entry: './src/client/index.js',
  output: {
    path: path.join(__dirname, 'public'),
    filename: 'main.js'
  }
}

module.exports = merge(baseConfig, config)
// webpack.server.js
const path = require('path')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')

const config = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  }
}

module.exports = merge(baseConfig, config)

优化:合并项目启动命令

三个命令启动项目显得比较繁琐,所以我们需要一个工具帮我们合并多个命令,运行一个命令即可把完整项目运行起来。这个工具就是: npm-run-all

$ npm i npm-run-all

配置命令:

"dev": "npm-run-all --parallel dev:*"

--parallel 参数:表示同时执行多个命令。

这样一来,我们的项目开发启动命令只需要运行dev命令即可:

$ npm run dev

优化:服务器端打包文件体积优化

开始前我们先来看一下目前打包出来的bundle.js文件的大小:

asset bundle.js 1.54 MiB [compared for emit]

问题:显然我们这么简单的一个应用,这个文件大小显得有点过大了。

究其原因是实际上是在打包是把node代码环境中的一些系统模块打包到了我们的应用当中。

所以接下来我们就要对这部分的代码进行打包优化,通过webpack的配置把这些不必要的系统代码剔除,避免它们被打包盗目标文件中。

解决方法:需要用到 webpack-node-externals

const path = require('path')
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')

const baseConfig = require('./webpack.base')

const config = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  externals: [nodeExternals()]
}

module.exports = merge(baseConfig, config)

重新运行打包:

asset bundle.js 7.96 KiB [emitted]

优化过后的体积只有8kb左右,非常的nice。

优化:代码拆分

在一个项目中,同构代码也需要实现前后端代码分离,所以我们需要将启动服务器代码和渲染代码进行模块化拆分。通过优化代码组织方式,渲染React组件代码是独立功能,所以把它从服务器端入口文件进行抽离。

把原本server/index.js中的HTML字符串渲染的内容抽离到server/render.js:

import React from 'react'
import Home from '../share/pages/Home'
import { renderToString } from 'react-dom/server'

export default (Component = Home) => {
  const content = renderToString(<Component />)
  return `<html>
    <head>
      <title>React SSR Base</title>
      
    </head>
    <body>
      <div id='root'>${content}</div>
      <script src='main.js'></script>
    </body>
  </html>`
}

拆分抽离后的index.js:

import app from './http'
import render from './render'

app.get('/', (req, res) => {
  // 未来会抽取出来作为入口html模版文件
  res.send(render())
})

实现服务器端路由

实现路由支持分析

  • 在React SSR项目中需要实现两端路由

  • 客户端路由是用于支持用户通过点击链接的形式跳转页面。

  • 服务器端路由是用于支持用户直接从浏览器地址栏中访问地址。

  • 客户端和服务器端共用一套路由规则。

也就是说路由规则属于share公共代码

在src/share/pages目录下新建页面组件home 和 list:

import React from 'react'
import routes from '../routes'

export default function List () {
  return (
    <div>
      <ul>
        {routes.map(route => {
          return (
            <li key={route.path}>
              <a href={route.path}>{route.component.name}</a>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

接着在src/share目录下新建路由规则routes.js文件,其内部采用数组对象的形式。使得规则较为通用,可在node环境中直接使用。

import Home from './pages/Home'
import List from './pages/List'

export default [
  {
    path: '/',
    component: Home,
    exact: true
  },
  {
    path: '/list',
    component: List
  }
]

这一步需要用到两个支持库: react-router-dom 和 react-router-config

$ npm i react-router-dom react-router-config

给render.js中的渲染函数添加路由支持代码:

import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
// renderRoutes方法旨在把数组形式的路由规则转换为组件形式的路由规则
import { renderRoutes } from 'react-router-config'
import routes from '../share/routes'

export default req => {
  const content = renderToString(
    // 根据请求req的path 匹配对应的路由获取页面组件渲染成字符串
    <StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
  )
  return `<html>
    <head>
      <title>React SSR Base</title>
    </head>
    <body>
      <div id='root'>${content}</div>
      <script src='main.js'></script>
    </body>
  </html>`
}

同时需要修改服务器端的路由处理匹配规则为 * ,这样就可以接收到任何的客户端路由请求:

import app from './http'
import render from './render'

app.get('*', (req, res) => {
  res.send(render(req)) // 把req传递给渲染函数render,以便可以进行路由支持
})

这里有个坑要留意:react-router的 v5 -> v6 有个升级指南说到react-router-config在v6中被合并并移除了,v6中使用useRoutes方法代替

这样以来,useRoutes是个钩子函数,我们得为页面路由组件列表创建一个入口组件App.js:

import React from 'react'
import { useRoutes } from 'react-router'
import routes from '../routes'

export default function App () {
  const element = useRoutes(routes)
  return element
}

则服务端渲染代码对应修改为:

import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
// renderRoutes方法旨在把数组形式的路由规则转换为组件形式的路由规则
// import { useRoutes } from 'react-router'
// import routes from '../share/routes'
import App from '../share/pages/App'

export default req => {
  // StaticRouter根据请求req的 url 匹配对应的路由获取页面组件渲染成字符串
  const content = renderToString(
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  )

  return `<html>
    <head>
      <title>React SSR Base</title>
    </head>
    <body>
      <div id='root'>${content}</div>
      <script src='main.js'></script>
    </body>
  </html>`
}

实现客户端路由

客户端hydrate渲染的代码修改为:

import React from 'react'
// import ReactDOM from 'react-dom'
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
// import { useRoutes } from 'react-router'
// import routes from '../share/routes'
import App from '../share/pages/App'

const container = document.querySelector('#root')
// ReactDOM.hydrate(<Home />, container)

const root = hydrateRoot(
  container,
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

Home页面添加Link组件实现客户端页面路由跳转:

import React from 'react'
import { Link } from 'react-router-dom'
export default function Home () {
  return (
    <>
      <div onClick={() => console.log('Hello onClick trigger')}>
        Home works run with nodemon cli{' '}
      </div>
      <Link to='/list'>navigate to list page</Link>
    </>
  )
}

实现客户端Redux

思路分析:

  • SSR同样需要实现两端的Redux,

  • 客户端Redux就是通过JavaScript管理store 中的数据。

  • 服务器端Redux 就是在服务器端搭建一套Redux 代码,用于管理组件中的数据。

  • 客户端和服务器端共用一套Reducer 代码

  • 创建Store的代码由于参数传递不同,所以不可以共用。

首先,创建客户端的store

import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'
import usersReducer from './features/users/usersSlice'

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    users: usersReducer
  }
})

创建状态数据切片并导出相对应的actions 和 reducers

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

// await axios.get('https://jsonplaceholder.typicode.com/users');
export const loadUsers = createAsyncThunk(
  'users/loads',
  // (payload, thunkAPI) => {
  //   axios
  //     .get(payload)
  //     .then(response => thunkAPI.dispatch(setUsers(response.data)))
  // }
  payload => {
    return axios.get(payload).then(response => response.data)
  }
)

const usersSlice = createSlice({
  name: 'users',
  initialState: [],
  reducers: {
    userAdded (state, action) {
      state.push({
        id: action.payload.id,
        text: action.payload.text,
        completed: false
      })
    },
    addUser: {
      prepare: user => {
        return { payload: { ...user, id: Math.ceil(Math.random() * 100) } }
      },
      reducer: (state, action) => {
        state.push(action.payload)
      }
    },
    userToggled (state, action) {
      const user = state.find(user => user.id === action.payload)
      user.completed = !user.completed
    },
    setUsers (state, action) {
      action.payload.forEach(user => state.push(user))
    }
  },
  extraReducers: {
    [loadUsers.fulfilled]: (state, action) => {
      action.payload.forEach(user => state.push(user))
    }
  }
})

export const { userAdded, userToggled, setUsers } = usersSlice.actions
export default usersSlice.reducer

在应用的最外层组件使用Provider封装 并提供上面创建的store 作为store 参数

 import React from 'react'
// import ReactDOM from 'react-dom'
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
// import { useRoutes } from 'react-router'
// import routes from '../share/routes'
import App from '../share/pages/App'
import { store } from '../store/Store.client'
import { Provider } from 'react-redux'

const container = document.querySelector('#root')
// ReactDOM.hydrate(<Home />, container)

const root = hydrateRoot(
  container,
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
)

在使用store中数据的 List 页面 通过connect 连接并使用store

import React, { useEffect } from 'react'
import routes from '../routes'
import { connect } from 'react-redux'
import { loadUsers } from '../../store/features/users/usersSlice'

function List ({ users, dispatch }) {
  useEffect(() => {
    try {
      dispatch(loadUsers('https://jsonplaceholder.typicode.com/users'))
      // dispatch(setTodos(data))
    } catch (error) {
      console.log(error)
    }
  }, [])
  return (
    <div>
      ListPage<a href='/8888'>To a not found page</a>
      <ul>
        {users.map(user => {
          return <li key={user.id}>{user.name}</li>
        })}
      </ul>
    </div>
  )
}

const mapStateProps = state => {
  return { users: state.users }
}

export default connect(mapStateProps)(List)

实现服务器端Redux

  1. 创建store

因为服务器端的 store 是在接收到请求之后才创建的,所以我们需要把store创建的代码封装在一个函数当中

import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'
import usersReducer from './features/users/usersSlice'
export const createStore = () => {
  return configureStore({
    reducer: {
      todos: todosReducer,
      users: usersReducer
    }
  })
}

export default createStore

  1. 配置store

import { createStore } from '../store/store.server'
import app from './http'
import render from './render'

app.get('*', async (req, res) => {
  // 接收到请求后创建服务器端store
  const store = createStore()
  // 未来会抽取出来作为入口html模版文件
  res.send(render(req, store)) // 把req传递给渲染函数render,以便可以进行路由支持
})

  1. 服务器端的数据填充

当客户端JavaScript被禁用的时候,前端的获取数据的就会失败,页面没有我们原本store中的数据。

问题:当前服务器端创建的Store是空的,组件并不能从Store中获取到任何数据。

解决:服务器端在渲染组件之前就获取到组件所需的数据。

  1. 在组件中添加loadData 方法,此方法用于获取组件所需的数据,方法被服务器端调用。

  1. loadData 方法保存在当前组件的路由信息对象

  1. 服务器端接收到请求之后,根据请求地址匹配出要渲染的组件的路由信息

  1. 从路由信息中获取组件中的loadData 方法并调用方法获取组件所需的数据

  1. 当数据获取完成后再渲染组件并将结果响应给客户端

List页面组件初始化时需要对已经在服务器端渲染过的内容做判断,避免重复加载数据产生二次渲染报错。

import React, { useEffect } from 'react'
import routes from '../routes'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { loadUsers } from '../../store/features/users/usersSlice'

function List ({ users, dispatch }) {
  useEffect(() => {
    try {
      // 这里做数据长度判断是为了避免服务端已经渲染过一遍的情况下页面组件初始化时再次渲染
      users.length === 0 &&
        dispatch(loadUsers('https://jsonplaceholder.typicode.com/users'))
      // dispatch(setTodos(data))
    } catch (error) {
      console.log(error)
    }
    return () => {
      console.log('Component unmounted')
      // 卸载回调
      window.INITIAL_STATE = undefined
    }
  }, [])
  return (
    <div>
      ListPage<Link to='/8888'>To a not found page</Link>
      <ul>
        {users.map(user => {
          return <li key={user.id}>{user.name}</li>
        })}
      </ul>
    </div>
  )
}

const mapStateProps = state => {
  return { users: state.users }
}
// 添加loadData 方法, 被服务器端调用
const loadData = async store => {
  return store.dispatch(loadUsers('https://jsonplaceholder.typicode.com/users'))
}

// export default connect(mapStateProps)(List)
// 把页面组件封装成路由对象所需的内容
export default {
  element: connect(mapStateProps)(List),
  loadData
}

路由信息对象数组,List对象结构中包含loadData 函数。

import React from 'react'
import Home from '../share/pages/Home'
import List from '../share/pages/List'
import NotFound from './pages/NotFound'

export default [
  {
    path: '/',
    element: <Home />,
    exact: true
  },
  {
    path: '/list',
    // element: <List />
    ...List,
    element: <List.element />
  },
  {
    path: '*',
    element: <NotFound />
  }
]

服务器端接收到请求后,获取当前请求路径对应的页面组件路由对象信息,并调用其中的获取数据方法loadData,并把后端的store对象传递进去以便获取数据后调用dispatch触发action更新state。

import routes from '../share/routes'
import { createStore } from '../store/store.server'
import app from './http'
import render from './render'
import { matchRoutes } from 'react-router'

app.get('*', async (req, res) => {
  // 接收到请求后创建服务器端store
  const store = createStore()
  // 获取请求地址
  // 获取路由配置信息
  // 根据地址匹配出要渲染的组件的路由对象信息
  const promises = matchRoutes(routes, req.path).map(({ route }) => {
    // 如何才能知道数据什么时候获取完成
    if (route.loadData) return route.loadData(store)
  })
  // 数据获取完成后将渲染结果响应给客户端
  Promise.all(promises).then(() => {
    res.send(render(req, store)) // 把req传递给渲染函数render,以便可以进行路由支持
  })
})

最后再响应渲染结果之前的后端渲染环节,给页面window挂载INITIAL_STATE记录当前服务器store数据状态,作为客户端的store的初始化状态。

window.INITIAL_STATE = ${serialize(store.getState())}
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import { Provider } from 'react-redux'
import serialize from 'serialize-javascript'
// renderRoutes方法旨在把数组形式的路由规则转换为组件形式的路由规则
// import { useRoutes } from 'react-router'
// import routes from '../share/routes'
import App from '../share/pages/App'
// import { store } from '../store/store.server'

export default (req, store) => {
  // StaticRouter根据请求req的 url 匹配对应的路由获取页面组件渲染成字符串
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.url}>
        <App />
      </StaticRouter>
    </Provider>
  )

  return `<html>
    <head>
      <title>React SSR Base</title>
    </head>
    <body>
      <div id='root'>${content}</div>
      <script>
        // 这里是为了避免前后端页面二次渲染出现错误提示
        // Warning: Did not expect server HTML to contain a <li> in <ul>.  
        window.INITIAL_STATE = ${serialize(store.getState())}
      </script>
      <script src='main.js'></script>
    </body>
  </html>`
}

window.INITIAL_STATE = ${serialize(store.getState())}

这句代码存在的意义是为了给客户端的hydrate提供store的初始化的状态值。

以消除React的警告:

Warning: Did not expect server HTML to contain a <li> in <ul>.

分析:

该警告出现的原因是客户端Store在初始化的状态是没有数据的,在渲染组件的时候生成的是空ul,但是服务器端是先获取数据在进行的组件渲染,其生成的页面元素是有li 子元素的ul,hydrateRoot方法在对比时发现两者不一致,所以报了个警告。

解决思路:

将服务器端获取到的数据回填给客户端,让客户端拥有初始数据。

import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'
import usersReducer from './features/users/usersSlice'

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    users: usersReducer
  },
  preloadedState: window.INITIAL_STATE
})

防止XSS攻击

就是说当服务器端返回的数据中包含恶意的JavaScript代码多的时候,我们要阻止这些恶意代码多的执行。即:转义状态的恶意代码

也就是说我们使用服务器端渲染给客户端返回的页面内容中包含的JavaScript代码片段,是可能被恶意利用的,不能简单的使用JSON.stringify()方法进行字符串序列化,而是需要进行转义,以防止XSS攻击。

例如:

let response = {
    name: '</script></script>setInterval(function(){alert(1)}, 1000)</script>',
    age: 18
}

这时候我们在服务器端返回数据前就需要通过一个serialize方法,它是从serialize-javascript这个包提供的。

import serialize from 'serialize-javascript'

const initialState = serialize(store.getState())

后续我们会继续深入学习当前流行的react服务端渲染支持框架Next.js和Gatsby。

Next.js 集成框架

Gatsby

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399187.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu20.04下安装vm17+win10/11

一、安装vmware17 1、官网下载 vmware官网&#xff1a;https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html 2、安装依赖 sudo apt update sudo apt install build-essential linux-headers-generic gcc make3、权限和安装 到下载的目录下…

vector你得知道的知识

vector的基本使用和模拟实现 一、std::vector基本介绍 1.1 常用接口说明 std::vector是STL中的一个动态数组容器&#xff0c;它可以自动调整大小&#xff0c;支持在数组末尾快速添加和删除元素&#xff0c;还支持随机访问元素。 以下是std::vector常用的接口及其说明&#xf…

品牌软文怎么写?教你几招

软文是什么&#xff1f;软文的本质就是广告&#xff0c;当然不是明晃晃的推销&#xff0c;而是自然隐晦地植入产品信息&#xff0c;引导更多用户自愿下单。 品牌软文对于写手的经验、内容的质量要求都相对较高&#xff0c;否则写出来的软文无法达到预期的效果。品牌软文怎么写…

一个古老的html后台的模板代码

效果图下&#xff1a; css部分代码&#xff1a;/* CSS Document / body{font-family:“宋体”, Arial,Verdana, sans-serif, Helvetica;font-size:12px;margin:0;background:#f4f5eb;color:#000;} dl,ul,li{list-style:none;} a img{border:0;} a{color:#000;} a:link,a:visit…

[css]通过网站实例学习以最简单的方式构造三元素布局

文章目录二元素布局纵向布局横向布局三元素布局b站直播布局实例左右-下 布局左-上下 布局上下-右 布局方案一方案二后言二元素布局 在学习三元素布局之前&#xff0c;让我们先简单了解一下只有两个元素的布局吧 两个元素的相对关系非常简单&#xff0c;不是上下就是左右 纵向布…

Anaconda配置Python科学计算库SciPy的方法

本文介绍在Anaconda环境中&#xff0c;安装Python语言SciPy模块的方法。 SciPy是基于Python的科学计算库&#xff0c;用于解决科学、工程和技术计算中的各种问题。它建立在NumPy库的基础之上&#xff0c;提供了大量高效、易于使用的功能&#xff0c;包括统计分析、信号处理、优…

用一个例子告诉你 怎样在spark中创建累加器

目录 1.说明 1.1 什么是累加器 1.2 累加器的功能 2. 使用累加器 3. 累加器和reduce、fold算子的区别 1.说明 1.1 什么是累加器 累加器是Spark提供的一个共享变量(Shared Variables) 默认情况下&#xff0c;如果Executor节点上使用到了Driver端定义的变量(通过算子传…

Redis常用命令及数据类型参数

1. 针对于string SET key value / GET key SET k1 v1 GET k1 // v1String是二进制安全的&#xff0c;是可变长度的&#xff0c; 底层类似于ArrayList 是可扩容的&#xff0c;最大存储内存为 512MB。 2. 判断key中是否存在某个内容 EXISTS key SET k1 v1 EXISTS k1 // …

Noah-MP陆面过程模型建模方法与站点、区域模拟

陆表过程的主要研究内容以及陆面模型在生态水文研究中的地位和作用 熟悉模型的发展历程&#xff0c;常见模型及各自特点&#xff1b; Noah-MP模型的原理 Noah-MP模型所需的系统环境与编译环境的搭建方法您都了解吗&#xff1f;&#xff1f; linux系统操作环境您熟悉吗&…

Linux驱动中的fasync(异步通知)和fsync

一、fsync用来同步设备的写入操作&#xff0c;考虑把一块设局写入到硬盘的操作&#xff0c;如果使用write函数&#xff0c;函数返回后只能保证数据被写入到驱动程序或者内核管理的数据缓存中&#xff0c;而无法保证数据被真正写入到硬盘的存储块里。但是fync可以做到这一点&…

查找、排序、二叉树的算法,统统记录于此。

文章目录一、查找1. 无序表的顺序查找2. 折半查找3. 分块查找4. 二叉排序树BST5. 哈希表查找二、排序1. 不带哨兵的直接插入排序2. 带哨兵的直接插入排序3. 带哨兵、折半查找的直接插入排序4. 希尔排序5. 冒泡排序6. 快速排序7. 选择排序8. 堆排序9. 归并排序二叉树1. 递归先序…

八,iperf3源代码分析:状态机及状态转换过程--->运行正向TCP单向测试时的客户端代码

本文目录一、测试用命令二、iperf3客户端状态机中各个状态解析状态机迁移图运行正向TCP单向测试时的客户端的状态列表三、iperf3客户端状态机迁移分析A-初始化测试对象&#xff08;NA--->初始化状态&#xff09;:B-建立控制连接&#xff0c;等待服务端PARAM_EXCHANGE的指令&…

西电机试数据结构核心算法与习题代码汇总(机考真题+核心算法)

文章目录前言一、链表问题1.1 反转链表1.1.1 题目1.1.2 代码1.2 多项式加减法1.2.1 题目1.2.2 代码二、队列和栈2.1 学生退学2.1.1 问题2.1.2 代码三、矩阵和串题目3.1 矩阵对角线求和3.1.1 问题3.1.2 代码四、排序问题4.1 多元素排序4.1.1 问题4.1.2 代码五、二叉树5.1 相同二…

synchronize优化偏向锁

偏向锁 轻量级锁在没有竞争时&#xff08;只有自己一个线程&#xff09;&#xff0c;仍然会尝试CAS替换mark word&#xff1b; 会造成一定的性能的损耗&#xff1b; JDK6之中引入了偏向锁进行优化&#xff0c;第一次使用时线程ID注入到Mark word中&#xff0c;之后重入不再进…

旅游预约APP开发具有什么优势和功能

旅游活动目前正在作为广大用户休闲娱乐的一个首选内容&#xff0c;不仅是公司团建活动可以选择旅游&#xff0c;而且一些节假日也可以集结自己的亲朋好友来一次快乐有趣的旅游活动&#xff0c;随着当代人对于旅游的需求呈现上升的趋势&#xff0c;也让旅游预约APP开发开始流行并…

大家都在用哪些研发流程管理软件?

全球知名的10款流程管理软件分享&#xff1a;1.IT/研发项目流程管理&#xff1a;PingCode&#xff1b;2.通用项目流程管理&#xff1a;Worktile&#xff1b;3.销售流程管理&#xff1a;Salesforce Workflow&#xff1b;4.合同流程管理&#xff1a;Agiloft&#xff1b;5.IBM Bus…

20230308 APDL Lsdyna结构学习笔记

可以用鼠标右键进行结构的旋转视图。 一、编辑材料 输入参数分别为: 密度; 弹性模量; 泊松比; 屈服应力; 切线模量 由于模型是分块建立的,这里需要把模型进行粘接 点击booleans(布尔工具) 点击Glue、areas,结构物是由面单元构成的

ReactDOM.render函数内部做了啥

ReactDOM.render函数是整个 React 应用程序首次渲染的入口函数&#xff0c;它的参数是什么&#xff0c;返回值是什么&#xff0c;函数内部做了什么&#xff1f; ReactDOM.render(<App />, document.getElementById("root")); 前序 首先看下首次渲染时候&…

二叉树OJ题目详解

根据二叉树创建字符串 采用前序遍历的方式&#xff0c;将二叉树转换成一个由括号和数字组成的字符串。 再访问每一个节点时&#xff0c;需要分情况讨论。 如果这个节点的左子树不为空&#xff0c;那么字符串应加上括号和左子树的内容&#xff0c;然后判断右子树是否为空&#x…

VBA小模板,跨表统计的2种写法

目标 1 统计一个excel 文件里&#xff0c;多个sheet里的内容2 有的统计需求是&#xff0c;每个表只单表统计&#xff0c;只是进行批量操作3 有的需求是&#xff0c;多个表得某些行列累加等造出来得文件 2 实现方法1 &#xff08;可能只适合VBAEXCEL&#xff0c;不太干净的写法…