該項目還有些功能在開發(fā)過程中,如果您有什么需求,歡迎您與我聯(lián)系。我希望能夠通過這個項目對React初學(xué)者,或者Babel/webpack初學(xué)者都有一定的幫助。我在此再強(qiáng)調(diào)一下,在我寫的這些文章末尾都附加了很多參考文獻(xiàn),而這些參考文獻(xiàn)的作用對我的幫助真的很大,在此表示感謝!!!!!
寫在前面的話
自已以前對redux,React,rect-redux,react-router都是有一點(diǎn)的了解,并且在真實(shí)的項目中也多少有些涉及。但是不足的地方在于沒有做一個demo將他們串起來,所以總是感覺似懂非懂。特別是react服務(wù)端渲染這一塊,對于自己完全就是一個黑箱,這對我深入理解react同構(gòu)等稍微難一點(diǎn)的內(nèi)容產(chǎn)生了很大的影響。所以我最后寫了這個例子,希望有同樣困擾的同學(xué)能夠有所收獲。也歡迎star,issue。
不得不說,當(dāng)你真實(shí)的去做一個項目的時候,哪怕是一個小小的demo,這都會完全顛覆你對React生態(tài)的認(rèn)識。從一開始的不知道如何入手,到遇到各種困難,然后各種google,最后解決問題,你會發(fā)現(xiàn)自己是真的在成長。遇到的問題以及解決方案,我在文章列表中也給出了。時間+經(jīng)歷=成長,對于我來說就夠了。默默的對自己說一句,加油把少年!
首先下載該項目,然后直接運(yùn)行下面的命令。
1 2 3 4 | npm install npm run dev //npm run pro //生產(chǎn)模式下執(zhí)行注釋部分命令 |
打開http://localhost:3222/ 就可以看到效果。項目截圖如下:
使用http-proxy來完成。其反向代理的原理如下圖:
通過如下代碼完成,其相當(dāng)于一個反向代理服務(wù)器,向我們的代理服務(wù)器,即API服務(wù)器發(fā)送請求:
1 2 3 4 5 6 7 8 9 10 | const targetUrl = 'http://' + (process.env.APIHOST|| "localhost" ) + ':' + (process.env.APIPORT|| "8888" );//其中APIHOST和APIPORT分別表示API服務(wù)器運(yùn)行的域名與端口號const proxy = httpProxy.createProxyServer({ target:targetUrl, ws: true //反代理服務(wù)器與服務(wù)器之間支持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相關(guān)的基本內(nèi)容。其中最重要的就是我們的redux-async-connect,他可以在跳轉(zhuǎn)到某個頁面之前或者之后發(fā)起某一個ajax請求。用法如下:
1 2 3 4 5 6 7 8 9 10 | @asyncConnect([{ //其中helpers來自于服務(wù)端渲染 promise: ({store: {dispatch, getState},helpers}) => { const promises = []; const state = getState(); //得到store的當(dāng)前狀態(tài) if (!isInfoLoaded(state)){ promises.push(dispatch(loadInfo())); } if (!isAuthLoaded(state)){ promises.push(dispatch(loadAuth())); } //如果沒有登錄或者相應(yīng)的數(shù)據(jù)沒有加載完成,那么我們在此時加載數(shù)據(jù) return Promise.all(promises); } }]) |
其中helpers方法來自于其服務(wù)端渲染的loadOnServer方法:
1 2 3 4 5 6 7 8 9 | 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等。如通過下面的配置:
1 | scripts: false |
就可以在當(dāng)前應(yīng)用中不引入bootstrap的javascript,而只是單獨(dú)使用樣式。如果在單獨(dú)使用樣式的情況下我們可以結(jié)合react-bootstrap,react-router-bootstrap來完成頁面的各種交互。如果你要單獨(dú)使用這部分的內(nèi)容,你可以參考這里
使用webpack實(shí)現(xiàn)HMR(react-transform-hmr)等基本功能,以及介紹了webpack-dev-middleware,webpack-hot-middleware等的使用。
1 2 3 4 5 | babelReactTransformPlugin[1].transforms.push({ transform: 'react-transform-hmr' , imports: [ 'react' ], locals: [ 'module' ] }); |
如果你想深入了解HMR,你也可以參考這里。
redux-devtools,redux-devtools-dock-monitor,redux-devtools-log-monitor等redux開發(fā)工具的使用。只需要添加下面的一段代碼就可以了:
1 2 3 4 5 6 7 8 9 10 | 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>); |
當(dāng)然,如果要添加這部分代碼要做一個判斷:
1 2 3 4 5 6 7 | 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); } |
也就是說我們只會在開發(fā)模式下,同時客戶端代碼(服務(wù)端顯然是不需要的,該工具只是為了在客戶端查看當(dāng)前state的狀態(tài))中,以及DEVTOOLS為true中才會添加我們的devTool工具。
服務(wù)端同構(gòu)是react開發(fā)中不可避免的問題,因為服務(wù)端渲染在一定程度上能夠減少首頁白屏的時間,同時對于SEO也具有很重要的作用。React中關(guān)于服務(wù)端渲染的介紹只是給出一個match方法,而更加深入的知識卻要自己反復(fù)琢磨。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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' ); } }); }); |
針對這部分內(nèi)容我寫了react服務(wù)端渲染中的renderProps與react-data-checksum以及React服務(wù)端同構(gòu)深入理解與常見問題等系列文章,也歡迎閱讀。文中提到了webpack-isomorphic-tools,該工具使得在服務(wù)端也能夠處理less/css/scss,image等各種文件,從而使得服務(wù)端同構(gòu)成為現(xiàn)實(shí)(服務(wù)端可以使用css module等特性生成className,從而使得checksum在客戶端與服務(wù)端一致,防止客戶端重新渲染)。
better-npm-run以及webpackcc等打包工具的使用。前者在package.json中直接配置就行:
1 2 3 4 5 6 7 8 9 10 11 | "betterScripts" : { "start-prod" : { "command" : "node ./bin/server.js" , "env" : { "NODE_PATH" : "./src" , "NODE_ENV" : "production" , "PORT" : 8080, "APIPORT" : 3030 } } } |
其主要作用在于方便設(shè)置各種環(huán)境變量。而webpackcc集成了多種打包方案,總有一個適合你
superagent,express等與服務(wù)器相關(guān)的內(nèi)容。其中前者主要用于向服務(wù)端發(fā)送請求,包括服務(wù)端向反向代理服務(wù)器以及客戶端向服務(wù)器發(fā)送請求。
1 2 3 4 5 6 7 8 9 10 11 | 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); } //如果傳入了參數(shù),那么通過query添加進(jìn)去 if (__SERVER__ && req.get( 'cookie' )) { request.set( 'cookie' , req.get( 'cookie' )); } if (data) { request.send(data); } //request.end才會真正發(fā)送請求出去 request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body)); })); |
高階組件對于組件復(fù)用是相當(dāng)重要的。比如有一種情況,你需要獲取所有的用戶列表,圖書列表,**列表等等,然后在數(shù)據(jù)獲取完成后來重新渲染組件,此時你也可以考慮高階組件的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //此時我們只是需要考慮真正的異步請求數(shù)據(jù)的邏輯,以及對prop進(jìn)行特別處理的邏輯,而不用管當(dāng)前是圖書列表,還是用戶列表等等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); |
你應(yīng)該很容易就看出來了,對于這種列表類型的高階組件抽象是相當(dāng)成功的。我們只需要關(guān)注重要的代碼邏輯,在componentDidMount請求數(shù)據(jù)結(jié)束后我們會自動調(diào)用setState來完成組件狀態(tài)的更新,而真實(shí)的更新的組件卻是我們通過自己的業(yè)務(wù)邏輯來指定的,可以是BookList,UserList,**List等等。這樣具有副作用的高階組件復(fù)用也就完成了。如果你需要深入了解高階組件的內(nèi)容,請查看我的這篇文章。在該項目中我們使用了multireducer
關(guān)于該項目中使用到的所有的react相關(guān)知識點(diǎn)我都進(jìn)行了詳細(xì)總結(jié)。但是很顯然,如果你要學(xué)習(xí)react,必須對webpack和babel都進(jìn)行一定的了解。因為在寫這個項目之前,我只是一個react/webpack/babel的新手,因此也是在不斷的學(xué)習(xí)中摸索前進(jìn)的。遇到了問題就各種google,baidu。而且我對于自己有一個嚴(yán)格的要求,那就是要知其然而且要知其所以然,因此我會把遇到的問題都進(jìn)行深入的分析。下面我把我在寫這個項目過程遇到問題,并作出的總結(jié)文章貼出來,希望對您有幫助。我也希望您能夠關(guān)注每一篇文章下面的參考文獻(xiàn),因為他們確實(shí)都是非常好的參考資料。
React的material-ui學(xué)習(xí)實(shí)例
集成webpack,webpack-dev-server的打包工具
webpack實(shí)現(xiàn)code splitting方式分析
webpack中的externals vs libraryTarget vs library
webpack的compiler與compilation對象
bootstrap-loader自定義bootstrap樣式
linux中軟鏈接與硬鏈接的區(qū)別學(xué)習(xí)
npm,webpack學(xué)習(xí)中遇到的各種問題
內(nèi)部所有的代碼都有詳細(xì)的注釋,而且都給出了代碼相關(guān)說明的鏈接。通過這個項目,對于react*全家桶*應(yīng)該會有一個深入的了解。該項目牽涉到了常見的React生態(tài)中的庫,因此命名為全家桶。該項目用到的React生態(tài)的主要庫如下:
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 ........
更多建議: