Rendering Practice of Egg+React+React Router+Redux Server

Keywords: React github Webpack Attribute

Summary

Implementation of Egg + React Server Rendering Solution egg-react-webpack-boilerplate Because of the lack of in-depth practice and energy problems in React + React Router + Redux, only a multi-page server rendering scheme was implemented. Recently, I received some advice from the community about how Egg + React Router + Redux implements SPA isomorphism. If this is the beginning of the Egg + React Router + Redux road of exploration, the process of practice encountered React-Router version problems, Redux use problems, tossed about for two days, but ultimately the desired solution is put into practice.

Fumbling stage

After consulting the relevant information of react router and redux, we found that there are V3 and V4 versions of react router. The new version of V4 is divided into react-router, react-router-dom, react-router-config, react-router-redux plug-in. Reux is related to redux, react-redux. We can only look at the meaning one by one. Look at a simple Todo example, compared with Vuex + Redux of Vue. Router's construction process, which is much more complicated, has to be completed in stages. First, we finished the example of React Router + Redux combination of pure front-end rendering. We have built up the API of React Router and Redux. We have mastered the React-Redux actions, reducer and store (here we first run the whole process through simple examples, and then gradually add bricks and tiles to realize the functions we want. For example, we do not consider asynchrony, do not consider data requests, and directly use the API of React Router and Redux. Connect with hack data, run through, and then gradually improve.

Dependency statement

react router(v4)

react-router React Router Core
react-router-dom React Router for DOM Binding
react-router-native React Router for React Native
react-router-redux Integration of React Router and Redux
react-router-config Static Routing Configuration Assistance
// The client uses Browser Router and the server renders the static routing component with StaticRouter
import { BrowserRouter, StaticRouter } from 'react-router-dom';

Redux and react-redux

Here, borrow a graph directly ([1]

973)):

Redux introduction

Redux is a javaScript state management container

Redux can facilitate data centralized management and communication between components, while the view and data logic are separated. Redux can make the code structure (data query status, data change status, data dissemination status) more reasonable for large and complex Reduct projects (business complexity, interaction complexity, frequent data interaction, etc.). In addition, there is no relationship between Redux and React. Redux supports React, Angular, jQuery and even pure JavaScript.

Redux's design idea is simple.

Redux is based on the idea of Flux. The basic idea is to ensure the one-way flow of data, and to facilitate control, use and testing.

  • Web application is a state machine, and views and states correspond one to one.
  • All states are stored in one object, that is, a single data source.
The Redux core consists of three parts: Store, Action and Reducer.
  • Store: The data that runs through your entire application should be stored here.
// component/spa/ssr/actions creates the store and initializes the store data
export function create(initalState){
 return createStore(reducers, initalState);
}
  • Action: The type attribute must be included, and the reducer will process the store accordingly based on the value of the attribute. In addition, the attribute is the data needed for this operation.
// component/spa/ssr/actions
export function add(item) {
  return {
    type: ADD,
    item
  }
}

export function del(id) {
  return {
    type: DEL,
    id
  }
}
  • Reducer: It's a function. Accept two parameters: the data state to be modified and the action object. Decide on the action according to action.type, modify the state, and finally return to the new state.
// component/spa/ssr/reducers
export default function update(state, action) {
  const newState = Object.assign({}, state);
  if (action.type === ADD) {
    const list = Array.isArray(action.item) ? action.item : [action.item];
    newState.list = [...newState.list, ...list];
  }
  else if (action.type === DEL) {
    newState.list = newState.list.filter(item => {
      return item.id !== action.id;
    });
  } else if (action.type === LIST) {
    newState.list = action.list;
  }
  return newState
}
redux use
// Creation of store
var createStore = require('redux').createStore;
var store = createStore(update);

// The callback function triggered when the data in the store changes
store.subscribe(function () {
  console.log('the state:', store.getState());
});

// The only way action triggers a state change is to change the method in the store
store.dispatch(add({id:1, title:'redux'})); 
store.dispatch(del(1));

react-redux

