Egg + React + React Router + Redux 服務(wù)端渲染實(shí)踐

概述

在實(shí)現(xiàn) Egg + React 服務(wù)端渲染解決方案 egg-react-webpack-boilerplate 時(shí)攘已,因在 React + React Router + Redux 方面沒有深入的實(shí)踐過以及精力問題, 只實(shí)現(xiàn)了多頁面服務(wù)端渲染方案。最近收到社區(qū)的一些咨詢渠退,想知道 Egg + React Router + Redux 如何實(shí)現(xiàn) SPA 同構(gòu)實(shí)現(xiàn)。如是就開始了 Egg + React Router + Redux 的摸索之路乡范,實(shí)踐過程中遇到 React-Router 版本問題裁眯,Redux 使用問題等問題,折騰了兩天蠢终,但最終還是把想要的方案實(shí)踐出來序攘。

摸索階段

在查閱 react router 和 redux 的相關(guān)資料,發(fā)現(xiàn) react router 有 V3 和 V4 版本寻拂, V4 新版本又分為 react-router程奠,react-router-dom,react-router-config祭钉,react-router-redux 插件瞄沙, redux 相關(guān)的有 redux,react-redux慌核,只能硬著頭皮一個(gè)一個(gè)看看啥含義距境,看一下簡單的Todo例子, 相比 Vue 的 vuex + vue-router 的工程搭建過程垮卓,這個(gè)要復(fù)雜的多垫桂,只好采用分階段完成。先完成了純前端渲染的 React Router + Redux 結(jié)合的例子粟按,把 React Router 和 Redux 的相關(guān) API 擼了一遍诬滩,基本掌握 React-Redux actions, reducer, store使用(這里自己先通過簡單的例子讓整個(gè)流程跑通霹粥,然后逐漸添磚加瓦,實(shí)現(xiàn)自己想要的功能. 比如不考慮異步疼鸟,不考慮數(shù)據(jù)請(qǐng)求蒙挑,直接hack數(shù)據(jù),跑通后愚臀,再逐漸改造完善)忆蚀。

依賴說明

react router(v4)

react-router React Router 核心
react-router-dom 用于 DOM 綁定的 React Router
react-router-native 用于 React Native 的 React Router
react-router-redux React Router 和 Redux 的集成
react-router-config 靜態(tài)路由配置輔助
// 客戶端用BrowserRouter, 服務(wù)端渲染用 StaticRouter 靜態(tài)路由組件
import { BrowserRouter, StaticRouter } from 'react-router-dom';

redux 和 react-redux

這里直接借個(gè)圖([


react-redux.png

973)):

Redux 介紹

Redux 是 javaScript 狀態(tài)管理容器

通過 Redux 可以很方便進(jìn)行數(shù)據(jù)集中管理和實(shí)現(xiàn)組件之間的通信姑裂,同時(shí)視圖和數(shù)據(jù)邏輯分離馋袜,對(duì)于大型復(fù)雜(業(yè)務(wù)復(fù)雜,交互復(fù)雜舶斧,數(shù)據(jù)交互頻繁等)的 React 項(xiàng)目欣鳖, Redux 能夠讓代碼結(jié)構(gòu)(數(shù)據(jù)查詢狀態(tài)、數(shù)據(jù)改變狀態(tài)茴厉、數(shù)據(jù)傳播狀態(tài))層次更合理泽台。另外,Redux 和 React 之間沒有關(guān)系矾缓。Redux 支持 React怀酷、Angular、jQuery 甚至純 JavaScript嗜闻。

Redux 的設(shè)計(jì)思想很簡單

Redux是在借鑒Flux思想上產(chǎn)生的蜕依,基本思想是保證數(shù)據(jù)的單向流動(dòng),同時(shí)便于控制琉雳、使用样眠、測試

  • Web 應(yīng)用是一個(gè)狀態(tài)機(jī),視圖與狀態(tài)是一一對(duì)應(yīng)的翠肘。
  • 所有的狀態(tài)檐束,保存在一個(gè)對(duì)象里面,也就是單一數(shù)據(jù)源
Redux 核心由三部分組成:Store, Action, Reducer。
  • Store : 貫穿你整個(gè)應(yīng)用的數(shù)據(jù)都應(yīng)該存儲(chǔ)在這里束倍。
// component/spa/ssr/actions 創(chuàng)建store被丧,初始化store數(shù)據(jù)
export function create(initalState){
 return createStore(reducers, initalState);
}
  • Action: 必須包含type這個(gè)屬性,reducer將根據(jù)這個(gè)屬性值來對(duì)store進(jìn)行相應(yīng)的處理肌幽。除此之外的屬性晚碾,就是進(jìn)行這個(gè)操作需要的數(shù)據(jù)。
// component/spa/ssr/actions
export function add(item) {
  return {
    type: ADD,
    item
  }
}

export function del(id) {
  return {
    type: DEL,
    id
  }
}
  • Reducer: 是個(gè)函數(shù)喂急。接受兩個(gè)參數(shù):要修改的數(shù)據(jù)(state) 和 action對(duì)象格嘁。根據(jù)action.type來決定采用的操作,對(duì)state進(jìn)行修改廊移,最后返回新的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的創(chuàng)建
var createStore = require('redux').createStore;
var store = createStore(update);

// store 里面的數(shù)據(jù)發(fā)生改變時(shí)探入,觸發(fā)的回調(diào)函數(shù)
store.subscribe(function () {
  console.log('the state:', store.getState());
});

