本項目的主要構建思路是:javascript
Github地址: https://github.com/wd2010/React-universal-ssrcss
前端用react+redux+router4,其中在處理異步action使用redux-thunk。先後端公用了configureStore和createApp,還有後端須要的前端路由配置routesConfig,因此在一個文件裏暴露他們三。html
export default { configureStore, createApp, routesConfig }
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
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
const renderApp=()=>{ let application=createApp({store,history}); hydrate(application,document.getElementById('root')); } window.main = () => { Loadable.preloadReady().then(() => { renderApp() }); };
其中 Loadable.preloadReady() 是按需加載'react-loadable'寫法,在服務器渲染時也會用到。github
本項目使用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-config
的matchRoutes
去匹配當前的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)
經過前端暴露的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