• 在前面的文章中,我已经对服务端渲染有了充分介绍,并且实现了最简单的服务端渲染。
  • 在这篇文章中,就基于React,一步一步来搭建一个服务端渲染的项目。

这里是github地址 react-ssr,欢迎start

第一步:React组件渲染

1. 目标

首先,我们将下面这个简单的React组件渲染出来。

在前面的文章中已经渲染出简单的HTML结构,现在需要在页面上渲染出React组件。

// 在页面上渲染出该home组件
const Home = () => {return (<div><div>Hello World</div></div>)
}

  • 你可能会想,直接把React组件引入不就可以了吗?就像这样:
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Home from '../containers/Home';const app = express();const content = ReactDOMServer.renderToString(<Home />); // 引入了组件Home
app.get('/', function(req, res) {res.send(`<html>  <head>    <title>ssr</title>  </head>   <body>    <div id="root">${content}</div>  </body>   </html>   `)
})app.listen(8000, () => {console.log('listen:8000')
})
  • 但是,上面的代码直接执行会失败
  • 首先是es6语法,如果比较高的版本的nodepackage.json中配置就可以了。如果是比较低版本的就不行了。
// 报错信息,无法识别 es mudule
SyntaxError: Cannot use import statement outside a module
  • 还有JSX需要结合babel 使用 @babel/preset-react 进行转换
// 报错信息,无法识别JSX语法
SyntaxError: Unexpected token '<'

所以在正式开始之前,需要将项目进行配置一下

2. 下载依赖

首先需要安装项目所需依赖

express

yarn add express // 用于启动服务

webpack

yarn add -D webpack webpack-cli webpack-dev-server webpack-merge webpack-node-externals

bable

yarn add -D @babel/cli @babel/core @babel/preset-env @babel/preset-react @babel/preset-stage-0 babel-loader @babel/runtime

react

yarn add react react-dom

命令

yarn add -D npm-run-all // 简化命令
yarn add -D nodemon     // 监听变化,自动执行JS文件

注意:可以参考我项目中的依赖版本。

3.项目配置

  • 我们这里开发的Home组件是不能直接在node中运行的,需要借助webpack工具将jsx语法打包编译成js语法,让nodejs可以争取的识别,我们需要创建一个webpack.server.js文件。
// webpack.server.js
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // 服务端运行webpack需要运行NodeExternals, 他的作用是将express这类node模块不被打包到js里。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: /.js?$/,loader: 'babel-loader',exclude: /node_modules/,options: {presets: ['react', 'stage-0', ['env', {targets: {browsers: ['last 2 versions']}}]]}}]}
}

4. 编写基于express服务

需要安装react-dom,借助renderToString将Home组件转换为标签字符串

// src/server/index.js
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Home from '../containers/Home';const app = express();const content = ReactDOMServer.renderToString(<Home />);
app.get('/', function(req, res) {res.send(`<html>  <head>    <title>ssr</title>  </head>   <body>    <div id="root">${content}</div>  </body>   </html>   `)
})app.listen(8000, () => {console.log('listen:8000')
})
// containers/Home/index.js
import React from 'react';
const Home = () => {return (<div><div>Hello World</div></div>)
} export default Home

5. 启动服务

想要访问到页面,我们需要打包产生bundle.js文件,执行该文件就能启动express服务,我们就能通过浏览器窗口的地址栏输入localhost:8000访问到了。

但是在项目进行的过程中,可能需要多次修改,这里我们就需要相应配置从而简化一下。

  • 自动打包:通过--watch监听文件变化进行自动打包
  • 运行JS文件:借助nodemon模块,监听build文件并且发生改变之后重新exec运行node ./build/bundile.js
  • 执行所有命令:创建一个dev命令, 里面执行npm-run-all, --parallel表示并行执行, 执行dev:开头的所有命令。

配置如下:

"scripts": {"dev": "npm-run-all --parallel dev:**","dev:build:server": "webpack --config webpack.server.js --watch","dev:start": "nodemon --watch build --exec node \"./build/bundle.js\""
},

