123

Egg + React + React Router + Redux 服务端渲染实践

 6 years ago
source link: https://zhuanlan.zhihu.com/p/30526168?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Egg + React + React Router + Redux 服务端渲染实践

阿里巴巴-前端开发

在实现 Egg + React 服务端渲染解决方案 egg-react-webpack-boilerplate 时,因在 React + React Router + Redux 方面没有深入的实践过以及精力问题, 只实现了多页面服务端渲染方案。最近收到社区的一些咨询,想知道 Egg + React Router + Redux 如何实现 SPA 同构实现。如是就开始了 Egg + React Router + Redux 的摸索之路,实践过程中遇到 React-Router 版本问题,Redux 使用问题等问题,折腾了两天,但最终还是把想要的方案实践出来。

在查阅 react router 和 redux 的相关资料,发现 react router 有 V3 和 V4 版本, V4 新版本又分为 react-router,react-router-dom,react-router-config,react-router-redux 插件, redux 相关的有 redux,react-redux,只能硬着头皮一个一个看看啥含义,看一下简单的Todo例子, 相比 Vue 的 vuex + vue-router 的工程搭建过程,这个要复杂的多,只好采用分阶段完成。先完成了纯前端渲染的 React Router + Redux 结合的例子,把 React Router 和 Redux 的相关 API 撸了一遍,基本掌握 React-Redux actions, reducer, store使用(这里自己先通过简单的例子让整个流程跑通,然后逐渐添砖加瓦,实现自己想要的功能. 比如不考虑异步,不考虑数据请求,直接hack数据,跑通后,再逐渐改造完善)。

依赖说明react router(v4)

// 客户端用BrowserRouter, 服务端渲染用 StaticRouter 静态路由组件
import{BrowserRoute,StaticRouter }from'react-router-dom'; 

redux 和 react-redux

这里直接借个图(https://segmentfault.com/a/1190000011473973):

Redux 介绍

Redux 是 javaScript 状态管理容器

通过 Redux 可以很方便进行数据集中管理和实现组件之间的通信,同时视图和数据逻辑分离,对于大型复杂(业务复杂,交互复杂,数据交互频繁等)的 React 项目, Redux 能够让代码结构(数据查询状态、数据改变状态、数据传播状态)层次更合理。另外,Redux 和 React 之间没有关系。Redux 支持 React、Angular、jQuery 甚至纯 JavaScript。

Redux 的设计思想很简单

Redux是在借鉴Flux思想上产生的,基本思想是保证数据的单向流动,同时便于控制、使用、测试

  • Web 应用是一个状态机,视图与状态是一一对应的。
  • 所有的状态,保存在一个对象里面,也就是单一数据源

Redux 核心由三部分组成:Store, Action, Reducer。

  • Store : 贯穿你整个应用的数据都应该存储在这里。
// component/spa/ssr/actions 创建store,初始化store数据
export function create(initalState){
 return createStore(reducers, initalState);
}
  • Action: 必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。除此之外的属性,就是进行这个操作需要的数据。
// component/spa/ssr/actions
export function add(item) {
  return {
    type: ADD,
    item
  }
}

export function del(id) {
  return {
    type: DEL,
    id
  }
}
  • Reducer: 是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的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 使用

// store的创建
var createStore = require('redux').createStore;
var store = createStore(update);

// store 里面的数据发生改变时,触发的回调函数
store.subscribe(function () {
  console.log('the state:', store.getState());
});

// action触发state改变的唯一方法, 改变store里面的方法
store.dispatch(add({id:1, title:'redux'})); 
store.dispatch(del(1));

react-redux

react-redux 对 redux 流程的一种简化,可以简化手动 dispatch 繁琐过程。 react-redux 重要提供以下两个API,详细介绍请见:http://cn.redux.js.org/docs/react-redux/api.html

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

更多信息请参考 http://cn.redux.js.org/

服务端渲染同构实现

页面模板实现

  • 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 {
 // 服务端渲染调用,这里mock数据,实际请改为服务端数据请求
  static fetch() {
    return Promise.resolve({
      list:[{
        id: 0,
        title: `Egg+React 服务端渲染骨架`,
        summary: '基于Egg + React + Webpack3/Webpack2 服务端渲染同构工程骨架项目',
        hits: 550,
        url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate'
      }, {
        id: 1,
        title: '前端工程化解决方案easywebpack',
        summary: 'programming instead of configuration, webpack is so easy',
        hits: 550,
        url: 'https://github.com/hubcarl/easywebpack'
      }, {
        id: 2,
        title: '前端工程化解决方案脚手架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 服务端渲染骨架-${id}`,
      summary: '基于Egg + React + Webpack3/Webpack2 服务端渲染骨架项目',
      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>+收藏</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 路由定义

// 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前端渲染同构实现

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服务端渲染同构实现

在服务端渲染时,这里纠结了一下,遇到两个问题

  • 参考一些资料的写法Node服务端都是在路由里面处理的,写起来好别扭, 希望 render时
  • ReactDOMServer.renderToString(ReactElement) 参数必须是ReactElement
  • 组件异步获取的数据Node render怎么获取到

这里通过函数回调的方式可以解决上面问题,也就是 export 出去的是一个函数,然后 render 判断是否直接renderToString还是调用函数,然后再进行renderToString。目前在 egg-view-react-ssr 做了一层简单判断,代码如下:

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);
    });
}

这样处理了以后,Node 服务端controller处理时就无需自己处理路由匹配问题和store问题,全部交给底层处理。现在的这种处理方式与Vue服务端渲染render思路一致,把服务端逻辑写到模板文件里面,然后由Webpack构建js文件。

SPA服务端渲染入口文件

Webpack 构建的文件 app/ssr.js 到 app/view 目录

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 为服务端初始化数据
export default function(context, options) {
    const url = context.state.url;
	// 根据服务端url地址找到匹配的组件
    const branch = matchRoutes(routes, url);
	// 收集组件数据
    const promises = branch.map(({route}) => {
      const fetch = route.component.fetch;
      return fetch instanceof Function ? fetch() : Promise.resolve(null)
    });
	// 获取组件数据,然后初始化store, 同时返回ReactElement
    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服务端controller调用

  • controller 实现
exports.ssr = function* (ctx) {
  yield ctx.render('spa/ssr.js', { url: ctx.url });
};
 app.get('/spa(/.+)?', app.controller.spa.spa.ssr); 
动图封面

服务端实现与普通模板渲染调用无差异,写起来简单明了。如果你对 Egg + React 技术敢兴趣,赶快来玩一玩 egg-react-webpack-boilerplate 项目吧!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK