乐趣区

React服务端渲染之路06优化

所有源代码、文档和图片都在 github 的仓库里,点击进入仓库

相关阅读

  • React 服务端渲染之路 01——项目基础架构搭建
  • React 服务端渲染之路 02——最简单的服务端渲染
  • React 服务端渲染之路 03——路由
  • React 服务端渲染之路 04——redux-01
  • React 服务端渲染之路 05——redux-02
  • React 服务端渲染之路 06——优化
  • React 服务端渲染之路 07——添加 CSS 样式
  • React 服务端渲染之路 08——404 和重定向
  • React 服务端渲染之路 09——SEO 优化

1. redux 与路由优化

  • 到目前我们已经实现了服务端的异步获取数据,但是现在依然还有几个问题
  • 第一,多级路由与路由精确匹配,我们并没有实现多级路由,而且对路由的校验也比较简单,没有深层次的校验
  • 第二,Promise.all 这个方法,要求的是,里边如果有一个方法失败了,那么整个 Promise.all 就是失败的。但是这样是有问题的,我们是要先通过 Promise.all 获取到数据并修改 store,然后才进行页面渲染的。如果 Promise.all 失败了,那么就不再往下执行,页面就一直处于 loading 的状态,渲染不出页面,这样明显不是我们想要的。我们想要的是,不管 Promise.all 里边有几个失败请求,都不会影响到我们客户端渲染,同时,如果服务端请求失败了,store 里没有拿到我们想要的值,那么客户端还可以继续获取数据,不影响用户使用,这样相当于是双保险
  • 那么接下来,我们就要开始解决这两个问题

1.1 建立 App 组件

  • 在解决这两个问题之前,我们先一个 App 组件,就像是客户端渲染时的 App 组件,主要是为了做整体页面的一个入口,就是说,我们进入到每一个页面及组件的时候,都要通过 App 组件,这样有助于我们建立多级路由
  • 同时,这样我们还可以把 Header 组件从 src/server/render.js 里拿出来,放在 App 组件里,这样便于代码的管理
  • src/App.js
import React, {Component} from 'react';
import Header from './components/Header';

class App extends Component {render() {
    return (
      <div>
        <Header />
        <div className="container">

        </div>
      </div>
    );
  }
}

export default App;
  • 那么我们可以想两个问题,第一个问题是,container 里应该放什么,肯定是路由,因为 Header 是导航,那么导航下边肯定要放路由,这样才能把内容显示出来
  • 第二个问题是,路由怎么放,我们要建立多级路由,肯定不能像之前那样采用 routes.map() 这样的方法显示路由,那么该怎么做,要解决这个问题,我们先修改路由文件

1.2 配置多级路由

  • 多级路由的根入口,就是 App 组件,那么也就是说,根路由就是 App 组件,剩下的 Home 和 News 组件都是子路由,尽管 Home 页面显示的也是根路由,但是我们依然把它作为一个子路由
  • 接下来,我们修改路由文件
  • src/routes.js
// src/routes.js
import React from 'react';
import {Route} from 'react-router-dom';
import App from './App';
import Home from './containers/Home';
import News from './containers/News';

export default [
  {
    path: '/',
    component: App,
    key: 'app',
    routes: [
      {
        path: '/',
        component: Home,
        loadData: Home.loadData,
        exact: true,
        key: '/'
      },
      {
        path: '/news',
        component: News,
        exact: true,
        key: '/news'
      }
    ]
  }
];

1.3 服务端使用多级路由

  • 配置完成了,但是我们该怎么使用呢,这是一个问题,react-router-dom 里有一个 matchPath 的方法,这个方法可以匹配路由,但是它只能匹配单级路由,多级路由不支持,所以我们不能使用 matchPath
  • 有一个库叫做 react-router-config,这个库里边有两个方法,一个是 matchRoutes,另一个是 renderRoutes,这两个方法一个是匹配路由,一个是渲染路由,刚好可以满足我们的需要。但是顺序我们要先搞清楚,肯定是先匹配路由,匹配完了之后,然后才开始渲染路由
  • 下载依赖 npm i react-router-config -S
  • 首先在服务端匹配多级路由

    • 就这一句话,routes 就是在 src/routes.js 配置的新的路由,req.path 就是服务的请求路由,匹配完之后得到的是一个数组对象,每个对象都是所匹配到的路由
    • 有一个不一样的地方就是,匹配完之后得到的 matchedRoutes 里的结构体不一样了,loadData 已经不在是属于单个 item 的,而是 item.route.loadData,其他的没有什么需要改变的
// src/server/render.js
import {matchRoutes} from 'react-router-config';

let matchedRoutes = matchRoutes(routes, req.path);

let promises = [];

matchedRoutes.forEach(item => {
  let loadData = item.route.loadData;

  if (loadData) {const promise = loadData(store);
    promises.push(promise);
  }
});
  • 然后服务端开始渲染路由,渲染路由就比较简单,直接调用 renderRoutes 方法,传入 routes 参数就行