这个时候我想启动服务器同时监听文件改变运行yarn dev就可以了。出现下面内容说明启动成功,在浏览器中可以看到页面内容。

6. 小结

  • React组件服务端渲染需要使用renderToString方法,将React组件渲染为字符串。并注入到需要发送到客户端的HTML中。

第二步:同构

1. 事件绑定

  • 前面我们实现了最基本的SSR服务端渲染的流程,但是通过renderToString方法将React组件渲染为字符串。
  • 因为只是字符串,所以并不能进行交互。也就是说一系列的事件绑定都没有应用上去。

接下来我们就来学习事件的绑定。

(1)做法

  • SSR两套代码,一套在服务端运行一次,一套在客户端运行一次。
  • 首先浏览器向服务器发送请求,服务器返回一个空的html。
  • 浏览器再请求并加载JS。
  • 执行JS代码接管页面执行流程,这个时候就可以触发点击事件了。

客户端获取到的页面结构如下:

(2)同构代码

浏览器后续请求的JS就是同构代码。这里我们同构代码使用hydrate代替render

// src/client/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Home from '../containers/Home';ReactDom.hydrate( < Home / > , document.getElementById('root'))

原组件中增加点击事件

// containers/Home/index.js  添加点击事件
import React from 'react';
const Home = () => {return <div onClick={() => { alert('click'); }}>home</div>
} export default Home

在服务端生成的html中引入JS,

// ./src/server/index.js 在输出的内容中引入JS文件
app.get('/', function(req, res) {res.send(`<html>  <head>    <title>Palate-ssr</title>   </head>   <body>    <div id="root">${content}</div>  <script src="/index.js"></script></body>   </html>   `)
})

(3)配置webpack

同构代码也需要先对React语法进行编译

// webpack.client.js
const path = require('path')
const { merge } = require('webpack-merge')
const config = require('./webpack.base') // 公共部分const clientConfig = {mode: 'development',entry: './src/client/index.js',module: {rules: [{test: /\.css?$/,use: ['style-loader', {loader: 'css-loader',options: {modules: true}}]}]},output: {filename: 'index.js',path: path.resolve(__dirname, 'public')},
}module.exports = merge(config, clientConfig)

抽离和webpack.server.js文件的公共部分,使用webpack-merge插件对内容进行合并。

// webpack.base.js
module.exports = {module: {rules: [{test: /\.jsx?$/,exclude: '/node_modules/',loader: 'babel-loader',options: {presets: ["@babel/react", ['@babel/env', {targets: {browsers: ['last 2 versions']}}]]}}]}
};

2. 前端路由

路由和上面的事件一样也是同构,因为路由本身也是通过修改URL,触发监听URL变化的事件来切换页面内容的。

所以,类似前面的逻辑:

  • 首先浏览器向服务器发送请求,服务器返回一个空的html。
  • 浏览器再请求JS,加载到js后会执行react代码。
  • react代码接管页面执行流程,这个时候可以根据浏览器的地址展示页面内容。

也就是说,首页是服务端拼接好的,后面是基于JS代码进行内容切换,即后续页面内容由JS生成。

(1)下载依赖

yarn add react-router-dom

注意

  • 这里的react-router-dom不能下载6.x版本的。因为最新版已经弃用了staticRouter,而我们需要用到。下载react-router-dom@5.3.0

(2)定义路由规则

// ./src/Routes.js
import React from 'react';
import {Route} from 'react-router-dom'
import Home from './containers/Home';
import Login from './containers/Login';export default (   <div> <Route path='/' exact component={Home}></Route> <Route path='/login' exact component={Login}></Route>   </div>
);

(3)路由导航组件

Header中引入Link, 并且使用他跳转至Home和Login。

// ./src/components/Header/index.js
import React from 'react';
import { Link } from 'react-router-dom';const Header = () => {return (<div><Link to="/">Home</Link><br /><Link to="/login">Login</Link></div>)
}export default Header;

