全部源代碼、文檔和圖片都在 github 的倉庫裏,點擊進入倉庫javascript
相關閱讀
1. 服務端異步獲取數據(最難最複雜)
- 剛纔咱們用了客戶端的同步和異步,其實仍是比較簡單的,由於用法與客戶端渲染的用法基本上如出一轍,差異很小
- 可是服務端異步獲取數據修改 store,這個是很是複雜的,咱們要首先明白這其中的原理
1.1 服務端怎麼知道它要獲取數據
- 服務端異步獲取數據的前提是,服務端須要知道它要去獲取什麼數據,就像是去餐館吃飯,你是客人,廚師確定不知道客人要吃什麼,因此必定要客人告訴廚師,我要吃什麼。這裏的客人就比如是客戶端,廚師比如是服務端
- 因此,客人告訴廚師,我要吃蛋炒飯,廚師知道了你要吃蛋炒飯,就開始給你作飯。這裏就是客戶端告訴服務端,客戶端須要什麼數據(客人要吃什麼),而後服務端知道了客戶端須要什麼(客人要吃蛋炒飯),而後服務端去獲取客戶端須要的數據(廚師去作蛋炒飯),最終返給客戶端(把蛋炒飯端給客人)
1.2 客戶端怎麼告訴服務端它須要數據
- 可是如今問題又來了,客人怎麼告訴廚師他要吃什麼,客人是點外賣,仍是打電話,仍是直接去店裏吃呢?客人怎麼告訴廚師這個方法,就比如是客戶端怎麼告訴服務端,客戶端須要什麼數據,因此這裏的關鍵就在於,客戶端經過什麼方式把它須要的東西,告訴服務端
- 經過什麼方式傳遞呢?咱們如今客戶端和服務端所共有的有 redux 和 router,能經過路由傳遞嗎?確定不行,路由主要是作路由跳轉的,路由可以傳遞參數是能夠,可是要讓路由去傳遞大量的數據,而且要對數據進行屢次的修改,這顯然不合適,因此咱們最好的選擇依然仍是 redux
- 好,咱們已經確認了要使用 redux 這個方式來告訴廚師,客人要吃什麼。也就是客戶端使用 redux 告訴服務端,它須要什麼數據,接下來就須要調用 redux 裏的方法
- 咱們能夠給組件添加一個靜態方法,由於組件就是類,類就有本身的靜態方法,咱們定義一個 loadData 的靜態方法,在這個靜態方法裏,經過 redux 告訴服務端,客戶端須要什麼數據,咱們以 Home 組件爲例,Home 組件如今有一個客戶端異步獲取 schoolList 的方法,咱們讓服務端調用這個方法
- Home/index.js
Home.loadData = store => store.dispatch(UserActions.getSchoolList());
- 能夠看到,咱們給 Home 定義了一個 loadData 的靜態方法,把 store 做爲參數傳遞進去,而後 dispatch UserActions 裏的 getSchoolList 方法,由於 loadData 是靜態方法,只有經過類才能調用,因此就是說,咱們能夠本身定義這個方法是在客戶端執行仍是在服務端執行
1.3 把 loadData 方法放在路由上
- 咱們不要去考慮客戶端,如今與客戶端沒有關係,咱們只考慮服務端該怎麼調用 loadData 這個方法
- 路由,咱們在 Home 這個組件的路由上邊,添加一個 loadData 的方法屬性,這也就解釋了爲何咱們上一節要把 routes.js 的形式修改爲數組對象的形式,目的就是爲了服務於調用 loadData 這個方法,因此咱們修改一下路由
- src/routes.js
// src/routes.js
export default [
{
path: '/',
component: Home,
loadData: Home.loadData,
exact: true,
key: '/'
},
{
path: '/news',
component: News,
exact: true,
key: '/news'
}
];
1.4 服務端怎麼調用路由上的 loadData 方法
- src/server/render.js
- 路由上的方法定義好了以後,咱們就要在使用路由的地方作一些其餘的操做,首先咱們看一下以前咱們在服務端是如何是使用路由的,咱們看一下 src/server/render.js 文件
// src/server/render.js
<div className="container" style={{ marginTop: 70 }}>
{
routes.map(route => <Route {...route} />)
}
</div>
- 能夠看到,咱們是直接使用路由,沒有對路由再作其餘的任何操做,可是如今不同了,路由裏有了 loadData 這個方法,並且咱們須要的是在服務端渲染 HTML 模板以前調用 loadData 這個方法,因此咱們要使用這個方法,咱們修改一下 src/server/render.js 裏的代碼
// src/server/render.js
export default (req, res) => {
let context = {};
let store = getServerStore();
let promises = [];
routes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData(store));
}
});
Promise.all(promises).then(() => {
console.log(store.getState());
let domContent = renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.path}>
<>
<Header />
<div className="container" style={{ marginTop: 70 }}>
{
routes.map(route => <Route {...route} />)
}
</div>
</>
</StaticRouter>
</Provider>
);
let html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
<title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script>
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src="/client.js"></script>
</body>
</html>
`;
res.send(html);
});
};
- 從代碼裏,咱們能夠看到,咱們主要定義了一個 promises 數組,而後把 routes 遍歷,若是 routes 裏有 loadData 這個屬性,那麼把這個 loadData push 進 promises 數組,最後統一調用 Promise.all(promises) 執行全部的 loadData,執行完畢以後,redux 裏的異步方法就已經執行完畢,修改了 store,而後開始渲染 HTML 模板
- 咱們要明確一件事,就是 store 裏的 action 的異步操做,咱們採用的是 axios,axios 請求返回的就是一個 promise,因此咱們的 loadData 在調用 action 的方法的時候,必定也是一個 promise,因此咱們能夠把 loadData push 進 promises 數組,最終統一用 Promise.all 執行
- 經過控制檯的輸出,咱們看到,如今服務端已經獲取到數據了,並且也已經修改了 store 裏的值,那麼接下來,咱們就可使用 store 裏的值了
1.5 脫水與注水
- 脫水與注水,很容易讓咱們想到洗衣機的脫水與注水。衣服放進洗衣機的時候,確定要注水。等衣服洗完以後,要進行脫水。
- 在這裏也是同樣,服務端獲取到數據後,怎麼才能把數據返回給客戶端,要知道服務端不能經過 ajax 的方式把數據再次返回給客戶端。
- 咱們看代碼就能夠看到,咱們是先獲取到 store 裏的數據,而後纔開始渲染頁面內容的,既然這樣,我直接把 store 裏的數據,放到 HTML 頁面裏就能夠了,客戶端頁面渲染完以後,直接從 HTML 頁面裏拿數據去使用就能夠了。
- 注水, 咱們在 HTML 模板中,添加一個 script 標籤,把 store 的值做爲 window 對象的一個屬性,掛到頁面上,掛載到頁面上的必定要是一個字符串,由於 HTML 頁面只認識字符串,js 對象識別不了,因此,咱們就實現了服務端的注水
<script>
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
- 脫水,服務端注水完成了,那麼該客戶端脫水了,怎麼脫水呢,換句話說,就是怎麼把服務端掛到 HTML 頁面上的 js 數據拿出來,而後修改頁面視圖,咱們打開
- /src/store/index.js 文件,修改 getClientStore,咱們把 HTML 模板裏的 window.context.state 的值獲取到,而後做爲 createStore 裏的第二個參數,就能夠實現脫水的功能
// /src/store/index.js
export const getClientStore = () => {
let initState = window.context.state;
return createStore(
reducers,
initState,
composeWithDevTools(applyMiddleware(thunk, logger))
);
}
- 這個時候,咱們不須要修改客戶端的任何代碼,直接重啓服務,刷新頁面,咱們能夠看到,咱們不須要點擊按鈕,瀏覽器已經把 schoolList 數據掛到頁面上。咱們查看頁面源代碼,就能夠看到,ul 標籤裏的 li 標籤裏,有各個學校的內容。同時,在代碼底部,有一個 script 的標籤裏,裏邊的內容就是咱們在服務端的 HTML 模板裏注水的內容
- 這個時候,咱們就實現了服務端異步獲取數據
- 總結一下就是,總體的代碼量很少,最核心的是思路,客戶端給本身定義一個 loadData 的靜態方法,loadData 經過調用 redux 裏的 actions 告訴服務端要獲取什麼數據,而後在路由裏添加 loadData 這個屬性,服務端調用 loadData 這個方法,本質上調用的是 redux 裏的 actions 裏的方法,由於可能有多個組件都有 loadData 方法,因此咱們遍歷出有 loadData 屬性的路由,把 loadData 屬性統一放入 Promise.all 裏進行處理,處理完畢以後, store 裏的數據已經修改,咱們經過服務端的注水,客戶端的脫水,就能夠把服務端異步獲取到的數據顯示在頁面上
- 頁面效果,學校列表是經過服務端渲染獲取到的,再也不是客戶端獲取數據

- 頁面源碼,主要是看紅框裏的, HTML 裏已經有了各個的 school 信息,同時,服務端注水的內容,也做爲字符串顯示在頁面上

- 因爲這一節的內容比較複雜,也比較難,因此咱們就介紹這麼多,下一節咱們主要介紹一下優化當前的代碼
相關閱讀