React-ssr

1、什么是服务端渲染

1.1 服务端渲染

页面上的内容是有服务器生产的

  1. cnpm i express -S
  1. let express = require('express')
  2. let app = express();
  3. app.get('/',(req,res)=>{
  4. res.send(`
  5. <html>
  6. <body>
  7. <div id="root">hello</div>
  8. </body>
  9. </html>
  10. `)
  11. })
  12. app.listen(3000,function(){
  13. console.log(`server started at port 3000`);
  14. })

1.2 客户端渲染

页面内容由浏览器加载脚本代码

  1. let express = require('express');
  2. let app = express();
  3. app.get('/',(req,res)=>{
  4. res.send(`
  5. <html>
  6. <body>
  7. <div id="root"></div>
  8. <script>
  9. document.getElementById('root').innerHTML = 'hello1'
  10. </script>
  11. </body>
  12. </html>
  13. `)
  14. })
  15. app.listen(3000,function(){
  16. console.log(`server started at port 3000`);
  17. })

2、配置路由

router.js

  1. import React,{ Component, Fragment } from 'react'
  2. import { Route } from 'react-router-dom'
  3. import Home from './containers/Home'
  4. import Counter from './containers/Counter'
  5. export default (
  6. <Fragment>
  7. <Route path="/" exact component={Home}/>
  8. <Route path="/counter" exact component={Counter}/>
  9. </Fragment>
  10. )

Client/index.js

  1. import React, { Component, Fragment } from "react";
  2. import ReactDOM from "react-dom";
  3. import routes from "../router";
  4. import { BrowserRouter } from "react-router-dom";
  5. import Header from "../components/Header";
  6. import { Provider } from "react-redux";
  7. import { getClientStore } from "../store";
  8. ReactDOM.hydrate(
  9. <Provider store={getClientStore()}>
  10. <BrowserRouter>
  11. <Fragment>
  12. <Header />
  13. <div className="container" style={{ marginTop: 70 }}>
  14. {routes}
  15. </div>
  16. </Fragment>
  17. </BrowserRouter>
  18. </Provider>,
  19. document.getElementById("root")
  20. );

Server/render.js

  1. import React, { Component, Fragment } from "react";
  2. // import Home from "../containers/Home";
  3. // import Counter from "../containers/Counter";
  4. import { StaticRouter } from "react-router-dom";
  5. import { renderToString } from "react-dom/server";
  6. import routes from "../router";
  7. import Header from "../components/Header";
  8. import { Provider } from "react-redux";
  9. import { getServerStore } from "../store";
  10. export default function (req, res) {
  11. // var html = renderToString(<Counter/>)
  12. var html = renderToString(
  13. <Provider store={getServerStore(req)}>
  14. <StaticRouter context={{}} location={req.path}>
  15. <Fragment>
  16. <Header />
  17. <div className="container" style={{ marginTop: 70 }}>
  18. {routes}
  19. </div>
  20. </Fragment>
  21. </StaticRouter>
  22. </Provider>
  23. );
  24. console.log(html);
  25. res.send(`
  26. <html>
  27. <head>
  28. <title>React-SSR</title>
  29. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" />
  30. </head>
  31. <body>
  32. <div id="root">${html}</div>
  33. <script></script>
  34. <script src="/client.js"></script>
  35. </body>
  36. </html>`);
  37. }

3、配置redux

Store/index.js

  1. import {createStore, applyMiddleware , combineReducers} from 'redux';
  2. import thunk from 'redux-thunk';
  3. import logger from 'redux-logger';
  4. import reducers from './reducers';
  5. export function getClientStore(){
  6. return createStore(reducers,applyMiddleware(thunk,logger))
  7. }
  8. export function getServerStore(){
  9. return createStore(reducers,applyMiddleware(thunk,logger))
  10. }

4、请求接口

api/server.js

  1. let express = require('express');
  2. let bodyParser = require('body-parser');
  3. let session = require('express-session');
  4. let app = express();
  5. // 如果浏览器不直接访问 API 接口服务器,那么就不存在跨域的问题,node 服务器访问 API 接口服务器不存在跨域问题
  6. app.use(bodyParser.urlencoded({extended: true}));
  7. app.use(bodyParser.json());
  8. app.use(session({
  9. saveUninitialized: true,
  10. resave: true,
  11. secret: 'react-test'
  12. }));
  13. let users = [{id: 1, name: 'user1'}, {id: 2, name: 'user2'}];
  14. app.get('/api/users', function (req, res) {
  15. res.json(users);
  16. });
  17. app.post('/api/login', function (req, res) {
  18. let user = req.body;
  19. req.session.user = user;
  20. res.json({
  21. code: 0,
  22. data: {
  23. user,
  24. success: '登录成功!'
  25. }
  26. });
  27. });
  28. app.get('/api/logout', function (req, res) {
  29. req.session.user = null;
  30. res.json({
  31. code: 0,
  32. data: {
  33. success: '退出成功!'
  34. }
  35. });
  36. });
  37. app.get('/api/user', function (req, res) {
  38. let user = req.session.user;
  39. if (user) {
  40. res.json({
  41. code: 0,
  42. data: {
  43. success: '获取用户信息成功!',
  44. user
  45. }
  46. });
  47. } else {
  48. res.json({
  49. code: 1,
  50. data: {
  51. error: '用户未登录!'
  52. }
  53. });
  54. }
  55. });
  56. app.listen(4000);

Store/action/home.js 客户端axios请求

  1. import * as types from "../action-types";
  2. import axios from "axios";
  3. export default {
  4. getHomeList() {
  5. // 这里使用 redux-thunk 中间件
  6. return function (dispatch, getState, request) {
  7. return axios.get("http://localhost:4000/api/users").then(function (result) {
  8. let list = result.data;
  9. dispatch({
  10. type: types.SET_HOME_LIST,
  11. payload: list,
  12. });
  13. });
  14. };
  15. },
  16. };

服务端接口数据的获取

5、启动

  1. npm run dev

6、问题

  • 解决跨域
    1、不是更好的方式 直接访问api不合理 后端改了要改地址 不好做权限设置,更好的方式是node做接口转发

    1. let cors = require('cors');
    2. app.use(cors({
    3. origin:'http://localhost:3000'
    4. }));
    1. 2node代理添加node中间层
    2. server.js
  1. // 如果是服务器端请求数据,则直接访问 API 服务器的 4000 端口
  2. // 如果是客户端请求数据,则要访问 node 服务器(中间层)的 3000 端口
  3. // 让 node 服务器帮我们访问 API 服务器的 4000 端口请求数据
  4. // 总结:客户端向 node 服务器请求数据,node 服务器转发给 API 服务器
  5. // 如果浏览器不直接访问 API 接口服务器,那么就不存在跨域的问题,node 服务器访问 API 接口服务器不存在跨域问题
  6. // 如果访问的路径是以 /api 开头的,会交给代理服务器处理
  7. / /api/users => http://127.0.0.1:4000/api/users
  8. app.use('/api', proxy('http://127.0.0.1:4000', {
  9. proxyReqPathResolver(req) {
  10. return `/api${req.url}`;
  11. }
  12. }));

Store/index.js

在readux-thunk中传入不同的客户端和服务端的request

  1. export function getClientStore(){
  2. let initState = window.context.state;
  3. return createStore(reducers,initState,applyMiddleware(thunk.withExtraArgument(ClientRequest),logger))
  4. }
  1. import axios from 'axios';
  2. // 如果是服务器端请求数据,则直接访问 API 服务器的 4000 端口
  3. // 如果是客户端请求数据,则要访问 node 服务器(中间层)的 3000 端口
  4. // 让 node 服务器帮我们访问 API 服务器的 4000 端口请求数据
  5. // 创建一个 axios 的实例, 配置 baseURL 的基准路径
  6. export default axios.create({
  7. baseURL: '/'
  8. });
  • 难点怎么匹配路由获取对应的接口数据
    将路由改成配置项的方式
    ```javascript // 集中式路由 export default [ [
    1. {
    2. path: '/',
    3. component: Home,
    4. exact: true,
    5. key: '/',
    6. // 加载数据,如果此配置项有了这个属性,那么意味着需要加载异步数据
    7. loadData: Home.loadData
    8. },
    9. {
    10. path: '/counter',
    11. component: Counter,
    12. key: '/counter'
    13. }
    ] ]

// export default ( // // // // // )

  1. 服务端获取数据, 通过路由中的loadData加载一个方法,执行调用页面接口,将数据放到window.context上面,在客户端的store中将页面初始值放到这里
  2. ```javascript
  3. export function getClientStore(){
  4. let initState = window.context.state;
  5. return createStore(reducers,initState,applyMiddleware(thunk,logger))
  6. }

客户端页面调用的时候,如果有初始值就不要在调用接口

  1. // 注意:这里访问的是客户端的仓库
  2. if(this.props.list.length == 0){
  3. this.props.getHomeList();
  4. }
  • 客户端服务端统一样式
    react-reater-config 模块 匹配多级路由
  1. npm i react-reater-config

import { renderRoutes , matchRoutes } from ‘react-reater-config’

  1. import { renderRoutes , matchRoutes } from 'react-reater-config'
  2. {/*
  3. {routes.map((route, index) => (
  4. <Route {...route} key={index} />
  5. ))}
  6. */}
  7. {renderRoutes(routes)}