react-redux simplifies the Redux process and simplifies the tedious manual dispatch process. react-redux provides the following two API s for detailed introduction. http://cn.redux.js.org/docs/react-redux/api.html

  • connect(mapStateToProps, mapDispatchToProps, mergeToProps)(App)
  • provider

For more information, please refer to http://cn.redux.js.org/

Server Rendering Isomorphism Implementation

Page Template Implementation

  • home.jsx
// component/spa/ssr/components/home.jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { add, del } from 'component/spa/ssr/actions';

class Home extends Component {
 // Server-side rendering calls, where mock data, actually change to server-side data requests
  static fetch() {
    return Promise.resolve({
      list:[{
        id: 0,
        title: `Egg+React Server rendering skeleton`,
        summary: 'Be based on Egg + React + Webpack3/Webpack2 Server Rendering Isomorphic Engineering Skeleton Project',
        hits: 550,
        url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate'
      }, {
        id: 1,
        title: 'Front End Engineering Solution easywebpack',
        summary: 'programming instead of configuration, webpack is so easy',
        hits: 550,
        url: 'https://github.com/hubcarl/easywebpack'
      }, {
        id: 2,
        title: 'Scaffolding for Front End Engineering Solutions easywebpack-cli',
        summary: 'easywebpack command tool, support init Vue/Reac/Weex boilerplate',
        hits: 278,
        url: 'https://github.com/hubcarl/easywebpack-cli'
      }]
    }).then(data => {
      return data;
    })
  }

  render() {
    const { add, del, list } = this.props;
    const id = list.length + 1;
    const item = {
      id,
      title: `Egg+React Server rendering skeleton-${id}`,
      summary: 'Be based on Egg + React + Webpack3/Webpack2 Server-side rendering skeleton project',
      hits: 550 + id,
      url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate'
    };
    return <div className="redux-nav-item">
      <h3>SPA Server Side</h3>
      <div className="container">
        <div className="row row-offcanvas row-offcanvas-right">
          <div className="col-xs-12 col-sm-9">
            <ul className="smart-artiles" id="articleList">
              {list.map(function(item) {
                return <li key={item.id}>
                  <div className="point">+{item.hits}</div>
                  <div className="card">
                    <h2><a href={item.url} target="_blank">{item.title}</a></h2>
                    <div>
                      <ul className="actions">
                        <li>
                          <time className="timeago">{item.moduleName}</time>
                        </li>
                        <li className="tauthor">
                          <a href="#" target="_blank" className="get">Sky</a>
                        </li>
                        <li><a>+Collection</a></li>
                        <li>
                          <span className="timeago">{item.summary}</span>
                        </li>
                        <li>
                          <span className="redux-btn-del" onClick={() => del(item.id)}>Delete</span>
                        </li>
                      </ul>
                    </div>
                  </div>
                </li>;
              })}
            </ul>
          </div>
        </div>
      </div>
      <div className="redux-btn-add" onClick={() => add(item)}>Add</div>
    </div>;
  }
}

function mapStateToProps(state) {
  return {
    list: state.list
  }
}

export default connect(mapStateToProps, { add, del })(Home)
  • about.jsx
// component/spa/ssr/components/about.jsx
import React, { Component } from 'react'
export default class About extends Component {
  render() {
    return <h3 className="spa-title">React+Redux+React Router SPA Server Side Render Example</h3>;
  }
}

react-router routing definition

// component/spa/ssr/ssr
import { connect } from 'react-redux'
import { BrowserRouter, Route, Link, Switch } from 'react-router-dom'
import Home from 'component/spa/ssr/components/home';
import About from 'component/spa/ssr/components/about';

import { Menu, Icon } from 'antd';

const tabKey = { '/spa/ssr': 'home', '/spa/ssr/about': 'about' };
class App extends Component {
  constructor(props) {
    super(props);
    const { url } = props;
    this.state = { current: tabKey[url] };
  }

  handleClick(e) {
    console.log('click ', e, this.state);
    this.setState({
      current: e.key,
    });
  };