// src/server/render.js

import {renderRoutes} from 'react-router-config';

let domContent = renderToString(<Provider store={store}>
    <StaticRouter context={context} location={req.path}>
      {renderRoutes(routes)
      }
    </StaticRouter>
  </Provider>
);

1.4 客户端使用多级路由

  • 客户端使用的多级路由,和服务端区别不大,不一样的地方就是客户端不需要匹配路由,直接渲染就可以
import {renderRoutes} from 'react-router-config';

hydrate(<Provider store={store}>
  <BrowserRouter>
    {renderRoutes(routes)}
  </BrowserRouter>
</Provider>, window.root);

1.5 App 组件使用多级路由

  • App 组件是客户端和服务端都要使用到的组件,所以这个组件就不太一样,渲染路由需要传递参数
  • renderRoutes 这个方法比较有意思,在服务端使用的时候,直接就获取到了所有匹配到的路由,同时,把匹配到的路由作为属性值保留在组件的 props 里,所以,在服务端 {renderRoutes(routes)} 的时候,就已经把匹配到的信息保留了下来。那么在 App 里使用的时候,直接调用 props 里的属性值就可以了
  • 我们先修改代码,src/App.js
// src/App.js
import React, {Component} from 'react';
import {renderRoutes} from 'react-router-config';
import Header from './components/Header';

class App extends Component {render() {console.log(this.props);
    return (
      <div>
        <Header />
        <div className="container">
          {renderRoutes(this.props.route.routes)
          }
        </div>
      </div>
    );
  }
}

export default App;
  • 我们在控制台查看一下 this.props 的值是什么

  • 可以看到,props 里有路由的三个属性,history,location 和 match,还有一个静态属性 staticContext,这个我们后边再说。最重要的是 route 属性,route.routes 里就是我们定义的路由文件里的 routes 属性,所以我们直接使用这个 routes 属性渲染路由
  • 此时我们再进行页面的切换,服务端异步获取数据,客户端同步修改数据,都可以正常操作,没有任何的问题

1.6 解决 Promise.all 的问题

  • Promise.all 的问题实际上就是每一个 promise 的问题,而每一个 promise 的问题就是这个 promise 的状态可能会失败,那么我们需要解决的就是,如何把 promise 失败的状态,也改为成功的状态。就算失败了,不修改 store 的值也没关系,客户端可以修改,最重要的是不能引起页面一直 loading 而不渲染页面
  • 所以我们修改 promise 的状态,把失败的状态也改为成功的状态,这样就解决了 promise 失败的状态,可以尝试一下把接口改为一个不存在的接口,然后调用接口,看一下页面是否会正常渲染,同时查看一下 store 里的值
  • src/server/render.js
// src/server/render.js
matchedRoutes.forEach(item => {
  let loadData = item.route.loadData;

  if (loadData) {const promise = new Promise((resolve) => {loadData(store).then(resolve).catch(resolve);
    });
    promises.push(promise);
  }
});
  • 这里还需要再进行一步操作,就是在有 loadData 属性的组件里,我们要在 componentDidMount 或者 componentWillMount 生命周期方法里去判断 store 里是否有我们想要的值,如果没有,在这两个生命周期方法里进行调用,这样就不会因为注水失败,而导致页面没有数据
  • 我们修改 Home 组件里的代码,src/containers/Home/index.js
// src/containers/Home/index.js

componentDidMount () {if (!this.props.user.schoolList.length) {this.props.propGetSchoolList();
  }
}
  • 但是,如果服务端没有获取到数据,客户端也没有数据,那就是出 bug 了,这个需要测试接口,还需要测试前端的代码

2. 服务端代理转发

  • 我们开发的时候,因为有安全问题,所以尽量不让客户端去直接调用第三方接口。但是我们服务端渲染的时候,不提供第三方的接口,这该怎么办?我们可以采用代理
  • 代理,跟代购 (海淘) 很像,我们要买一双空军一号耐克鞋,但是由于国内粉丝热情,一鞋难求,而且还有黄牛囤货,价钱太贵。国内没有空军一号,但是美国有呀,我们可以去美国买呀。这下就有问题了,去美国就得买机票办签证定酒店,而且还有拒签的风险,再说,也不可能去美国专门买一双鞋回来的呀。这成本加起来,别说买一双了,买 10 双都够了,何必呢,所以,找代购,代购才多花多少钱,花不了多少钱,比起签证机票酒店便宜了的海了去了,所以,代购,就是我们最好的选择
  • 我们把我们需要购买的物品告诉代购,代购去美国购买,购买完之后,再把物品带回来给我。这个过程,代购就是一个中间人的过程,跟代理是一模一样的,我们的客户端是建立在 Node 服务器上的,客户端发送请求给 Node 服务器,Node 服务器把请求信息转发给第三方服务器,比如 Java,那么 Node 服务器去请求 Java 服务器,Java 服务器把数据返回给 Node 服务器,Node 服务端再把数据返回给客户端。这里的 Node 服务器,就是中间层代理

  • 我们采用 express-http-proxy 这个库来做转发,但是转发之前我们要考虑一下,哪些需要转发?哪些不需要转发?肯定不是所有的都转发,我们要知道第三方服务是做数据提供的,不是做页面渲染和资源提供的,所以只需要转发 api 数据接口就行,如果有需要,也可以转发需要的请求头,比如 cookie 信息,其他的不需要转发,所以,我们统一把以 api 开头的接口,转发给 Java 服务器
  • src/server/index.js