(4)组件中引入导航组件

新建一个Login组件,用于测试路由跳转

// .src/containers/Login/index.js
import React from 'react';
import Header from '../../components/Header';const Login = () => {return ( <div><Header /><div> Login </div> </div>)
};export default Login;

Home组件中也需要引入Header组件

// containers/Home/index.js
import React from 'react';
import Header from '../../components/Header';
const Home = () => {return (<div><Header />home<button onClick={() => { alert('click'); }}>按钮</button></div>)
} export default Home

同构代码

路由规则需要在客户端执行

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import Routes from '../Routes'    const App = () => {    return (    <BrowserRouter>   {Routes}    </BrowserRouter>  )
}ReactDom.hydrate(<App />, document.getElementById('root'));

服务端代码

路由规则也需要在服务端执行

  • 服务端要使用StaticRouter组件替代浏览器的browserRouter
  • StaticRouter是不知道请求路径是什么的,因为他运行在服务器端,这是他不如BrowserRouter的地方,他需要在请求体中获取到路径传递给他,。
  • 这里我们就需要将content写在请求里面。将location的值赋为req.path
// ./src/server/index.js
import express from 'express';
import { render } from './utils';const app = express();app.use(express.static('public'));app.get('*', function(req, res) {res.send(render(req))
});app.listen(8000, () => {console.log('listen:8000')
})

提取出render模块:

// ./src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '../Routes';export const render = (req) => {const content = renderToString((<StaticRouter location={req.path} context={{}}>{Routes}</StaticRouter>));return `<html><head> <title>这里是Palate的博客</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>    `;
}

启动项目,可以看到

页面源码

<html><head><title>这里是Palate的博客</title>
</head><body><div id="root"><div><div><div><a href="/">Home</a><br/><a href="/login">Login</a></div>home<button>按钮</button></div></div></div><script src="/index.js"></script>
</body></html>

注意

  • 当我们在做页面同构的时候,服务器端渲染只放生在我们第一次进入页面的时候,后面使用Link的跳转都是浏览器端的跳转。
  • 所以服务器端渲染不是每个页面都做服务器端渲染,而是只访问的第一个页面具有服务端渲染的特性,其他的页面仍旧是React的路由机制, 这是我们要注意的。

3. 小结

这一步讲同构代码,主要是事件绑定和前端路由的实现。

基本思路就是:

  • 两套代码,一套在服务端运行一次,一套在客户端运行一次。服务端完成html元素的渲染,客户端完成元素事件的绑定。
  • 把React组件和路由规则编译打包成JS文件交给服务端。
  • 服务端先发送HTML给客户端。客户端解析HTML模板时,通过script标签请求并加载JS文件激活页面

路由:服务端需要将路由逻辑执行一遍,服务端的路由使用的是 StaticRouter

第三步:引入Redux

1. 安装依赖

yarn add redux react-redux redux-thunk

2. 创建全局store

// src/client/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
//合并项目组件中store的reducer
const reducer = combineReducers({home: homeReducer
})
// 导出创建store方法
const getStore = () => {return createStore(reducer, applyMiddleware(thunk));
}
export default getStore;

3. 项目引入store

对于store的连接操作,在同构项目中分两个部分,一个是与客户端store的连接,另一部分是与服务端store的连接。通过react-redux中的Provider来传递store

// utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '../Routes';
import getStore from '../store'; // 使用store
import { Provider } from 'react-redux';const store = getStore()
export const render = (req) => {const content = renderToString((<Provider store={store}><StaticRouter location={req.path} context={{}}><Routes /></StaticRouter></Provider>));return `<html><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`;
}
// client/index.js
import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom'
import Routes from '../Routes'
import { Provider } from 'react-redux';
import getstore from '../store'; // 使用storeconst store = getStore()const App = () => {return ( <Provider store={store}><BrowserRouter > {Routes}</BrowserRouter></Provider>    )
}ReactDom.hydrate(<App />, document.getElementById('root'));

