React SSR 服务端渲染实现

ReactDOMServer
ReactDOMServer 对象允许你将组件渲染成静态标记。通常,它被使用在 Node 服务端上:
// ES modulesimport ReactDOMServer from \'react-dom/server\';
// CommonJS
var ReactDOMServer = require(\'react-dom/server\');
概览
下述方法可以被使用在服务端和浏览器环境。
- renderToString()
- renderToStaticMarkup()
下述附加方法依赖一个只能在服务端使用的 package(stream)。它们在浏览器中不起作用。
- renderToNodeStream()
- renderToStaticNodeStream()
参考
renderToString()
ReactDOMServer.renderToString(element)将 React 元素渲染为初始 HTML。React 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
renderToStaticMarkup()
ReactDOMServer.renderToStaticMarkup(element)此方法与 renderToString 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。
如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToString 或在前端上使用 ReactDOM.hydrate() 来代替此方法。
renderToNodeStream()
ReactDOMServer.renderToNodeStream(element)将一个 React 元素渲染成其初始 HTML。返回一个可输出 HTML 字符串的可读流。通过可读流输出的 HTML 完全等同于 ReactDOMServer.renderToString 返回的 HTML。你可以使用本方法在服务器上生成 HTML,并在初始请求时将标记下发,以加快页面加载速度,并允许搜索引擎抓取你的页面以达到 SEO 优化的目的。
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
注意:
这个 API 仅允许在服务端使用。不允许在浏览器使用。
通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。
renderToStaticNodeStream()
ReactDOMServer.renderToStaticNodeStream(element)此方法与 renderToNodeStream 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。
通过可读流输出的 HTML,完全等同于 ReactDOMServer.renderToStaticMarkup 返回的 HTML。
如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToNodeStream 或在前端上使用 ReactDOM.hydrate() 来代替此方法。
//下面为转载部分呢内容
早期的SSR(Server Side Rendering) : 服务端渲染,在最早期的网页开发时代,就是采用这种形式,由服务端渲染出页面结构,直接返回给客户端,首屏页面直出,SEO也较友好,但页面路由跳转会导致整个页面重新加载;
**CSR(Client Side Rendering):**随着前后端分离、提高开发效率的思想逐渐流行,react、vue等前端框架的默认支持,前端路由的无刷新切换页面,逐渐成为目前前端开发的主流形式。服务端返回的只是一个空页面,通过客户端加载js,填充生成整个页面展现给客户,减小了服务端的压力,但首屏等待时间较长,而且由于服务端返回空页面,导致对SEO并不友好。
**新时代的SSR:**为了解决CSR的痛点,开发者们重新把目光投向了SSR,结合CSR, 采用同构的模式,刷新SSR直出页面结构,之后客户端接管页面,前端路由无刷新切页,兼具了SSR和CSR的优点。目前结合react和vue也有了对应的SSR框架,next.js和nuxt.js.
本文通过实现简单的demo, 理解React SSR 服务端渲染的过程。
**同构:**同构这个概念存在于 Vue,React 这些新型的前端框架中,同构实际上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示内容和交互写在一起,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互。
SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在,dom的操作在服务端是无法实现的,而虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务端将虚拟dom映射成字符串返回,在客户端将虚拟dom映射为真实dom挂载到页面上。
SSR一般都需要一个node服务器作为中间层,由node处理服务端渲染,以及转发客户端到数据服务器的请求。
1. 配置webpack
既然需要node中间层, 那么就必须有node服务代码和客户端代码的入口,配置两份webpack配置
客户端 webpack.client.js:
const path = require(\'path\');module.exports = {
  mode: \'development\',
  entry: \'./src/client/index.js\',
  output: {
    filename: \'index.js\',
    path: path.resolve(__dirname, \'public\')
  },
  module: {
    rules: [
      { 
        test: /\.jsx?$/,
        loader: \'babel-loader\',
        exclude: /node_modules/,
      }
  }
  resolve: {
    extensions: [".js", ".jsx"], //引入文件时支持省略后缀,配置越多性能消耗越多
    alias: {
        "@": path.resolve(__dirname, "../src"), //引用文件时可以用“@”代表“src”的绝对路径,样式文件中为“~@”
    }
  }
}
服务端 webpack.server.js
const path = require(\'path\');const nodeExternals = require(\'webpack-node-externals\');
module.exports = {
  target: \'node\',
  mode: \'development\',
  entry: \'./src/server/index.js\',
  output: {
    filename: \'bundle.js\',
    path: path.resolve(__dirname, \'build\')
  },
  externals: [nodeExternals()],
  module: {
    rules: [
      { 
        test: /\.jsx?$/,
        loader: \'babel-loader\',
        exclude: /node_modules/,
      }
  }
  resolve: {
    extensions: [".js", ".jsx"], //引入文件时支持省略后缀,配置越多性能消耗越多
    alias: {
        "@": path.resolve(__dirname, "../src"), //引用文件时可以用“@”代表“src”的绝对路径,样式文件中为“~@”
    }
  }
}
webpack-node-externals插件是用于在node环境下三方模块不被打包到最终的源码中,因为node环境下的npm已经安装了这些依赖;target: node 是让node 的核心模块不被webpack打包。
2. 配置路由,前后端同构 --- react-router-config;
对于页面代码我们使用的同一套,只是前后端使用的路由并不同,客户端使用BrowserRouter, 而react-router-dom为客户端渲染提供了StaticRouter, 对于路由的渲染管理建议使用react-router-config
路由配置文件:
import App from "./containers/App"import Home from "./containers/Home";
import Login from "./containers/Login";
import Personal from "./containers/Personal";
import NotFound from "./containers/NotFound";
const routes = [
  {
    path: "/",
    component: App,
    loadData: App.loadData,
    routes:[
      {
        path: "/",
        component: Home,
        exact: true,
        // 每个路由组件的静态方法就是为在服务端的store灌入初始数据
        loadData: Home.loadData,
      },
      {
        path: "/login",
        exact: true,
        component: Login,
      },
      {
        path: "/personal",
        exact: true,
        component: Personal
      },
      {
        component: NotFound,
      }
    ]
  }
]
export default routes;
client端入口路由:
import { renderRoutes } from "react-router-config";import routes from \'../Router\';
const App = () => {
  return <Provider store={getClientStore()}>
      <BrowserRouter>
        {renderRoutes(routes)}
      </BrowserRouter>
    </Provider>
}
// 挂载到页面
ReactDom.render(<App/>, document.querySelector(\'#root\'))
server端入口路由:import { renderRoutes } from "react-router-config";import routes from \'../Router\';
const App = () => {
  return <Provider store={getClientStore()}>
      <StaticRouter location={url} context={{}}>
          {renderRoutes(routes)}
       </StaticRouter>
    </Provider>
}
// 转换为字符串返回
return ReactDom.renderToString(<App/>)
StaticRouter的匹配需要手动传入匹配的路由地址 location={url}。
3. 结合Redux实现首页的数据直出
node转发请求, node端我采用了koa, 使用koa-server-http-proxy做代理请求
import proxy from \'koa-server-http-proxy\';...
app.use(proxy(\'/api\', {
  target: \'http://xxx.com\',
  changeOrigin: true
}))
...
store的创建:
// 服务端store// 服务器端的 Store 是所有用户都要用的,每个用户访问的时候,这个函数重新执行,为每个用户提供一个独立的 Store, 而不是提前创建好的一个单例:
export const getServerStore = (ctx) => createStore(reducer, applyMiddleware(logger, thunk.withExtraArgument(serverHttp)));
// 客户端store
export const getClientStore = () => {
    const initState = window._content.state;
    return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp)));
}
同构的存在服务端的初始页面数据请求不需要代理,而客户端需要代理,解决方案:
axios构建两个实例clientHttp 和 serverHttp,设置不同的baseURL,在createStore应用redux-thunk中间件时 thunk.withExtraArgument(api)传入,在异步dispatch的第三个参数获取到axios实例,通过该实例派发请求。
首屏数据的获取, 通过redux和dispatch去获取
....server端解析页面需要的数据import routes from \'../Router\';
// 获取匹配到的路由
  const matchedRoutes = matchRoutes(routes, ctx.url);
  // 得到数据请求数组 --- 一组promise
  const promiseDatas =  [];
  matchedRoutes.forEach(({route}) => {
    if(route.loadData) {
      promiseDatas.push(route.loadData(store));
    }
  })
  // 执行数据请求,为store灌入初始数据
Promise.all(promises).then(() => {
  // 生成要返回的页面
})
................................
...组件中
import {getNewsList} from \'./store/actions\';
import {useSelector, useDispatch} from \'react-redux\';
import styles from \'./index.css\';
const Home = () => {
  const name = useSelector(({root}) => root.name);
  const list = useSelector(({home}) => home.list);
  const dispatch = useDispatch();
  useEffect(() => {
    if(!list.length) {
      dispatch(getNewsList());
    }
  }, [])
  return <div>
    <h1 className={styles.title}>Home Page !!!</h1>
    <h2>name: {name}</h2>
    <ul>
      {
        list.map(({title, content}) => <li key={title}>
            <h4>{title}</h4>
            <p>{content}</p>
          </li>)
      }
    </ul>
    <button onClick={() => console.log(\'click button\')}>click</button>
  </div> 
}
// 此静态方法为服务端用来做数据直出
Home.loadData = (store) => {
  return store.dispatch(getNewsList());
}
export default Home;
数据的脱水和注水
服务端渲染之后,拿到了首页数据,但客户端会再次渲染,store是空的。解决办法:在服务端渲染的时候将获取到的数据赋值一个全局变量(注水),客户端创建的store以这个变量的值作为初始值(脱水),这样就做到的首屏的数据直出。
// server端注水,再返回的模板字符串中注入数据`<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.8.2/antd.min.css" integrity="sha512-CPolmBEaYWn1PClN5taQQ0ucEhAt+9j7+Tiog/SblkFjZ5k6M3TioqmlpcHKwUhIcsu1s7lgnX4Plsb6T8Kq5A==" crossorigin="anonymous" />
    <title>React-SSR</title>
  </head>
  <body>
    <div id="root">${contents}</div>
    <script>
      window._content = {
        state: ${JSON.stringify(store.getState())}
      }
    </script>
    <script src="/index.js"></script>
  </body>
  </html>`
// 客户端脱水
export const getClientStore = () => {
    const initState = window._content.state;
    return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp)));
}
4. 首屏样式的直出
webpack配置css解析
// webpack.client.js --- 客户端正常配置css-loader和style-loader.....
module: {
    rules: [
      { 
        test: /\.css$/i,
        use: [
          \'style-loader\', 
          {
            loader: \'css-loader\',
            options: {
              importLoaders: 1,
              esModule: false,
              modules: {
                compileType: \'module\',
                localIdentName: \'[name]_[local]_[hash:base64:5]\'
              },
            }
          }
        ]
      },
    ]
  }
.....
// webpack.server.js --- server端使用isomorphic-style-loader代替style-loader, 因为style-loader是生成style标签挂载到页面的,服务端明显不合适
module: {
    rules: [
      { 
        test: /\.css$/,
        use: [\'isomorphic-style-loader\', {
          loader: \'css-loader\',
          options: {
            esModule: false,
            importLoaders: 1,
            modules: {
              compileType: \'module\',
              localIdentName: \'[name]_[local]_[hash:base64:5]\'
            },
          }
        }]
      },
    ]
  }
服务端改造
import React from \'React\';import {renderToString} from \'react-dom/server\';
import { renderRoutes } from "react-router-config";
import StyleContext from \'isomorphic-style-loader/StyleContext\';
// react服务端渲染路由需要使用StaticRouter
import {StaticRouter} from \'react-router-dom\';
import {Provider} from \'react-redux\';
export const render = (store, routes, url, context) => {
  const css = new Set();
  const insertCss = (...styles) => {
    styles.forEach(style => {
      css.add(style._getCss());
    })
  };
  const contents = renderToString(
    <StyleContext.Provider value={{ insertCss }}>
      <Provider store={store}>
        // context可以在服务端渲染时在组件的props.staticContext中获取到,以区分两端环境
        <StaticRouter location={url} context={{}}>
          {renderRoutes(routes)}
        </StaticRouter>
      </Provider>
    </StyleContext.Provider>
  );
  return `<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <style id="ssr-style">${[...css].join(\'\n\')}</style>
    <title>React-SSR</title>
  </head>
  <body>
    <div id="root">${contents}</div>
    <script>
      window._content = {
        state: ${JSON.stringify(store.getState())}
      }
    </script>
    <script src="/index.js"></script>
  </body>
  </html>`;
}
客户端使用
import useStyles from \'isomorphic-style-loader/useStyles\';const Home = () => {
 ...
 // 区分server端和client端
 if(props.staticContext) {
   useStyles(styles);
 }
  return <div>
    ....
  </div> 
}
最后贴一下依赖版本
"dependencies": {    "@babel/core": "^7.12.3",
    "@babel/plugin-proposal-function-bind": "^7.12.1",
    "@babel/plugin-transform-runtime": "^7.12.1",
    "@babel/preset-env": "^7.12.1",
    "@babel/preset-react": "^7.12.1",
    "@babel/preset-stage-0": "^7.8.3",
    "@babel/runtime": "^7.12.1",
    "axios": "^0.21.0",
    "babel-loader": "^8.1.0",
    "css-loader": "^5.0.1",
    "isomorphic-style-loader": "^5.1.0",
    "koa": "^2.13.0",
    "koa-router": "^9.4.0",
    "koa-server-http-proxy": "^0.1.0",
    "koa-static": "^5.0.0",
    "react": "16.14.0",
    "react-dom": "16.14.0",
    "react-redux": "^7.2.2",
    "react-router-config": "^5.1.1",
    "react-router-dom": "^5.2.0",
    "redux": "^4.0.5",
    "redux-thunk": "^2.3.0",
    "style-loader": "^2.0.0",
    "webpack": "5.4.0",
    "webpack-cli": "^4.1.0",
    "webpack-node-externals": "^2.5.2"
  },
  "devDependencies": {
    "redux-logger": "^3.0.6",
    "webpack-merge": "^5.3.0"
  }
部分摘自juejin :https://juejin.cn/post/6907164030385782791
以上是 React SSR 服务端渲染实现 的全部内容, 来源链接: utcz.com/z/383470.html








