React16+Redux+Router4+Koa+Webpack服务器端渲染(按需加载,热更新)

项目结构图


本项目的主要构建思路是:javascript

  1. 开发环境使用webpack-dev-server作后端服务器,实现不刷新页面的热更新,包括组件和reducer变更的热更新。
  2. 生产环境使用koa作后端服务器,与前端公用createApp代码,打包后经过读取文件得到createApp的方法,而后经过react-loadable按需分离代码,在渲染以前请求初始数据,一并塞入首页。

Github地址: https://github.com/wd2010/React-universal-ssrcss

代码结构

前端用react+redux+router4,其中在处理异步action使用redux-thunk。先后端公用了configureStore和createApp,还有后端须要的前端路由配置routesConfig,因此在一个文件里暴露他们三。html

export default {
  configureStore,
  createApp,
  routesConfig
}
其中configureStore.js为:
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from 'history/createMemoryHistory';
import {  routerReducer, routerMiddleware } from 'react-router-redux'
import rootReducer from '../store/reducers/index.js';

const routerReducers=routerMiddleware(createHistory());//路由
const composeEnhancers = process.env.NODE_ENV=='development'?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers];

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

其中我把router放入到reducer中前端

const routerReducers=routerMiddleware(createHistory());//路由
const middleware=[thunkMiddleware,routerReducers];

这样就能够在reducer中直接读取router的信息而不须要从组件中一层层往下传。java

createApp.js
import React from 'react';
import {Provider} from 'react-redux';
import Routers from './router/index';
import Loadable from 'react-loadable';

const createApp=({store,history,modules})=>{
  if(process.env.NODE_ENV==='production'){
    return (
      <Loadable.Capture report={moduleName => modules.push(moduleName)}>
        <Provider store={store}>
          <Routers history={history} />
        </Provider>
      </Loadable.Capture>
    )
  }else{
    return (
      <Provider store={store}>
        <Routers history={history} />
      </Provider>
    )
  }
}

export default createApp;

前端使用的history为:react

import createHistory from 'history/createBrowserHistory';
let history=createHistory();

然后端使用的history为:webpack

import createHistory from 'history/createMemoryHistory';
let history=createHistory();

开发版热加载更新

if(process.env.NODE_ENV==='development'){
  if(module.hot){
    module.hot.accept('./store/reducers/index.js',()=>{
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      /*import('./store/reducers/index.js').then(({default:module})=>{
        store.replaceReducer(module)
      })*/
    })
    module.hot.accept('./app/index.js',()=>{
      let {createApp}=require('./app/index.js');
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      let application=createApp({store,history});
      hydrate(application,document.getElementById('root'));
      /*import('./app/index.js').then(({default:module})=>{
        let {createApp}=module;
        import('./store/reducers/index.js').then(({default:module})=>{
          store.replaceReducer(module)
          let application=createApp({store,history});
          render(application,document.getElementById('root'));
        })
      })*/
    })
  }
}

其中包括组件的热更新和reducer热更新,在引入变化的文件时可使用require或import。git

前端dom节点生成

const renderApp=()=>{
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
}

window.main = () => {
  Loadable.preloadReady().then(() => {
    renderApp()
  });
};

其中 Loadable.preloadReady() 是按需加载'react-loadable'写法,在服务器渲染时也会用到。github

router4动态按需加载

本项目使用react-loadable实现按需加载。web

const Loading=(props)=>
  <div>Loading...</div>

const LoadableHome = Loadable({
  loader: () =>import(/* webpackChunkName: 'Home' */'../../containers/Home'),
  loading: Loading,
});
const LoadableUser = Loadable({
  loader: () =>import(/* webpackChunkName: 'User' */'../../containers/User'),
  loading: Loading,
});

const routesConfig=[{
  path: '/',
  exact: true,
  component: LoadableHome,
  thunk: homeThunk,
}, {
  path: '/user',
  component: LoadableUser,
  thunk: ()=>{},
}];

不单单是在路由里面能够这样使用,也能够在组件中动态import()一个组件能够动态按需加载组件。thunk: homeThunk为路由跳转时的action处理,由于第一种多是在刚开始进入Home页面以前是须要服务器先请求home页面初始数据再渲染给前端,另外一种是服务器进入的是user页面,当从user页面跳转至home页面时也须要请求初始数据,此时是前端组件ComponentDidMount时去请求,因此为了公用这个方法放到跳转路由时去请求,无论是从前端link进去的仍是从服务器进入的。

export const homeThunk=store=>store.dispatch(getHomeInfo())
//模拟动态请求数据
export const getHomeInfo=()=>async(dispatch,getState)=>{
  let {name,age}=getState().homeInfo;
  if(name || age)return
  await new Promise(resolve=>{
    let homeInfo={name:'wd2010',age:'25'}
    console.log('-----------请求getHomeInfo')
    setTimeout(()=>resolve(homeInfo),1000)
  }).then(homeInfo=>{
    dispatch({type:GET_HOME_INFO,data:homeInfo})
  })
}

而服务器端是经过react-router-configmatchRoutes去匹配当前的url和路由routesConfig

let branch=matchRoutes(routesConfig,ctx.req.url)
let promises = branch.map(({route,match})=>{
    return route.thunk?(route.thunk(store)):Promise.resolve(null)
  });
await Promise.all(promises)

koa渲染renderToString

经过前端暴露的createApp、configureStore和routesConfig,经过renderToString方法渲染前端html页面须要的rootString字符串。结合按需加载有:

let store=configureStore();
let history=createHistory({initialEntries:[ctx.req.url]});
let rootString= renderToString(createApp({store,history,modules}));

在koa server 入口文件监听端口时使用react-loadable:

Loadable.preloadAll().then(() => {
  app.listen(port)
})

这样koa后端渲染就能动态按需加载。

而动态生成的html是没有User.js的:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>yyy</title>
  <link href="/css/style.7dae77f648cd2652a570.css" rel="stylesheet"></head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="/manifest.7dae77f6.js"></script>
    <script type="text/javascript" src="/vendors.7dae77f6.js"></script>
    <script type="text/javascript" src="/client.7dae77f6.js"></script>
  </body>
  <script>window.main()</script>
</html>

在每次刷新时,localhost已经包含了首屏的全部内容,解决了首屏白屏和SEO搜索问题。

结语

作完这个练习后我在想,当代码编译以后,服务器渲染以前去请求首屏须要的数据时会出现短暂的白屏,那此时其实仍是没有解决白屏的问题,因此是否能够在编译代码时就去请求全部的首页须要的数据呢?又想到此时的编译过程须要大量的时间,并且请求了本能够在前端路由跳转时的数据。全部首屏白屏问题看似解决,其实还有更好的解决办法。

由于本身也是初次弄react服务端渲染,不少地方是参考了大神们的作法弄出来的,还有不少不懂得地方,请你们多多指点,完整的代码在 https://github.com/wd2010/React-universal-ssr

相关文章
相关标签/搜索