在實現 Egg + React 服務端渲染解決方案 egg-react-webpack-boilerplate 時,因在 React + React Router + Redux 方面沒有深刻的實踐過以及精力問題, 只實現了多頁面服務端渲染方案。最近收到社區的一些諮詢,想知道 Egg + React Router + Redux 如何實現 SPA 同構實現。如是就開始了 Egg + React Router + Redux 的摸索之路,實踐過程當中遇到 React-Router 版本問題,Redux 使用問題等問題,折騰了幾天,但最終仍是把想要的方案實踐出來。javascript
在查閱 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數據,跑通後,再逐漸改造完善)。html
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 |
靜態路由配置輔助 |
// 客戶端用BrowserRouter, 服務端渲染用 StaticRouter 靜態路由組件 import { BrowserRouter, StaticRouter } from 'react-router-dom';
這裏直接借個圖(http://www.javashuo.com/article/p-vnrsyvyn-dd.html):前端
經過 Redux 能夠很方便進行數據集中管理和實現組件之間的通訊,同時視圖和數據邏輯分離,對於大型複雜(業務複雜,交互複雜,數據交互頻繁等)的 React 項目, Redux 可以讓代碼結構(數據查詢狀態、數據改變狀態、數據傳播狀態)層次更合理。另外,Redux 和 React 之間沒有關係。Redux 支持 React、Angular、jQuery 甚至純 JavaScript。vue
Redux是在借鑑Flux思想上產生的,基本思想是保證數據的單向流動,同時便於控制、使用、測試java
// component/spa/ssr/actions 建立store,初始化store數據 export function create(initalState){ return createStore(reducers, initalState); }
// component/spa/ssr/actions export function add(item) { return { type: ADD, item } } export function del(id) { return { type: DEL, id } }
// 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 }
// 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 對 redux 流程的一種簡化,能夠簡化手動 dispatch 繁瑣過程。 react-redux 重要提供如下兩個API,詳細介紹請見:http://cn.redux.js.org/docs/react-redux/api.htmlreact
更多信息請參考 http://cn.redux.js.org/webpack
// 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)
// 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>; } }
// 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;
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') );
在服務端渲染時,這裏糾結了一下,遇到兩個問題git
這裏經過函數回調的方式能夠解決上面問題,也就是 export 出去的是一個函數,而後 render 判斷是否直接renderToString仍是調用函數,而後再進行renderToString。目前在 egg-view-react-ssr 作了一層簡單判斷,代碼以下:github
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文件。web
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> ) }); };
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 項目吧!