到这里,就完成了Redux的引入

4. 创建服务端资源

在根目录的public文件夹下

// api/news.json
{"data": [{"id": 1,"title": "1111111"},{"id": 2,"title": "2222222"},{"id": 3,"title": "3333333"},{"id": 4,"title": "4444444"},{"id": 5,"title": "5555555"}]
}
  • 可以通过localhost:8000/api/news.json访问到该数据,可以在浏览器中尝试。
  • 我们下面要做的做的就是让Home组件请求到这个数据。

注意:浏览器会自动请求一个favicon文件,造成代码重复执行,我们可以在public文件夹中加入这个图片解决该问题。

5. 组件内action和reducer的构建

下载axios

yarn add axios

四个JS文件

在Home文件夹下创建store文件夹,创建以下文件

// constants.js
export const CHANGE_LIST = 'HOME/CHANGE_LIST'; // 存储常量
// actions.js
import axios from 'axios';
import { CHANGE_LIST } from "./constants";//普通action
const changeList = list => ({type: CHANGE_LIST,list
});export const getHomeList = () => {return dispatch => {//请求服务端资源 return axios.get('http://localhost:8000/api/news.json').then((res) => {const list = res.data.data;dispatch(changeList(list))})}
}
// reducer.js
import { CHANGE_LIST } from './constants';
const defaultState = { //初始化数据name: 'palate',list: []
}export default (state = defaultState, action) => {switch (action.type) {case CHANGE_LIST:const newState = {...state,list: action.list // 修改数据}return newStatedefault:return state;}
}
// index.js
import reducer from "./reducer";
//这么做是为了导出reducer让全局的store来进行合并
//那么在全局的store下的index.js中只需引入Home/store而不需要Home/store/reducer.js
//因为脚手架会自动识别文件夹下的index文件
export { reducer }

6. 组件连接全局store

// src/container/Home/index.js
import React, { Component } from 'react';
import Header from '../../components/Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';class Home extends Component {getList() {const { list } = this.props;return list.map(item => <div key={item.id}>{item.title}</div>)}render() {return (<div><Header/><div>Home</div>{this.getList()}<button onClick={() => { alert('click1'); }}>按钮</button></div>)}  componentDidMount() {   this.props.getHomeList() // 更新数据}
}const mapStatetoProps = state => ({ // 传入sotre中的数据list: state.home.list
});const mapDispatchToProps = dispatch => ({ // 传入更新数据方法getHomeList() {    dispatch(getHomeList());}
})// 组件使用react-redux提供的connect方法连接store
export default connect(mapStatetoProps, mapDispatchToProps)(Home);

componentDidMount()在服务端无法执行,需要添加一个静态方法Home.loadData加载数据,这个方法只在服务端有效。

// src/container/Home/index.js
// 省略其他内容...
Home.loadData = (store) => {// 执行action,扩充store。return store.dispatch(getHomeList());
}

7. 改造路由

这里需要改造一下路由配置,根据路由来判断是否需要通过loadData加载数据。

// src/Routes.js
import React from 'react';
import Home from './components/Home';
import Login from './components/Login';export default [{path: '/',component: Home,exact: true,key: 'home',loadData: Home.loadData // 服务端异步获取数据方法},{path: '/login',component: Login,key: 'login',exact: true}
]

使用Router.js的地方也要修改

<Provider store={store}>  <BrowserRouter>   <div> {   routers.map(route => {  <Route {...route} />  })  }   </div>    </BrowserRouter>
</Provider>
<Provider store={store}>  <StaticRouter>    <div> {   routers.map(route => {  <Route {...route} />  })  }   </div>    </StaticRouter>
</Provider>

6. 渲染list数据

先下载react-router-config

server/utils.js中加入以下逻辑

import { matchRoutes as matchRoute } from 'react-router-config';// ...获取store之后://调用matchRoutes用来匹配当前路由(支持多级路由) const matchedRoutes = matchRoute(routes, req.path) //promise对象数组       let promises = [];matchedRoutes.forEach(item => {//如果这个路由对应的组件有loadData方法if (item.route.loadData) {promises.push(item.route.loadData(store));}});  Promise.all(promises).then(() => {  //此时该有的数据都已经到store里面去了(renderToString操作)  //执行渲染的过程(res.send操作)   })

将对server的内容整理一下,提取出render函数

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import Routes from '../Routes';
import getStore from '../store'; // 使用store
import { Provider } from 'react-redux';
import { matchRoutes as matchRoute } from 'react-router-config';export const render = (req, res) => {const store = getStore();const matchedRoutes = matchRoute(Routes, req.path);let promises = [];matchedRoutes.forEach(item => {if (item.route.loadData) {promises.push(item.route.loadData(store));}});Promise.all(promises).then(() => {const content = renderToString(( <Provider store = { store } ><StaticRouter location={req.path} context={{}}><div>{Routes.map(route => (<Route {...route} />))}</div> </StaticRouter > </Provider>));res.send(`<html><body><div id="root">${content}</div></body><script src="/index.js"></script></html>`);})
}

server/index.js

import express from 'express';
import { render } from './utils';const app = express();
app.use(express.static('public'));app.get('*', function(req, res) {render(req, res)
});app.listen(8000, () => {console.log('listen:8000')
})

7. 数据注水和脱水

目的:解决客户端和服务端的store可能不同步的问题。

  • 因为服务端和客户端的store是分别创建的,如果中间有代码不一致,就有可能导致store不同步。
  • 以服务端的store为准,客户端获取服务端的store。

这就需要分两步进行:

(1)注水

数据的“注水”操作,即把服务端的store数据注入到window全局环境中。

做法:在返回的html代码中加入这样一个script标签:

<script>   window.context = { state: ${JSON.stringify(store.getState())}  }
</script>

(2)脱水

接下来是“脱水”处理,换句话说也就是把window上绑定的数据给到客户端的store,可以在客户端store产生的源头进行,即在全局的store/index.js中进行。

import { legacy_createStore as createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';//合并项目组件中store的reducer
const reducer = combineReducers({home: homeReducer
})//创建store,并引入中间件thunk进行异步操作的管理
// 导出创建store的方法,每个用户执行这个函数就会拿到一个新的store
export const getStore = () => {return createStore(reducer, applyMiddleware(thunk));
}//客户端的store创建函数
export const getClientStore = () => {const defaultState = window.context ? window.context.state : {};return createStore(reducer, defaultState, applyMiddleware(thunk));
}

然后对引入getStore的地方进行修改,这里省略了。

这样我们访问浏览器就可以发现页面结构已经渲染出来了。

8. 注意问题

1.渲染出页面后,可能会遇到报错情况

需要在script标签添加 defer属性,避免阻塞HTML解析

2.注意需要使用div包裹一下所有Route,否则会报错。因为react-route-dom要求route成组出现。

3.避免组件重复获取数据

  • 因为数据已经在服务端获取并拼接在HTML结构中了,所以判断服务端已经获取到数据,就不重复获取。
 componentDidMount() {   if (!this.props.list.length) {  //判断当前的数据是否已经从服务端获取this.props.getHomeList() // 请求数据}}
  • 那为什么不直接删掉?
  • 因为该页不一定是作为首页展现,而是跳转到该页面的,这时候还是需要发送请求。

9. 小结

在这一步完成的redux的引入和异步请求数据。

  • 对于store的连接操作,在同构项目中分两个部分,一个是与客户端store的连接,另一部分是与服务端store的连接。
  • 客户端和服务端都需要异步获取数据创建store
  • 客户端需要获取服务端的store,保持数据同步。(注水、脱水)

第四步:node作中间层

1. 为什么引入中间层

  • 前端每次发送请求都是去请求node层的接口,然后node层对于相应的前端请求做转发,用node层去请求真正的后端接口获取数据,获取后再由node层做对应的数据计算等处理操作,然后返回给前端。
  • node层替前端接管了对数据的操作,减轻对服务器端的性能消耗。
  • 之前搭建的SSR框架中,服务端和客户端请求利用的是同一套请求后端接口的代码,但这是不合理的。

2. 组件请求判断

  • 我们先对组件的请求做个修改,判断请求时哪里发出的。
  • 如果是在客户端,那么是发送给该中间层。中间层请求是发送到真正的服务端。
//actions.js
//参数server表示当前请求是否发生在node服务端
const getUrl = (server) => {   return server ? 'xxxx(后端接口地址)' : '/api/sanyuan.json(node接口)';
}
//这个server参数是Home组件里面传过来的,
export const getHomeList = (server) => {   return dispatch => {    return axios.get(getUrl(server))    .then((res) => {    const list = res.data.data;    dispatch(changeList(list))  })  }
}

Home组件调用getHome()时需要传入参数

componentDidMount中调用这个action时传入false,因为这个是客户端发送的请求

loadData函数中调用时传入true,因为这个是中间层发送的请求

3. 中间层转发请求

//增加如下代码
import proxy from 'express-http-proxy';
//相当于拦截到了前端请求地址中的/api部分,然后换成另一个地址
app.use('/api', proxy('http://xxxxxx(服务端地址)', { proxyReqPathResolver: function(req) {   return '/api'+req.url;   }
}));

4. 代码优化

请求在组件中判断并不合理。其实,每个组件中都需要进行一样的判断。

(1)封装axios

我们把这部分判断提取出来,对axios做一个封装。

//新建server/request.js
import axios from 'axios' const instance = axios.create({    baseURL: 'http://xxxxxx(服务端地址)'
})  export default instance //新建client/request.js
import axios from 'axios' const instance = axios.create({    //即当前路径的node服务  baseURL: '/'
})  export default instance

(2)通过store传递

import { legacy_createStore as createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
import clientAxios from '../client/request';
import serverAxios from '../server/request';const reducer = combineReducers({    home: homeReducer
})  export const getStore = () => {    //让thunk中间件带上serverAxios    return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = () => {  const defaultState = window.context ? window.context.state : {};   //让thunk中间件带上clientAxios    return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

(3)组件内获取axios实例

export const getHomeList = () => {//返回函数中的默认第三个参数是withExtraArgument传进来的axios实例  return (dispatch, getState, axiosInstance) => {return axiosInstance.get('资源地址').then((res) => {const list = res.data.data;dispatch(changeList(list))})}
}

5. 另外启动一个服务

这个服务是用来接收中间层发送过来的请求的,将前面放在public/api/news.json放置在该项目中

const express = require("express")
const app = express()app.use(express.static('public'))app.listen(4000, () => { console.log('running 4000') })
  • 将这个是目标服务器,先启动该服务,再启动原来的项目,可以看到数据成功展示。(因为请求方式没有变化)
  • 这里可以先将中间层获取数据的逻辑去掉尝试一下,因为中间层获取数据并拼接到HTML中了,客户端不会重新获取。
  • 或者在login页面测试一下,因为这里是经过跳转的,获取数据就需要客户端发送请求。

6. 小结

  • 在这里将启动了另外一个服务器,作为真正的目标服务器。
  • 而之前搭建的作为中间层,只负责页面渲染和请求转发。
  • 这一步实现了请求转发,也就是客户端发送请求到中间层,中间层转发请求给目标服务器,从目标服务器获取数据。
  • 使用thunk.withExtraArgument传递封装的axios请求。

这里需要注意,中间层渲染出页面的时候会向后端发送请求

第五步:多级路由渲染

1. 修改路由规则

现在的需求是,页面中有一个App组件包含所有组件

import Home from './containers/Home';
import Login from './containers/Login';
import App from './App'   //这里出现了多级路由
export default [{   path: '/',    component: App, routes: [   {   path: "/",    component: Home,    exact: true,    loadData: Home.loadData,    key: 'home',  },  {   path: '/login',   component: Login,   exact: true,    key: 'login', }   ]
}]

2. App组件

前面我们再Home组件和Login组件中分别引用了Header组件,这里实现共用一个

import React from 'react';
import Header from './components/Header'; const  App = (props) => {  console.log(props.route)    return (    <div> <Header></Header>   </div>    )
}   export default App;

记得将Header组件从其它两个组件中去掉。

3. 修改路由渲染形式

将服务端和客户端路由渲染的形式由原来的

{Routes.map(route => (<Route {...Route} />))
}

改为:

import { renderRoutes } from 'react-router-config';{renderRoutes(Routes)}

这里用到的renderRoutes方法,就是根据url渲染一层路由的组件(这里渲染的是App组件),然后将下一层的路由通过props传给目前的App组件,依次循环。

4. 小结

这一步比较简单,新建一个App组件包含所有组件。

第六步:CSS服务端渲染

1. 客户端

下载依赖

yarn add -D style-loader css-loader

对应配置

在webpack文件中进行配置

const clientConfig = {  mode: 'development',  entry: './src/client/index.js',   module: {   rules: [{   test: /\.css?$/,    use: ['style-loader', {   loader: 'css-loader', options: {  modules: true   }   }]  }]  },

引入CSS文件

import styles from './style.css';
  • 这个时候打开启动项目,就可以看到页面有样式了。
  • 但是打开源码,发现并没有样式代码。

2. 服务端

下载依赖

服务端使用的是isomorphic-style-loader,对应配置:

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

获取css代码

  • 引入css文件时,styles中挂了三个函数。通过styles._getCss即可获得CSS代码
  • react-router-dom中的StaticRouter中已经帮我们准备了一个钩子变量context。CSS代码可以从这里传入。
  • 在路由配置对象routes中的组件都能在服务端渲染的过程中拿到这个context,而且这个context对于组件来说,就相当于组件中的props.staticContext。

注意:这个props.staticContext只会在服务端渲染的过程中存在,而客户端渲染的时候不会被定义。

//context从外界传入
<StaticRouter location={req.path} context={context}>    <div> {renderRoutes(routes)}  </div>
</StaticRouter>

我们需要在服务端的render函数执行之前,初始化context变量的值:

let context = { css: [] }

在组件中获取到CSS代码

//context从外界传入
<StaticRouter location={req.path} context={context}>    <div> {renderRoutes(routes)}  </div>
</StaticRouter>

服务端的renderToString执行完成后,拼接css代码

//拼接代码
const cssStr = context.css.length ? context.css.join('\n') : '';

挂载到页面

//放到返回的html字符串里的header里面
<style>${cssStr}</style>

接下来就可以查看结果

这是依赖版本引起的问题,修改webpack配置,将esModule改为false

options: {modules: true,esModule: false
}

3. 小结

  • 在CSS引入组件时获取到CSS的代码,放入routes提供的context中。
  • 在输出HTML前,将CSS代码进行拼接,然后注入到HTML代码中。

到这里,一个服务端渲染项目的就基本搭建好了。

参考:

《React服务器渲染原理解析与实践》

实现一个简单的SSR,了解服务端渲染相关推荐

  1. java服务器向客户端发消息_java一个简单的客户端向服务端发送消息

    java一个简单的客户端向服务端发送消息 客户端代码: package com.chenghu.tcpip; import java.io.IOException; import java.io.Ou ...

  2. 一个简单的完成端口(服务端/客户端)类

    一个简单的完成端口(服务端/客户端)类 作者:spinoza 翻译:麦子芽儿, POWERCPP(后面部分内容) 下载源代码 原文网址:http://www.codeproject.com/KB/IP ...

  3. python批量下载文件只有1kb_详解如何用python实现一个简单下载器的服务端和客户端...

    话不多说,先看代码: 客户端: import socket def main(): #creat: download_client=socket.socket(socket.AF_INET,socke ...

  4. SSR(服务端渲染)

    客户端渲染: 在服务端放了一个html 页面,里面有 客户端发起请求,服务端把页面(响应的是字符串)发送过去,客户端从上到下依次解析,如果在解析的过程中,发现ajax 请求,再次像服务器发送新的请求, ...

  5. 使用Nuxt.js搭建VUE应用的SSR(服务端渲染)

    Nuxt.js的介绍 Nuxt.js概述 nuxt.js简单的说是Vue.js的通用框架,最常用的就是用来作SSR(服务器端渲染) Vue.js是开发SPA(单页应用)的,Nuxt.js这个框架,用V ...

  6. Vue SSR之服务端渲染

    目录 准备工作 开始折腾 1. 首先安装 ssr 支持 2. 增加路由test与页面 3. 在src目录下创建两个js: 4. 修改router配置. 5. 改造main.js 6. entry-cl ...

  7. SSR(服务端渲染)于CSR(客户端渲染)

    SSR (Server Side Rendering,服务端渲染) 希望的是:服务端第一次只把渲染好的 HTML 发给客户端,这样客户端就能直接显示出来网页的样式,首次绘制(First Paint)就 ...

  8. 一个较复杂的React服务端渲染示例(React-Koa-Redux等)

    好久没写博客了~前段时间一直在忙着一个项目上线,最近终于完事了~有一段清闲,正好研究研究React的服务端渲染: 其实React服务端渲染就是用Node.js的v8引擎,在Node端执行JS代码,把R ...

  9. SGAME:一个简单的go游戏服务端框架

    SGame是一个由GO实现的游戏简单服务端框架. 说明 主要是使用GO丰富的库资源和较高的开发效率. 开发简单 可以使用已有的代码框架很方便的构建一个新的进程 方便扩展 基于已有的框架可以动态的扩展进 ...

  10. vue ssr搭建服务端渲染项目

    什么是服务器端渲染 (SSR) Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲染为服务器端的 HT ...

最新文章

  1. pytorch的一些函数
  2. 秘鲁农业功臣-国际农民丰收节贸易会:蔬菜用广州话发音
  3. PHP内核的学习--PHP生命周期
  4. mysql多表查询插入更新_Mysql多表查询,多表插入和多表更新
  5. 为什么说嵌入式开发比单片机要难很多?
  6. YUV常用的两种保存方式_YUY2和YV12
  7. 二维码识别中面临的主要问题
  8. 形式化方法|形式化方法对软件开发的挑战:历史与发展
  9. 程序员35岁辞职后都做了什么工作三位过来人透露了实情,引热议
  10. .net oa 用到那些技术_惨绝人寰!OA高达834分却只配收拒信?
  11. python3虚拟环境的设置
  12. Vivado下使用Microblaze控制LED(vcu118,HLS级开发)
  13. Html + Java登录验证码实现代码
  14. 3DGIS城市规划信息管理系统
  15. 数据结构 栈的结构特点及基本操作
  16. githug关卡小游戏,练习git
  17. [论文笔记]Rob-GAN: Generator, Discriminator, and Adversarial Attacker
  18. 微信小程序订阅信息之Java实现详解
  19. Java位运算优化:位域、位图棋盘等
  20. css模糊遮罩效果_如何实现遮罩模糊样式?

热门文章

  1. JAVA——GUI学习
  2. 城堡争霸服务器维护,城堡争霸怪物攻城服务器心得技巧 压制攻略
  3. 针对商业、公共场所的室内SLAM优化方法
  4. javaweb+jasperreports报表+struts2
  5. 会议 期刊论文 高清晰图片设置
  6. android 仿微博评论编辑框_Android仿新浪微博加#话题的EditText实现
  7. vscode双标签自动同步修改
  8. Windows隐藏应用程序界面
  9. 了解语言模型Model Language,NLP必备
  10. 软件测试常用工具的用途及优缺点比较(详细)