React16+Redux+Router4+Koa+Webpack服務器端渲染(按需加載,熱更新)

項目結構圖

本項目的主要構建思路是:

  1. 開發環境使用webpack-dev-server作後端服務器,實現不刷新頁面的熱更新,包括組件和reducer變更的熱更新。
  2. 生產環境使用koa作後端服務器,與前端公用createApp代碼,打包後經過讀取文件得到createApp的方法,而後經過react-loadable按需分離代碼,在渲染以前請求初始數據,一併塞入首頁。

Github地址: github.com/wd2010/Reac…javascript

代碼結構

前端用react+redux+router4,其中在處理異步action使用redux-thunk。先後端公用了configureStore和createApp,還有後端須要的前端路由配置routesConfig,因此在一個文件裏暴露他們三。css

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中html

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

這樣就能夠在reducer中直接讀取router的信息而不須要從組件中一層層往下傳。前端

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爲:java

import createHistory from 'history/createBrowserHistory'let history=createHistory();
複製代碼

然後端使用的history爲:react

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。webpack

前端dom節點生成

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

window.main = () => {
  Loadable.preloadReady().then(() => {
    renderApp()
  });
};
複製代碼

其中 Loadable.preloadReady() 是按需加載'react-loadable'寫法,在服務器渲染時也會用到。git

router4動態按需加載

本項目使用react-loadable實現按需加載。github

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進去的仍是從服務器進入的。web

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服務端渲染,不少地方是參考了大神們的作法弄出來的,還有不少不懂得地方,請你們多多指點,完整的代碼在 github.com/wd2010/Reac…

相關文章
相關標籤/搜索