App.js

  1. import React, { Component, Fragment } from "react";
  2. import { renderRoutes, matchRoutes } from "react-router-config";
  3. import Header from "../components/Header";
  4. class App extends Component {
  5. render() {
  6. // console.log(this.props)
  7. return (
  8. <Fragment>
  9. <Header />
  10. <div className="container" style={{ marginTop: 70 }}>
  11. {renderRoutes(this.props.route.routes)}
  12. </div>
  13. </Fragment>
  14. );
  15. }
  16. }
  17. export default App;

7、权限

注册登陆

登陆通过设置cookie的携带进行,设置axios的cookie值

server/request.js

  1. export default (req) => axios.create({
  2. baseURL: 'http://localhost:4000',
  3. headers: {
  4. cookie: req.get('cookie') || ''
  5. }
  6. });

Api/server.js 接口设置session

  1. let session = require('express-session');
  2. let app = express();
  3. // let cors = require('cors');
  4. // 如果浏览器不直接访问 API 接口服务器,那么就不存在跨域的问题,node 服务器访问 API 接口服务器不存在跨域问题
  5. app.use(bodyParser.urlencoded({extended: true}));
  6. app.use(bodyParser.json());
  7. app.use(session({
  8. saveUninitialized: true,
  9. resave: true,
  10. secret: 'react-test'
  11. }));

server/index.js 设置正向代理

  1. // 如果是服务器端请求数据,则直接访问 API 服务器的 4000 端口
  2. // 如果是客户端请求数据,则要访问 node 服务器(中间层)的 3000 端口
  3. // 让 node 服务器帮我们访问 API 服务器的 4000 端口请求数据
  4. // 总结:客户端向 node 服务器请求数据,node 服务器转发给 API 服务器
  5. // 如果浏览器不直接访问 API 接口服务器,那么就不存在跨域的问题,node 服务器访问 API 接口服务器不存在跨域问题
  6. // 如果访问的路径是以 /api 开头的,会交给代理服务器处理
  7. // /api/users => http://127.0.0.1:4000/api/users
  8. app.use('/api', proxy('http://127.0.0.1:4000', {
  9. proxyReqPathResolver(req) {
  10. return `/api${req.url}`;
  11. }
  12. }));

App.js 服务端通过App.loadData这个属性获取getUser这个方法,来判断是否登陆了

  1. App.loadData = function (store) {
  2. return store.dispatch(actions.getUser());
  3. };
  4. // 设置重定向到首页
  5. return this.props.user ? (
  6. <div className="row">
  7. <div className="col-md-6 col-md-offset-6">个人中心</div>
  8. </div>
  9. ) : (
  10. <Redirect to="/login"/>
  11. );

8、设置css样式

webapck.config.js

  1. // 服务端
  2. module:{
  3. rules:[
  4. {
  5. test:/\.css$/,
  6. use:[
  7. 'isomorphic-style-loader',
  8. {
  9. loader:'css-loader',
  10. options:{
  11. modules:true
  12. }
  13. }
  14. ]
  15. }
  16. ]
  17. }
  18. // 客户端
  19. module:{
  20. rules:[
  21. {
  22. test:/\.css$/,
  23. use:[
  24. 'style-loader',
  25. {
  26. loader:'css-loader',
  27. options:{
  28. modules:true
  29. }
  30. }
  31. ]
  32. }
  33. ]
  34. }

server/render.js 通过context收集css

  1. import React, { Component, Fragment } from "react";
  2. // import Home from "../containers/Home";
  3. // import Counter from "../containers/Counter";
  4. export default function (req, res) {
  5. // css 代码进行收集
  6. let context = { csses:[] };
  7. // ...
  8. let cssStr = context.csses.join("\n")
  9. // ...
  10. res.send(`
  11. <html>
  12. <head>
  13. <title>React-SSR</title>
  14. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" />
  15. <style type="text/css">${cssStr}</style>
  16. </head>
  17. <body>
  18. <div id="root">${html}</div>
  19. <script>
  20. window.context={
  21. state:${JSON.stringify(store.getState())}
  22. }
  23. </script>
  24. <script src="/client.js"></script>
  25. </body>
  26. </html>`);
  27. });
  28. }

withStyles.js. 设置高阶组件 通过 this.props.staticContext 让服务端拿到样式, 使样式和客户端渲染一致,解决刷新页面闪动的问题

  1. import React,{Component} from 'react';
  2. export default function withStyles(OriginalComponent,styles){
  3. class ProxyComponent extends Component{
  4. componentWillMount(){
  5. if(this.props.staticContext){
  6. // _getCss方法可以得到处理后的 css 源代码
  7. this.props.staticContext.csses.push(styles._getCss());
  8. }
  9. }
  10. render(){
  11. return <OriginalComponent {...this.props}/>
  12. }
  13. }
  14. return ProxyComponent;
  15. }

未完任务

  • 登陆和退出的相关跳转