微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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,所以在一个文件里暴露他们三。

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的信息而不需要从组件中一层层往下传。

createApp.js
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为:

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

而后端使用的history为:

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。

前端dom节点生成

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实现按需加载。

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进去的还是从服务器进入的。

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(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      {createApp({store,history})}
    </Loadable.Capture>
  );

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

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

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

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

结语

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...