import proxy from 'express-http-proxy';

app.use('/api', proxy('http://localhost:8757', {proxyReqPathResolver(req) {return `/api${req.url}`;
  }
}));
  • 所以,我们实际上仅仅是把以 api 开头的接口进行了转发,其他的都没有修改

3. axios 的请求优化

3.1 为什么要做 axios 的请求优化

  • 既然服务端已经把请求的接口进行了转发,那么我们的客户端在请求的时候,就不需要直接请求第三方服务,直接去请求服务端就可以,因为服务端已经帮助我们做了一层代理
  • 还有一个问题,客户端请求服务端要转发,那么服务端请求的话,服务端就不需要转发,因为服务端直接请求的就是第三方服务,换句话说,客户端和服务端请求的路径,还是有一些不一样的,我们可以对不同的端请求进行不同的处理

3.2 如何对 axios 进行优化

  • 我们通过 axios.create 方法创建一个实例,这个实例本质上与 axios 是一样的,只不过是说,创建出来的做个实例,我们可以对请求头和响应头做统一的处理,这样更加方便。
  • 在 src/client/ 创建一个 request.js 文件,供客户端请求使用
import axios from 'axios';

export default axios.create({baseURL: `/`});
  • 在 src/server/ 创建一个 request.js 文件,供服务端请求使用
import axios from 'axios';

const serverAxios = axios.create({baseURL: 'http://localhost:8757'});

export default serverAxios;

3.3 如何使用 axios 实例

  • 定义 axios 实例之后,我们需要考虑如何去使用
  • 我们知道,redux 的 action 返回的是一个对象,通过 redux-thunk 可以返回一个方法。redux-thunk 还有一个 withExtraArgument 的属性方法,我们可以把 axios 的实例作为 withExtraArgument 的参数进行传递,在 action 中直接使用这个参数
  • 把 axios 的实例传递到 store 里,src/store/index.js
// src/store/index.js
import clientAxios from '../client/request';
import serverAxios from '../server/request';

export const getServerStore = (req) => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);

export const getClientStore = () => {
  let initState = window.context.state;
  return createStore(
    reducers,
    initState,
    composeWithDevTools(applyMiddleware(thunk.withExtraArgument(clientAxios), logger))
  )
};
  • 在 action 中使用,src/store/user/createActions.js
// src/store/user/createActions.js
export const getSchoolList = () => {return (dispatch, getState, axiosInstance) => {return axiosInstance.get('http://localhost:8758/api/getSchoolList').then(res => {if (res.status === 200) {
        let schoolList = res.data.schoolList;
        dispatch({
          type: Types.GET_SCHOOL_LIST,
          payload: schoolList
        });
      }
    });
  }
}
  • 这个时候,我们就实现了针对客户端和服务端不一样,请求的方式也不一样,也方便我们后期对不同的端的 axios 请求做一些扩展

3.4 关于 cookie

  • 我们知道,我们现在的浏览器向服务器发送请求的时候,是有 cookie 信息的,然而现在服务端在向 Java 服务器请求的时候,是没有 cookie 信息的,但是我们经常需要做登录校验的判断,所以我们还需要把 cookie 转发给 Java 服务器
  • 我们修改一下服务端的 axios 实例
  • src/server/render.js
// src/server/render.js
let store = getServerStore(req);
  • src/store/index.js
export const getServerStore = (req) => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);
  • src/server/request.js
// src/server/request.js
const serverAxios = axios.create({
  baseURL: 'http://localhost:8757',
  headers: {cookie: req.get('cookie') || ''
  }
});
  • 实际上就是,我们把请求信息全部通过 getServerStore 传递给 serverAxios 实例,然后在 serverAxios 里修改请求头信息,服务端在向 Java 服务器发送请求的时候,就可以把 cookie 携带上,也可以携带其他的信息,比如请求的其他参数之类的

相关阅读

  • React 服务端渲染之路 01——项目基础架构搭建
  • React 服务端渲染之路 02——最简单的服务端渲染
  • React 服务端渲染之路 03——路由
  • React 服务端渲染之路 04——redux-01
  • React 服务端渲染之路 05——redux-02
  • React 服务端渲染之路 06——优化
  • React 服务端渲染之路 07——添加 CSS 样式
  • React 服务端渲染之路 08——404 和重定向
  • React 服务端渲染之路 09——SEO 优化
退出移动版