本項目的最新開發進度,請在github閱讀,也歡迎star,issue,共同進步。
寫在前面的話
自已以前對redux,react,rect-redux,react-router都是有一點的瞭解,並且在真實的項目中也多少有些涉及。但是不足的地方在於沒有做一個demo將他們串起來,所以總是感覺似懂非懂。特別是react服務端渲染這一塊,對於自己完全就是一個黑箱,這對我深入理解react同構等稍微難一點的內容產生了很大的影響。所以我最後寫了這個例子,希望有同樣困擾的同學能夠有所收穫。也歡迎star,issue。
不得不說,當你真實的去做一個項目的時候,哪怕是一個小小的demo,這都會完全顛覆你對React生態的認識。從一開始的不知道如何入手,到遇到各種困難,然後各種google,最後解決問題,你會發現自己是真的在成長。遇到的問題以及解決方案,我在文章列表中也給出了。時間+經歷=成長,對於我來說就夠了。默默的對自己說一句,加油把少年!
1.項目說明
克隆該項目,然後直接運行就可以了。
git clone https://github.com/liangklfangl/react-universal-bucket.git
npm install
npm run dev
//開發模式下運行下面的命令
//npm run pro
打開http://localhost:3222/ 就可以看到效果。項目截圖如下:
2.項目基本知識點
2.1 代理與反代理的基本內容
使用http-proxy來完成。其反向代理的原理如下圖:
通過如下代碼完成,其相當於一個反向代理服務器,向我們的代理服務器,即API服務器發送請求:
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'});
});
2.2 react全家桶常見庫
react-router,react,redux,react-redux,redux-async-connect,redux-thunk等一系列react相關的基本內容。其中最重要的就是我們的redux-async-connect,他可以在跳轉到某個頁面之前或者之後發起某一個ajax請求。用法如下:
@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方法:
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}\/>));
});
對於上面的promise方法的用法不理解的可以參考這裏
2.3 自定義bootstrap
使用bootstrap-loader來加載自定義的bootstrap文件(.bootstraprc),從而減小打包後文件的大小。我們通過在項目目錄下建立.bootstraprc文件,該文件可以指定我們需要使用的bootstrap樣式,是否使用javascript等。如通過下面的配置:
scripts: false
就可以在當前應用中不引入bootstrap的javascript,而只是單獨使用樣式。如果在單獨使用樣式的情況下我們可以結合react-bootstrap,react-router-bootstrap來完成頁面的各種交互。如果你要單獨使用這部分的內容,你可以參考這裏
2.4 webpack的HMR功能集成
使用webpack實現HMR(react-transform-hmr)等基本功能,以及介紹了webpack-dev-middleware,webpack-hot-middleware等的使用。
babelReactTransformPlugin[1].transforms.push({
transform: 'react-transform-hmr',
imports: ['react'],
locals: ['module']
});
如果你想深入瞭解HMR,你也可以參考這裏。
2.5 redux開發工具使用
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工具。
2.6 react服務器端同構
服務端同構是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在客戶端與服務端一致,防止客戶端重新渲染)。
2.7 各種打包工具
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集成了多種打包方案,總有一個適合你
2.8 服務端客戶端其他的庫
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));
}));
2.9 高階組件的組件複用邏輯
高階組件對於組件複用是相當重要的。比如有一種情況,你需要獲取所有的用戶列表,圖書列表,**列表等等,然後在數據獲取完成後來重新渲染組件,此時你也可以考慮高階組件的方式:
//此時我們只是需要考慮真正的異步請求數據的邏輯,以及對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
3.React全家桶文章總結
關於該項目中使用到的所有的react相關知識點我都進行了詳細總結。但是很顯然,如果你要學習react,必須對webpack和babel都進行一定的瞭解。因爲在寫這個項目之前,我只是一個react/webpack/babel的新手,因此也是在不斷的學習中摸索前進的。遇到了問題就各種google,baidu。而且我對於自己有一個嚴格的要求,那就是要知其然而且要知其所以然,因此我會把遇到的問題都進行深入的分析。下面我把我在寫這個項目過程遇到問題,並作出的總結文章貼出來,希望對您有幫助。我也希望您能夠關注每一篇文章下面的參考文獻,因爲他們確實都是非常好的參考資料。
3.1 React+redux相關
3.2 webpack相關
集成webpack,webpack-dev-server的打包工具
webpack中的externals vs libraryTarget vs library
webpack的compiler與compilation對象
3.3 Babel相關
3.4 其他內容
bootstrap-loader自定義bootstrap樣式
4.你能夠學到的東西
內部所有的代碼都有詳細的註釋,而且都給出了代碼相關說明的鏈接。通過這個項目,對於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
........
如果你有不懂的地方,也可以通過github上的郵箱聯繫我。希望我們能夠共同進步。同時,我的這些文章也發表到了CSDN上,您可以點擊這裏查看
5.結語
該項目還有些功能在開發過程中,如果您有什麼需求,歡迎您給我issue。我希望能夠通過這個項目對React初學者,或者Babel/webpack初學者都有一定的幫助。我再此再強調一下,在我寫的這些文章末尾都附加了很多參考文獻,而這些參考文獻的作用對我的幫助真的很大,在此表示感謝!!!!!