实现一个简单的SSR,了解服务端渲染
- 在前面的文章中,我已经对服务端渲染有了充分介绍,并且实现了最简单的服务端渲染。
- 在这篇文章中,就基于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
语法,如果比较高的版本的node
在package.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,了解服务端渲染相关推荐
- java服务器向客户端发消息_java一个简单的客户端向服务端发送消息
java一个简单的客户端向服务端发送消息 客户端代码: package com.chenghu.tcpip; import java.io.IOException; import java.io.Ou ...
- 一个简单的完成端口(服务端/客户端)类
一个简单的完成端口(服务端/客户端)类 作者:spinoza 翻译:麦子芽儿, POWERCPP(后面部分内容) 下载源代码 原文网址:http://www.codeproject.com/KB/IP ...
- python批量下载文件只有1kb_详解如何用python实现一个简单下载器的服务端和客户端...
话不多说,先看代码: 客户端: import socket def main(): #creat: download_client=socket.socket(socket.AF_INET,socke ...
- SSR(服务端渲染)
客户端渲染: 在服务端放了一个html 页面,里面有 客户端发起请求,服务端把页面(响应的是字符串)发送过去,客户端从上到下依次解析,如果在解析的过程中,发现ajax 请求,再次像服务器发送新的请求, ...
- 使用Nuxt.js搭建VUE应用的SSR(服务端渲染)
Nuxt.js的介绍 Nuxt.js概述 nuxt.js简单的说是Vue.js的通用框架,最常用的就是用来作SSR(服务器端渲染) Vue.js是开发SPA(单页应用)的,Nuxt.js这个框架,用V ...
- Vue SSR之服务端渲染
目录 准备工作 开始折腾 1. 首先安装 ssr 支持 2. 增加路由test与页面 3. 在src目录下创建两个js: 4. 修改router配置. 5. 改造main.js 6. entry-cl ...
- SSR(服务端渲染)于CSR(客户端渲染)
SSR (Server Side Rendering,服务端渲染) 希望的是:服务端第一次只把渲染好的 HTML 发给客户端,这样客户端就能直接显示出来网页的样式,首次绘制(First Paint)就 ...
- 一个较复杂的React服务端渲染示例(React-Koa-Redux等)
好久没写博客了~前段时间一直在忙着一个项目上线,最近终于完事了~有一段清闲,正好研究研究React的服务端渲染: 其实React服务端渲染就是用Node.js的v8引擎,在Node端执行JS代码,把R ...
- SGAME:一个简单的go游戏服务端框架
SGame是一个由GO实现的游戏简单服务端框架. 说明 主要是使用GO丰富的库资源和较高的开发效率. 开发简单 可以使用已有的代码框架很方便的构建一个新的进程 方便扩展 基于已有的框架可以动态的扩展进 ...
- vue ssr搭建服务端渲染项目
什么是服务器端渲染 (SSR) Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲染为服务器端的 HT ...
最新文章
- pytorch的一些函数
- 秘鲁农业功臣-国际农民丰收节贸易会:蔬菜用广州话发音
- PHP内核的学习--PHP生命周期
- mysql多表查询插入更新_Mysql多表查询,多表插入和多表更新
- 为什么说嵌入式开发比单片机要难很多?
- YUV常用的两种保存方式_YUY2和YV12
- 二维码识别中面临的主要问题
- 形式化方法|形式化方法对软件开发的挑战:历史与发展
- 程序员35岁辞职后都做了什么工作三位过来人透露了实情,引热议
- .net oa 用到那些技术_惨绝人寰!OA高达834分却只配收拒信?
- python3虚拟环境的设置
- Vivado下使用Microblaze控制LED(vcu118,HLS级开发)
- Html + Java登录验证码实现代码
- 3DGIS城市规划信息管理系统
- 数据结构 栈的结构特点及基本操作
- githug关卡小游戏,练习git
- [论文笔记]Rob-GAN: Generator, Discriminator, and Adversarial Attacker
- 微信小程序订阅信息之Java实现详解
- Java位运算优化:位域、位图棋盘等
- css模糊遮罩效果_如何实现遮罩模糊样式?