寫在前面的話javascript
自已之前對redux,React,rect-redux,react-router都是有一點的瞭解,而且在真實的項目中也多少有些涉及。可是不足的地方在於沒有作一個demo將他們串起來,因此老是感受似懂非懂。特別是react服務端渲染這一塊,對於本身徹底就是一個黑箱,這對我深刻理解react同構等稍微難一點的內容產生了很大的影響。因此我最後寫了這個例子,但願有一樣困擾的同窗可以有所收穫。也歡迎star,issue。css
不得不說,當你真實的去作一個項目的時候,哪怕是一個小小的demo,這都會徹底顛覆你對React生態的認識。從一開始的不知道如何入手,到遇到各類困難,而後各類google,最後解決問題,你會發現本身是真的在成長。遇到的問題以及解決方案,我在文章列表中也給出了。時間+經歷=成長,對於我來講就夠了。默默的對本身說一句,加油把少年!html
首先下載該項目,而後直接運行下面的命令。前端
npm install npm run dev //npm run pro //生產模式下執行註釋部分命令
打開http://localhost:3222/ 就能夠看到效果。項目截圖以下:java
使用http-proxy來完成。其反向代理的原理以下圖:node
經過以下代碼完成,其至關於一個反向代理服務器,向咱們的代理服務器,即API服務器發送請求:react
const targetUrl = 'http://' + (process.env.APIHOST||"localhost") + ':' + (process.env.APIPORT||"8888");//其中APIHOST和APIPORT分別表示API服務器運行的域名與端口號const proxy = httpProxy.createProxyServer({ target:targetUrl, ws:true //反代理服務器與服務器之間支持webpack socket}); app.use("/api",(req,res)=>{ proxy.web(req,res,{target:targetUrl}); }); app.use('/ws', (req, res) => { proxy.web(req, res, {target: targetUrl + '/ws'}); });
react-router,react,redux,react-redux,redux-async-connect,redux-thunk等一系列react相關的基本內容。其中最重要的就是咱們的redux-async-connect,他能夠在跳轉到某個頁面以前或者以後發起某一個ajax請求。用法以下:linux
@asyncConnect([{ //其中helpers來自於服務端渲染 promise: ({store: {dispatch, getState},helpers}) => { const promises = []; const state = getState(); //獲得store的當前狀態 if(!isInfoLoaded(state)){ promises.push(dispatch(loadInfo())); } if(!isAuthLoaded(state)){ promises.push(dispatch(loadAuth())); } //若是沒有登陸或者相應的數據沒有加載完成,那麼咱們在此時加載數據 return Promise.all(promises); } }])
其中helpers方法來自於其服務端渲染的loadOnServer方法:webpack
loadOnServer({...renderProps, store, helpers: {client}}).then(() => { const component = ( <Provider store={store} key="provider"> <ReduxAsyncConnect {...renderProps} /> <\/Provider> ) res.status(200); global.navigator = {userAgent: req.headers['user-agent']}; res.send('<!doctype html>\n' + renderToString(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store}\/>)); });
使用bootstrap-loader來加載自定義的bootstrap文件(.bootstraprc),從而減少打包後文件的大小。咱們經過在項目目錄下創建.bootstraprc文件,該文件能夠指定咱們須要使用的bootstrap樣式,是否使用JavaScript等。如經過下面的配置:git
scripts: false
就能夠在當前應用中不引入bootstrap的javascript,而只是單獨使用樣式。若是在單獨使用樣式的狀況下咱們能夠結合react-bootstrap,react-router-bootstrap來完成頁面的各類交互。若是你要單獨使用這部分的內容,你能夠參考這裏
使用webpack實現HMR(react-transform-hmr)等基本功能,以及介紹了webpack-dev-middleware,webpack-hot-middleware等的使用。
babelReactTransformPlugin[1].transforms.push({ transform: 'react-transform-hmr', imports: ['react'], locals: ['module'] });
若是你想深刻了解HMR,你也能夠參考這裏。
redux-devtools,redux-devtools-dock-monitor,redux-devtools-log-monitor等redux開發工具的使用。只須要添加下面的一段代碼就能夠了:
import React from 'react'; import { createDevTools } from 'redux-devtools'; import SliderMonitor from "redux-slider-monitor"; import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; export default createDevTools( <DockMonitor changeMonitorKey='ctrl-m' defaultPosition="right" toggleVisibilityKey="ctrl-H" changePositionKey="ctrl-Q"> <LogMonitor /> <SliderMonitor keyboardEnabled /> <\/DockMonitor>);
固然,若是要添加這部分代碼要作一個判斷:
if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) { const { persistState } = require('redux-devtools'); const DevTools = require('../containers/DevTools/DevTools'); finalCreateStore = compose( applyMiddleware(...middleware), window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument(), //若是有window.devToolsExtension,那麼使用用戶本身的,不然使用咱們配置的 persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) )(_createStore); }
也就是說咱們只會在開發模式下,同時客戶端代碼(服務端顯然是不須要的,該工具只是爲了在客戶端查看當前state的狀態)中,以及DEVTOOLS爲true中才會添加咱們的devTool工具。
服務端同構是react開發中不可避免的問題,由於服務端渲染在必定程度上可以減小首頁白屏的時間,同時對於SEO也具備很重要的做用。React中關於服務端渲染的介紹只是給出一個match方法,而更加深刻的知識卻要本身反覆琢磨。
match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => { if (redirectLocation) { res.redirect(redirectLocation.pathname + redirectLocation.search); //重定向要添加pathname+search } else if (error) { console.error('ROUTER ERROR:', pretty.render(error)); res.status(500); hydrateOnClient(); } else if (renderProps) { loadOnServer({...renderProps, store, helpers: {client}}).then(() => { const component = ( <Provider store={store} key="provider"> <ReduxAsyncConnect {...renderProps} /> <\/Provider> ); res.status(200); global.navigator = {userAgent: req.headers['user-agent']}; res.send('<!doctype html>\n' + renderToString(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store}\/>)); }); } else { res.status(404).send('Not found'); } }); });
針對這部份內容我寫了react服務端渲染中的renderProps與react-data-checksum以及React服務端同構深刻理解與常見問題等系列文章,也歡迎閱讀。文中提到了webpack-isomorphic-tools,該工具使得在服務端也可以處理less/css/scss,image等各類文件,從而使得服務端同構成爲現實(服務端可使用css module等特性生成className,從而使得checksum在客戶端與服務端一致,防止客戶端從新渲染)。
better-npm-run以及webpackcc等打包工具的使用。前者在package.json中直接配置就行:
"betterScripts": { "start-prod": { "command": "node ./bin/server.js", "env": { "NODE_PATH": "./src", "NODE_ENV": "production", "PORT": 8080, "APIPORT": 3030 } } }
其主要做用在於方便設置各類環境變量。而webpackcc集成了多種打包方案,總有一個適合你
superagent,express等與服務器相關的內容。其中前者主要用於向服務端發送請求,包括服務端向反向代理服務器以及客戶端向服務器發送請求。
const methods = ['get', 'post', 'put', 'patch', 'del']; import superagent from 'superagent'; this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => { const request = superagent[method](formatUrl(path)); if (params) { request.query(params); } //若是傳入了參數,那麼經過query添加進去 if (__SERVER__ && req.get('cookie')) { request.set('cookie', req.get('cookie')); } if (data) { request.send(data); } //request.end纔會真正發送請求出去 request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body)); }));
高階組件對於組件複用是至關重要的。好比有一種狀況,你須要獲取全部的用戶列表,圖書列表,**列表等等,而後在數據獲取完成後來從新渲染組件,此時你也能夠考慮高階組件的方式:
//此時咱們只是須要考慮真正的異步請求數據的邏輯,以及對prop進行特別處理的邏輯,而不用管當前是圖書列表,仍是用戶列表等等function connectPromise({promiseLoader, mapResultToProps}) { return Comp=> { return class AsyncComponent extends Component { constructor(props) { super(); this.state = { result: undefined } } componentDidMount() { promiseLoader() .then(result=> this.setState({result})) } render() { return ( <Comp {...mapResultToProps(props)} {...this.props}/> ) } } } } const UserList = connectPromise({ promiseLoader: loadUsers, mapResultToProps: result=> ({list: result.userList}) })(List); //List can be a pure component const BookList = connectPromise({ promiseLoader: loadBooks, mapResultToProps: result=> ({list: result.bookList}) })(List);
你應該很容易就看出來了,對於這種列表類型的高階組件抽象是至關成功的。咱們只須要關注重要的代碼邏輯,在componentDidMount請求數據結束後咱們會自動調用setState來完成組件狀態的更新,而真實的更新的組件倒是咱們經過本身的業務邏輯來指定的,能夠是BookList,UserList,**List等等。這樣具備反作用的高階組件複用也就完成了。若是你須要深刻了解高階組件的內容,請查看個人這篇文章。在該項目中咱們使用了multireducer
關於該項目中使用到的全部的react相關知識點我都進行了詳細總結。可是很顯然,若是你要學習react,必須對webpack和babel都進行必定的瞭解。由於在寫這個項目以前,我只是一個react/webpack/babel的新手,所以也是在不斷的學習中摸索前進的。遇到了問題就各類google,baidu。並且我對於本身有一個嚴格的要求,那就是要知其然並且要知其因此然,所以我會把遇到的問題都進行深刻的分析。下面我把我在寫這個項目過程遇到問題,並做出的總結文章貼出來,但願對您有幫助。我也但願您可以關注每一篇文章下面的參考文獻,由於他們確實都是很是好的參考資料。
集成webpack,webpack-dev-server的打包工具
webpack中的externals vs libraryTarget vs library
webpack的compiler與compilation對象
bootstrap-loader自定義bootstrap樣式
內部全部的代碼都有詳細的註釋,並且都給出了代碼相關說明的連接。經過這個項目,對於react*全家桶*應該會有一個深刻的瞭解。該項目牽涉到了常見的React生態中的庫,所以命名爲全家桶。該項目用到的React生態的主要庫以下:
react react-addons-perf react-bootstrap react-dom react-helmet react-redux react-router react-router-bootstrap react-router-redux react-tap-event-plugin react-transform-hmr redux redux-async-connect redux-devtools redux-devtools-dock-monitor redux-devtools-log-monitor redux-form, redux-slider-monitor redux-thunk multireducer ........