乐趣区

关于react.js:面试官说说ReactSSR的原理

前言

所谓同构,简而言之就是,第一次拜访后盾服务时,后盾间接把前端要显示的界面全副返回,而不是像 SPA 我的项目只渲染一个 <div id="root"></div> 剩下的都是靠 JavaScript 脚本去加载。这样一来能够大大减少首屏等待时间。

同构概念并不简单,它也非我的项目必需品,然而摸索它的原理却是必须的。

浏览本文须要你具备以下技术根底:Node.jsReactReact RouterReduxwebpack

本文将分以下两局部去讲述:

  1. 同构思路剖析,让你对同构有一个概念上的理解;
  2. 手写同构框架,深刻了解同构原理。

同构思路

CSR 客户端渲染

CSR 客户端渲染,这个就是很好了解了,应用 ReactReact Router 前端本人管制路由的 SPA 我的项目,就能够了解成客户端渲染。它有一个十分大的劣势就是,只是首次拜访会申请后盾服务加载相应文件,之后的拜访都是前端本人判断 URL 展现相干组件,因而除了首次访问速度慢些之外,之后的访问速度都很快。

执行命令:create-react-app react-csr 创立一个 React SPA 单页面利用我的项目。
执行命令:npm run start 启动我的项目。

查看网页源代码:只有一个 <div id="root"></div> 和 一些 script 脚本。最终出现进去的界面却是这样的:原理很简略,置信学习过 webpack 的同学都晓得,那就是 webpack 把所有代码都打包成相应脚本并插入到 HTML 界面中,浏览器会解析 script 脚本,通过动静插入 DOM 的形式展现出相应界面。

客户端渲染的优劣势

客户端渲染流程如下:

劣势:

  • 前端负责渲染页面,后端负责实现接口,各自干好各自的事件,对开发效率有极大的晋升;
  • 前端在跳转界面的时候不须要申请后盾,减速了界面跳转的速度,进步用户体验。

劣势:

  • 因为须要期待 JS 文件加载以及后盾接口数据申请因而首屏加载工夫长,用户体验较差;
  • 因为大部分内容都是通过 JS 加载因而搜索引擎无奈爬取剖析网页内容导致网站无奈 SEO

SSR 服务端渲染

SSR 是服务端渲染技术,它自身是一项比拟一般的技术,Node.js 应用 ejs 模板引擎输入一个界面这就是服务端渲染。每次拜访一个路由都是申请后盾服务,从新加载文件渲染界面。

同样咱们也来创立一个简略的 Node.js 服务:

mkdir express-ssr
cd express-ssr
npm init -y
touch app.js
npm i express --save

app.js

const express = require('express')
const app = express()

app.get('/',function (req,res) {
  res.send(`<html>        <head>            <title>express ssr</title>        </head>        <body>            <h1>Hello SSR</h1>        </body>    </html>`)
})

app.listen(3000);

启动服务:node app.js

这就是最简略的服务端渲染一个界面了。 服务端渲染的实质就是页面显示的内容是服务器端生产进去的。

服务端渲染的优劣势

服务端渲染流程:

劣势:

  • 整个 HTML 都通过服务端间接输入 SEO 敌对;
  • 加载首页不须要加载整个利用的 JS 文件,首页加载速度快。

劣势:

  • 拜访一个应用程序的每个界面都须要拜访服务器,体验比照 CSR 稍差。

咱们会发现一件很有意思的事,服务端渲染的长处就是客户端渲染的毛病,服务端渲染的毛病就是客户端渲染的长处,反之亦然。那为何不将传统的纯服务端直出的首屏劣势和客户端渲染站内跳转劣势联合,以获得最优解?这就引出了以后风行的服务端渲染(Server Side Rendering),或者称之为“同构渲染”更为精确。

同构渲染