  render() {
    return <div>
      <Menu onClick={this.handleClick.bind(this)} selectedKeys={[this.state.current]} mode="horizontal">
        <Menu.Item key="home">
          <Link to="/spa/ssr">SPA-Redux-Server-Side-Render</Link>
        </Menu.Item>
        <Menu.Item key="about">
          <Link to="/spa/ssr/about">About</Link>
        </Menu.Item>
      </Menu>
      <Switch>
        <Route path="/spa/ssr/about" component={About}/>
        <Route path="/spa/ssr" component={Home}/>
      </Switch>
    </div>;
  }
}

export default App;

SPA Front-end Rendering Isomorphism Implementation

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import {match, RouterContext} from 'react-router'
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import { matchRoutes, renderRoutes } from 'react-router-config'
import Header from 'component/layout/standard/header/header';
import SSR from 'component/spa/ssr/ssr';
import { create } from 'component/spa/ssr/store';
import routes from 'component/spa/ssr/routes'
const store = create(window.__INITIAL_STATE__);
const url = store.getState().url;
ReactDOM.render(
    <div>
      <Header></Header>
      <Provider store={ store }>
        <BrowserRouter>
          <SSR url={ url }/>
        </BrowserRouter>
      </Provider>
    </div>,
    document.getElementById('app')
);

SPA Server Rendering Isomorphism Implementation

When rendering on the server side, there are two problems.

  • Reference to some information written Node server is handled in the routing, write awkward, I hope render time.
  • ReactDOMServer.renderToString(ReactElement) parameter must be ReactElement
  • How to get the data Node render from component asynchronously

Here we can solve the above problem by function callback, that is, export goes out of a function, then render decides whether to renderToString directly or call the function, and then renderToString. Currently in egg-view-react-ssr A simple judgement is made. The code is as follows:

app.react.renderElement = (reactElement, locals, options) => {
    if (reactElement.prototype && reactElement.prototype.isReactComponent) {
      return Promise.resolve(app.react.renderToString(reactElement, locals));
    }
    const context = { state: locals };
    return reactElement(context, options).then(element => {
      return app.react.renderToString(element, context.state);
    });
  }

After that, the Node server controller does not need to deal with the routing matching problem and store problem by itself, and all of them are handled by the bottom layer. The current approach is consistent with the idea of render rendering on Vue server, which writes server logic into template files, and then builds js files from Webpack.

SPA Server Renders Entry Files

The file app/ssr.js built by Webpack goes to the app/view directory

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import {match, RouterContext} from 'react-router'
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import { matchRoutes, renderRoutes } from 'react-router-config'
import Header from 'component/layout/standard/header/header';
import SSR from 'component/spa/ssr/ssr';
import { create } from 'component/spa/ssr/store';
import routes from 'component/spa/ssr/routes'
// context Initialize data for the server
export default function(context, options) {
    const url = context.state.url;
    // According to the server url Address Finding Matched Components
    const branch = matchRoutes(routes, url);
    // Collecting component data
    const promises = branch.map(({route}) => {
      const fetch = route.component.fetch;
      return fetch instanceof Function ? fetch() : Promise.resolve(null)
    });
    // Get component data, then initializestore, Return at the same timeReactElement
    return Promise.all(promises).then(data => {
      const initState = {};
      data.forEach(item => {
        Object.assign(initState, item);
      });
      context.state = Object.assign({}, context.state, initState);
      const store = create(initState);
      return () =>(
        <div>
          <Header></Header>
          <Provider store={store}>
            <StaticRouter location={url} context={{}}>
              <SSR url={url}/>
            </StaticRouter>
          </Provider>
        </div>
      )
    });
};

Node Server controller Call

  • controller implementation
exports.ssr = function* (ctx) {
  yield ctx.render('spa/ssr.js', { url: ctx.url });
};
  • Routing configuration
 app.get('/spa(/.+)?', app.controller.spa.spa.ssr);
  • Effect

There is no difference between server-side implementation and common template rendering call, which is simple and clear to write. If you're interested in Egg + React technology, come and play quickly. egg-react-webpack-boilerplate Project!

Posted by JacobYaYa on Thu, 10 Jan 2019 15:51:12 -0800