// action觸發(fā)state改變的唯一方法, 改變store里面的方法
store.dispatch(add({id:1, title:'redux'})); 
store.dispatch(del(1));

react-redux

react-redux 對(duì) redux 流程的一種簡化,可以簡化手動(dòng) dispatch 繁瑣過程懂诗。 react-redux 重要提供以下兩個(gè)API蜂嗽,詳細(xì)介紹請(qǐng)見:http://cn.redux.js.org/docs/react-redux/api.html

  • connect(mapStateToProps, mapDispatchToProps, mergeToProps)(App)
  • provider
redux.png

更多信息請(qǐng)參考 http://cn.redux.js.org/

服務(wù)端渲染同構(gòu)實(shí)現(xiàn)

頁面模板實(shí)現(xiàn)

  • 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 {
 // 服務(wù)端渲染調(diào)用,這里mock數(shù)據(jù)殃恒,實(shí)際請(qǐng)改為服務(wù)端數(shù)據(jù)請(qǐng)求
  static fetch() {
    return Promise.resolve({
      list:[{
        id: 0,
        title: `Egg+React 服務(wù)端渲染骨架`,
        summary: '基于Egg + React + Webpack3/Webpack2 服務(wù)端渲染同構(gòu)工程骨架項(xiàng)目',
        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 服務(wù)端渲染骨架-${id}`,
      summary: '基于Egg + React + Webpack3/Webpack2 服務(wù)端渲染骨架項(xiàng)目',
      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前端渲染同構(gòu)實(shí)現(xiàn)

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服務(wù)端渲染同構(gòu)實(shí)現(xiàn)

在服務(wù)端渲染時(shí)植旧,這里糾結(jié)了一下,遇到兩個(gè)問題

  • 參考一些資料的寫法Node服務(wù)端都是在路由里面處理的离唐,寫起來好別扭, 希望 render時(shí)
  • ReactDOMServer.renderToString(ReactElement) 參數(shù)必須是ReactElement
  • 組件異步獲取的數(shù)據(jù)Node render怎么獲取到

這里通過函數(shù)回調(diào)的方式可以解決上面問題病附,也就是 export 出去的是一個(gè)函數(shù),然后 render 判斷是否直接renderToString還是調(diào)用函數(shù)亥鬓,然后再進(jìn)行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 服務(wù)端controller處理時(shí)就無需自己處理路由匹配問題和store問題覆积,全部交給底層處理。現(xiàn)在的這種處理方式與Vue服務(wù)端渲染render思路一致熟呛,把服務(wù)端邏輯寫到模板文件里面宽档,然后由Webpack構(gòu)建js文件。

SPA服務(wù)端渲染入口文件

Webpack 構(gòu)建的文件 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 為服務(wù)端初始化數(shù)據(jù)
export default function(context, options) {
    const url = context.state.url;
    // 根據(jù)服務(wù)端url地址找到匹配的組件
    const branch = matchRoutes(routes, url);
    // 收集組件數(shù)據(jù)
    const promises = branch.map(({route}) => {
      const fetch = route.component.fetch;
      return fetch instanceof Function ? fetch() : Promise.resolve(null)
    });
    // 獲取組件數(shù)據(jù)惰拱,然后初始化store雌贱, 同時(shí)返回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服務(wù)端controller調(diào)用

  • controller 實(shí)現(xiàn)
exports.ssr = function* (ctx) {
  yield ctx.render('spa/ssr.js', { url: ctx.url });
};
  • 路由配置
 app.get('/spa(/.+)?', app.controller.spa.spa.ssr);
  • 效果
egg-react-demo.gif

服務(wù)端實(shí)現(xiàn)與普通模板渲染調(diào)用無差異,寫起來簡單明了偿短。如果你對(duì) Egg + React 技術(shù)敢興趣,趕快來玩一玩 egg-react-webpack-boilerplate 項(xiàng)目吧馋没!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末昔逗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子篷朵,更是在濱河造成了極大的恐慌勾怒,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件声旺,死亡現(xiàn)場離奇詭異笔链,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)腮猖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門鉴扫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澈缺,你說我怎么就攤上這事坪创】簧簦” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵莱预,是天一觀的道長柠掂。 經(jīng)常有香客問我,道長依沮,這世上最難降的妖魔是什么涯贞? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮危喉,結(jié)果婚禮上肩狂,老公的妹妹穿的比我還像新娘。我一直安慰自己姥饰,他們只是感情好傻谁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著列粪,像睡著了一般审磁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岂座,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天态蒂,我揣著相機(jī)與錄音,去河邊找鬼费什。 笑死钾恢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鸳址。 我是一名探鬼主播瘩蚪,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼稿黍!你這毒婦竟也來了疹瘦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤巡球,失蹤者是張志新(化名)和其女友劉穎言沐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酣栈,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡险胰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了矿筝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片起便。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缨睡,到底是詐尸還是另有隱情鸟悴,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布奖年,位于F島的核電站细诸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陋守。R本人自食惡果不足惜震贵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望水评。 院中可真熱鬧猩系,春花似錦、人聲如沸中燥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疗涉。三九已至拿霉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間咱扣,已是汗流浹背绽淘。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闹伪,地道東北人沪铭。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像偏瓤,于是被迫代替她去往敵國和親杀怠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容