所谓同构,艰深的讲,就是一套 React 代码在服务器上运行一遍,达到浏览器又运行一遍。
服务端渲染实现页面构造,客户端渲染绑定事件。它是在 SPA 的根底上,利用服务端渲染直出首屏,解决了单页面利用首屏渲染慢的问题。参考 前端进阶面试题具体解答

同构渲染流程

简略同构案例

要实现同构,简略来说就是以下两步:

  1. 服务端要能运行 React 代码;
  2. 浏览器同样运行 React 代码。

1、创立我的项目

mkdir react-ssr
cd react-ssr
npm init -y

2、我的项目目录构造剖析

├── src
│   ├── client
│   │   ├── index.js // 客户端业务入口文件
│   ├── server
│   │   └── index.js // 服务端业务入口文件
│   ├── container    // React 组件
│   │   └── Home
│   │       └── Home.js
│   │
├── config // 配置文件夹
│   ├── webpack.client.js // 客户端配置文件
│   ├── webpack.server.js // 服务端配置文件
│   ├── webpack.common.js // 共有配置文件
├── .babelrc // babel 配置文件
├── package.json

首先咱们编写一个简略的 React 组件,container/Home/Home.js

import React from "react";

const Home = ()=>{
  return (
    <div>
      hello world      <br/>
      <button onClick={()=> alert("hello world")}> 按钮 </button>
    </div>
  )
}

export default Home;

装置客户端渲染的常规,咱们写一个客户端渲染的入口文件,client/index.js

import React from "react";
import ReactDom from "react-dom";
import Home from "../containers/Home";

ReactDom.hydrate(<Home/>,document.getElementById("root"));
// ReactDom.render(<Home/>,document.getElementById("root"));

以前看到的都是调用 render 办法,这里应用 hydrate 办法,它的作用是什么?

ReactDOM.hydrate

render() 雷同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。

咱们都晓得纯正的 React 代码放在浏览器上是无奈执行的,因而须要打包工具进行解决,这里咱们应用 webpack,上面咱们来看看 webpack 客户端的配置:

webpack.common.js

module.exports = {
  module:{
    rules:[
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      }
    ]
  }
}

.babelrc

{
  "presets":[["@babel/preset-env"],
    ["@babel/preset-react"]
  ]
}

webpack.client.js

const path = require("path");
const {merge} = require("webpack-merge");
const commonConfig = require("./webpack.common");

const clientConfig = {
  mode: "development",
  entry:"./src/client/index.js",
  output:{
    filename:"index.js",
    path:path.resolve(__dirname,"../public")
  },
}

module.exports = merge(commonConfig,clientConfig);

代码解析:通过 entry 配置的入口文件,对 React 代码进行打包,最初输入到 public 目录下的 index.js

在以往,间接在 HTML 引入这个打包后的 JS 文件,界面就显示进去了,咱们称之为纯客户端渲染。这里咱们就不这样应用,因为咱们还须要服务端渲染。

接下来,看看服务端渲染文件 server/index.js

import express from "express";
import {renderToString} from "react-dom/server";
import React from "react";
import Home from "../containers/Home";

const app = express(); // {1}
app.use(express.static('public')) // {2}
const content = renderToString(<Home />); //{3}

app.get('/',function (req,res) {// {4}
  res.send(`    <!doctype html>    <html lang="en">      <head>          <meta charset="UTF-8">          <meta name="viewport"                content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">          <meta http-equiv="X-UA-Compatible" content="ie=edge">          <title>React SSR</title>      </head>      <body>        <div id="root">${content}</div>        <script src="/index.js"></script>        </body>    </html>  `)
})

app.listen(3000);

代码解析:

  • {1},创立一个 express 实例对象
  • {2},开启一个动态资源服务,监听 public 目录,还记得客户端的打包文件就放到了 public 目录了把,这里通过监听,咱们就能够这样 localhost:3000/index.js 拜访该动态资源
  • {3},把 React 组件通过 renderToString 办法生成 HTML
  • {4},当用户拜访 localhost:3000 时便会返回 res.send 中的 HTML 内容,该 HTML 中把 React 生成的 HTML 片段也插入进去一起返回给用户了,这样就实现了服务端渲染。通过 <script src="/index.js"></script> 这段脚本加载了客户端打包后的 React 代码,这样就实现了客户端渲染,因而一个简略同构我的项目就这样实现了。

你会发现一个奇怪的景象,为什么写 Node.js 代码应用的却是 ESModule 语法,是的没错,因为咱们要在服务端解析 React 代码,作为同构我的项目,因而对立语法也是十分必要的。所以 Node.js 也须要配置相应的 webpack 编译文件:

webpack.server.js

const path = require("path");
const nodeExternals = require("webpack-node-externals");
const {merge} = require("webpack-merge");
const commonConfig = require("./webpack.common");

const serverConfig = {
  target:"node", // 为了不把 nodejs 内置模块打包进输入文件中,例如:fs net 模块等;mode: "development",
  entry:"./src/server/index.js",
  output:{
    filename:"bundle.js",
    path:path.resolve(__dirname,"../build")
  },
  externals:[nodeExternals()], // 为了不把 node_modules 目录下的第三方模块打包进输入文件中, 因为 nodejs 默认会去 node_modules 目录上来寻找和应用第三方模块。};

module.exports = merge(serverConfig,commonConfig);

到此咱们就实现了一个简略的同构我的项目,这里您应该会有几个疑难?

  1. renderToString 有什么作用?
  2. 为什么服务端加载了一次,客户端还须要再次加载呢?
  3. 服务端加载了 React 输入的代码片段,客户端又执行了一次,这样是不是会加载两次导致资源节约呢?
ReactDOMServer.renderToString(element)

React 元素渲染为初始 HTMLReact 将返回一个 HTML 字符串。你能够应用此办法在服务端生成 HTML,并在首次申请时将标记下发,以放慢页面加载速度,并容许搜索引擎爬取你的页面以达到 SEO 优化的目标。

为什么服务端加载了一次,客户端还须要再次加载呢?

起因很简略,服务端应用 renderToString 渲染页面,而 react-dom/server 下的 renderToString 并没有做事件相干的解决,因而返回给浏览器的内容不会有事件绑定,渲染进去的页面只是一个动态的 HTML 页面。只有在客户端渲染 React 组件并初始化 React 实例后,能力更新组件的 stateprops,初始化 React 的事件零碎,让 React 组件真正“动”起来。

是否加载两次?

如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 办法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个十分高性能的首次加载体验。因而不用放心加载屡次的问题。

是否意犹未尽?那就让咱们更加深刻的学习它,手写一个同构框架,彻底了解同构渲染的原理。

手写同构框架

实现一个同构框架,咱们还有很多问题须要解决:

  1. 兼容路由;
  2. 兼容 Redux
  3. 兼容异步数据申请;
  4. 兼容 CSS 款式渲染。

问题很多,咱们一一击破。

兼容路由

同构我的项目中当在浏览器中输出 URL 后,浏览器是如何找到对应的界面?

  1. 浏览器收到 URL 地址例如:http://localhost:3000/login
  2. 后盾路由找到对应的 React 组件传入到 renderToString 中,而后拼接 HTML 输入页面;
  3. 浏览器加载打包后的 JS 文件,并解析执行前端路由,输入相应的前端组件,发现是服务端渲染,因而只做事件绑定解决,不进行反复渲染,此时前端路由路由开始接管界面,之后跳转界面与后盾无关。

既然须要路由咱们就先装置下:npm install react-router-dom

之前咱们只定义了一个 Home 组件,为了演示路由,咱们再定义一个 Login 组件:

...
import {Link} from "react-router-dom";

const Login = ()=>{
  return (
    <div>
      <h1> 登录页 </h1>
      <br/>
      <Link to="/"> 跳转到首页 </Link>
    </div>
  )
}

革新 Home 组件

const Home = ()=>{
  return (
    <div>
      <h1> 首页 </h1>
      <br/>
      <Link to="/login"> 跳转到登录页 </Link>
      <br/>
      <button onClick={() => console.log("click me")}> 点击 </button>
    </div>
  )
}

当初咱们有两个组件了,能够开始定义相干路由:

src/Routes.js

...
import {Route} from "react-router-dom";

export default (
  <div>
    <Route path="/" exact component={Home} /> // 拜访根门路时展现 Home 组件    <Route path="/login" component={Login} /> // 拜访 /login 门路时展现 Login 组件  </div>
)

革新客户端路由:src/client/index.js

...
import {BrowserRouter} from "react-router-dom";
import Routes from "../Routes";

const App = ()=>{
  return (
    <BrowserRouter>
      {Routes}    </BrowserRouter>
  )
}

ReactDom.hydrate(<App />,document.getElementById("root"));

与一般 SPA 我的项目没有任何区别。

革新服务端路由:src/server/index.js

...
import {StaticRouter} from "react-router-dom";
import Routes from "../Routes";

const app = express();
app.use(express.static('public'))

const render = (req)=>{
  const content = renderToString((<StaticRouter location={req.path}>
      {Routes}    </StaticRouter>
  ));
  return `    <!doctype html>    <html lang="en">      <head>          <meta charset="UTF-8">          <meta name="viewport"                content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">          <meta http-equiv="X-UA-Compatible" content="ie=edge">          <title>React SSR</title>      </head>      <body>        <div id="root">${content}</div>        <script src="/index.js"></script>        </body>    </html>  `
}

app.get('*',function (req,res) {res.send(render(req))
})

服务端跟之前的区别就是这段代码:

<StaticRouter location={req.path}>
  {Routes}
</StaticRouter>

为什么不是 BrowserRouter 而是 StaticRouter 呢?

次要是因为 BrowserRouter 应用的是 History API 记录地位,而 History API 是属于浏览器的 API,在 SSR 的环境下,服务端不能应用浏览器 API

StaticRouter

动态路由,通过初始传入的 location 地址找到相应组件。区别于客户端的动静路由。

兼容 Redux

Redux 始终以来都是 React 技术栈里最难了解的局部,它的概念繁多,如果想要彻底了解本大节及当前的内容,须要您对 Redux 有肯定的理解

安装包:

npm i redux react-redux redux-thunk --save
  • redux 库;
  • react-reduxreactredux 的桥梁;
  • redux-thunkredux 中间件,redux 解决异步申请计划。

src/store/index.js

import {createStore, applyMiddleware} from "redux";
import thunk from "redux-thunk";

const reducer = (state={name:"Lion"},action)=>{return state;}

const getStore = ()=>{return createStore(reducer,applyMiddleware(thunk));
}

export default getStore;

输入一个办法 getStore 用于创立全局 store 对象。

革新 server 端,src/server/render.js

... 省略
import {Provider} from "react-redux";
import getStore from "../store";

export const render = (req)=>{

  const content = renderToString((<Provider store={getStore()}>
      <StaticRouter location={req.path}>
        {Routes}      </StaticRouter>
    </Provider>
  ));
  return `    ... 省略  `
}

通过 Provider 组件把 store 对象数据共享给所有子组件,它的实质还是通过 context 共享数据。

革新 client 端,src/client/index.js

...
import {Provider} from "react-redux";
import getStore from "../store";

const App = ()=>{
  return (<Provider store={getStore()}>
      <BrowserRouter>
        {Routes}      </BrowserRouter>
    </Provider>
  )
}

ReactDom.hydrate(<App />,document.getElementById("root"));

server 端革新十分相似。

redux 都增加结束后,最初咱们在组件中应用 redux 的形式获取数据,革新 Home 组件:

import React from "react";
import {Link} from "react-router-dom";
import {connect} from "react-redux";

const Home = (props)=>{
  return (
    <div>
      <h1> 首页 </h1>
      <div>{props.name}</div>
      <br/>
      <Link to="/login"> 跳转到登录页 </Link>
      <br/>
      <button onClick={() => console.log("click me")}> 点击 </button>
    </div>
  )
}

const mapStateToProps = (state)=>({name:state.name})

export default connect(mapStateToProps,null)(Home);

其实外围就是这几行代码:

const mapStateToProps = (state)=>({name:state.name})

export default connect(mapStateToProps,null)(Home);

connect 接管 mapStateToPropsmapDispatchToProps 两个办法,返回一个高阶函数,这个高阶函数接管一个组件,返回一个新组件,其实就是给传入的组件减少一些属性和性能。

这样一来咱们的 Home 组件就能够应用 name 属性了。革新结束

能够失常应用,这样咱们就轻松的集成了 redux

兼容异步数据申请

在构建企业级我的项目时,redux 应用就更为简单,而且实战中咱们个别都须要申请后盾数据,让咱们来革新革新我的项目,使他成为企业级我的项目。

redux 革新

个别咱们会把 redux 相干的代码都放入 store 文件夹下,咱们来看看它的新目录:

├── src
│   ├── store
│   │   ├── actions.js
│   │   ├── constans.js
│   │   └── reducer.js
└───────└── index.js
  • actions 负责生成 action
  • constans 定义常量;
  • reducer 定义 reducer
  • index 输入 store

actions.js

import axios from 'axios';
import {CHANGE_USER_LIST} from "./constants";

const changeUserList = (list)=>{
  return {
    type:CHANGE_USER_LIST,
    list
  }
}

export const getUserList = (dispatch)=>{return ()=>{axios.get('https://reqres.in/api/users').then((res)=>{dispatch(changeUserList(res.data.data));
    });
  }
}

导出 getUserList 办法,它的主要职责是向后盾发送实在数据申请。

[留神] 这里发送的申请是实在的

constants.js

export const CHANGE_USER_LIST = 'HOME/CHANGE_USER_LIST';

输入常量,定义常量能够保障您在调用时不容易出错。

reducer.js

import {CHANGE_USER_LIST} from "./constants";
// {1}
const defaultState = {userList:[]
};

export default (state = defaultState , action)=>{switch (action.type) {// {2}
    case CHANGE_USER_LIST:
      return {
        ...state,
        userList:action.list
      }
    default:
      return state;
  }
}

代码解析:

  • {1},定义默认 stateuserList 为空数组;
  • {2},当接管到 typeCHANGE_USER_LISTdispatch 时,更新用户列表,这也是咱们在 actions 那里接管到后盾申请数据之后发送的 dispatchdispatch(changeUserList(res.data.data));

redux 革新的差不多了,接下来革新 Home 组件:src/containers/Home/index.js

import React,{useEffect} from "react";
import {Link} from "react-router-dom";
import {connect} from "react-redux";
import {getUserList} from "../../store/actions";

const Home = ({getUserList,name,userList})=>{// {2}
  useEffect(()=>{getUserList();
  },[])

  return (
    <div>
      <h1> 首页 </h1>
      <ul>
        {{/* 3 */}          userList.map(user=>{            const { first_name, last_name, email, avatar, id} = user;            return <li key={id}>
              <img src={avatar} alt="用户头像" style={{width:"30px",height:"30px"}}/>
              <div> 姓名:{`${first_name}${last_name}`}</div>
              <div>email:{email}</div>
            </li>
          })        }      </ul>
      <br/>
      <Link to="/login"> 跳转到登录页 </Link>
      <br/>
      <button onClick={() => console.log("click me")}> 点击 </button>
    </div>
  )
}

const mapStateToProps = (state)=>({
  name:state.name,
  userList:state.userList
});
// {1}
const mapDispatchToProps = (dispatch)=>({getUserList(){dispatch(getUserList(dispatch))
  }
})

export default connect(mapStateToProps,mapDispatchToProps)(Home);

代码解析:

  • {1},mapDispatchToPropsmapStateToProps 作用统一都是 connect 的入参,把相干的 dispatchstate 传入 Home 组件中。
  • {2},useEffect Hook 中调用 getUserList 办法,获取后盾实在数据
  • {3},依据实在返回的 userList 渲染组件

咱们来看看实际效果:

看起来很不错,react-routerredux 都曾经反对了,然而当你查看下网页源码时会发现一个问题:

用户列表数据并不是服务端渲染的,而是通过客户端渲染的。为什么会这样呢?咱们一起剖析下申请过程你就会明确:

接下来咱们次要的指标就是服务端如何可获取到数据?既然 useEffect 不会在服务端执行,那么咱们就本人创立一个 “Hook”

Next.jsgetInitialProps 就是这个被创立的 “Hook”,它的主要职责就是使服务端渲染能够获取初始化数据。

getInitialProps 实现

Home 组件中咱们先增加这个静态方法:

Home.getInitialData = (store)=>{return store.dispatch(getUserList());
}

getInitialData 中做的事件同 useEffect 雷同,都是去发送后盾申请获取数据。

在 React Router 文档中对于服务端渲染想要先获取到数据须要把路由改为动态路由配置。

src/Routes.js

import {Home, Login} from "./containers";
export default [
  {
    key:"home",
    path: "/",
    exact: true,
    component: Home,
  },
  {
    key:"login",
    path: "/login",
    exact: true,
    component: Login,
  }
];

当初剩下最次要的工作就是服务端渲染网页之前拿到后盾数据了。

react-router-config 这个包是 React Router 提供给咱们用于剖析动态路由配置的包。咱们先装置它 npm install react-router-config --save

src/server/render.js

... 省略
import {matchRoutes, renderRoutes} from "react-router-config";
import Routes from "../Routes";

export const render = (req,res)=>{const store = getStore();
  // {1}
  const promises = matchRoutes(Routes, req.path).map(({route}) => {
    const component = route.component;
    return component.getInitialData ? component.getInitialData(store) : null;
  });
  // {2}
  Promise.all(promises).then(()=>{
    const content = renderToString((<Provider store={store}>
                // {3}        <StaticRouter location={req.path}>{renderRoutes(Routes)}</StaticRouter>
      </Provider>
    ));
    res.send(`      ...    `)
  })
}

代码解析:

  • {1},matchRoutes 获取以后拜访路由所匹配到的组件,匹配到的组件如果有 getInitialData 办法就间接调用;
  • {2},component.getInitialData(store) 返回都是 Promise,期待全副 Promise 执行实现后,store 中的 state 就有数据了,此时服务端就曾经获取到相应组件的后盾数据;
  • {3},renderRoutes 它的作用是依据动态路由配置渲染出 <Route /> 组件,相似上面代码,不过 renderRoutes 边界解决的更加欠缺。
{routes.map(route => (<Route {...route} />
))}

仔细的你必定会发现,明明服务器曾经拿到数据了为什么刷新浏览器会一闪一闪呢,起因在于,客户端渲染接管时,初始化的用户列表仍然是个空数组,通过发送后盾申请获取到数据这个异步过程,导致的页面一闪一闪的。它的解决方案有一个术语叫做数据的脱水与注水。

数据脱水与注水

其实非常简单,在渲染服务端时,曾经拿到了后盾申请数据,因而咱们能够做:

  res.send(`      <!doctype html>      <html lang="en">                ...        <body>          <div id="root">${content}</div>          <script>            window.INITIAL_STATE = ${JSON.stringify(store.getState())}
          </script>          <script src="/index.js"></script>          </body>      </html>    `)

通过 INITIAL_STATE 全局变量把后盾申请到的数据存起来。客户端创立 store 时,当做初始化的 state 应用即可:

src/store/index.js

export const getClientStore = ()=>{
  const defaultState = window.INITIAL_STATE;
  return createStore(reducer,defaultState,applyMiddleware(thunk));
}

这样创立进去的 store 初始化的 state 中就曾经有了用户列表。界面就不再会呈现一闪一闪的成果了。

到这里为止,一个繁难的同构框架曾经有了。

兼容 CSS 款式渲染

Home 组件中增加一个款式文件:styles.module.css,轻易写点款式

.box{
    background: red;
    margin-top: 100px;
}

Home 组件中引入款式:

import styles from "./styles.module.css";

<div className={styles.box}>...</div>

间接编译必定报错,咱们须要在 webpack 中增加相应的 loader

webpack.client.js

module:{
    rules:[
      {
        test:/\.css$/i, // 正则匹配到.css 款式文件
        use:[
          'style-loader', // 把失去的 CSS 内容插入到 HTML 中
          {
            loader: 'css-loader',
            options: {modules: true // 开启 css modules}
          }
        ]
      }
    ]
  }

webpack.server.js

  module:{
    rules:[
      {
        test:/\.css$/i,
        use:[
          'isomorphic-style-loader', 
          {
            loader: 'css-loader',
            options: {modules: true}
          },
        ]
      }
    ]
  }

仔细的你必定会发现,server 端的配置应用了 isomorphic-style-loaderclient 端应用了 style-loader,它们的区别是什么?

isomorphic-style-loader vs style-loader

style-loader 它的作用是把生成进去的 css 款式动静插入到 HTML 中,然而在服务端渲染是没有方法应用 DOM 的,因而服务端渲染不能应用它。

isomorphic-style-loader 次要是导出了 3 个函数,_getCss_insertCss_getContent,供使用者调用,而不再是简略粗犷的插入 DOM 中。

server 端反对款式

src/server/render.js

export const render = (req,res)=>{
  const context = {css: []
  };
  Promise.all(promises).then(()=>{
    const content = renderToString((<Provider store={store}>
        <StaticRouter location={req.path} context={context}>{renderRoutes(Routes)}</StaticRouter>
      </Provider>
    ));

  const css = context.css.length ? context.css.join('\n') : '';

  res.send(`      <!doctype html>      <html lang="en">        <head>                        ...            <style>${css}</style>        </head>        ...      </html>    `)
}

StaticRouter 反对传入一个 context 属性,这样被拜访的组件则能够共享该属性。在被拜访组件的生命周期中通过调用 _getCss() 办法向 staticContext 中推入款式。最初在服务端拼接出所有款式插入到 HTML 中。

Home 组件 (革新成 class 组件 )

  componentWillMount() {if(this.props.staticContext){this.props.staticContext.css.push(styles._getCss());
    }
  }

componentWillMount 生命周期(服务端渲染会调用该生命周期),向 staticContext 中推入组件应用的款式。最初在服务端拼接成残缺的款式文件。

这里应用 staticContext 能够实现,应用 redux 也一样能够实现。

总结

到此为止咱们就实现了一个繁难的同构框架。上面做一个简略的总结:

  • 同构渲染其实就是将同一套 react 代码在服务端执行一遍渲染动态页面,又在客户端执行一遍实现事件绑定。
  • 它的劣势是,放慢首页访问速度以及 SEO 敌对,如果你的我的项目没有这方面的需要,则不须要抉择同构。
  • 它的毛病是,不能在服务端渲染期间操作 DOMBOMapi,比方 documentwindow 对象等,并且它减少了代码的复杂度,某些代码操作须要辨别运行环境。
  • 在理论我的项目中,倡议应用 Next.js 框架去做,站在伟人的肩旁上,能够少踩很多坑。
退出移动版