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

前端之家收集整理的这篇文章主要介绍了React16+Redux+Router4+Koa+Webpack服务器端渲染(按需加载,热更新)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

项目结构图


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

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

Github地址: [https://github.com/wd2010/Rea...]()

代码结构

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

@H_301_21@export default { configureStore,createApp,routesConfig }
其中configureStore.js为:
@H_301_21@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中

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

这样就可以在reducer中直接读取router的信息而不需要从组件中一层层往下传。

createApp.js
@H_301_21@import React from 'react'; import {Provider} from 'react-redux'; import Routers from './router/index'; const createApp=({store,history})=> <Provider store={store}> <Routers history={history} /> </Provider> export default createApp;

前端使用的history为:

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

而后端使用的history为:

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

开发版热加载更新

@H_301_21@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。

前端dom节点生成

@H_301_21@const renderApp=()=>{ let application=createApp({store,history}); hydrate(application,document.getElementById('root')); } window.main = () => { Loadable.preloadReady().then(() => { renderApp() }); };

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

router4动态按需加载

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

@H_301_21@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'),}); 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进去的还是从服务器进入的。

@H_301_21@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

@H_301_21@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字符串。结合按需加载有:

@H_301_21@let store=configureStore(); let history=createHistory({initialEntries:[ctx.req.url]}); let rootString= renderToString( <Loadable.Capture report={moduleName => modules.push(moduleName)}> {createApp({store,history})} </Loadable.Capture> );

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

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

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

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

结语

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

因为自己也是初次弄react服务端渲染,很多地方是参考了大神们的做法弄出来的,还有很多不懂得地方,请大家多多指点,完整的代码在 [https://github.com/wd2010/Rea...]()

猜你在找的React相关文章