以前react作的一個應用,最近把首頁改爲了服務端渲染的形式,過程仍是很周折的,踩到了很多坑,記錄一些重點,但願有所幫助
項目地址,喜歡的給個star,感謝。。。。。。。css
React下同構的解決方案有next.js、react-server等,這裏,由於這個項目以前已經採用create-react-app、redux作完了,只是想在現有系統基礎上把首頁改爲服務端直出的方式,就選擇了webpack-isomorphic-tools這個模塊html
若是咱們想在現有React系統中引入同構,首先要解決的一個重要問題是:代碼中咱們import了圖片,svg,css等非js資源,在客戶端webpack的各類loader幫咱們處理了這些資源,在node環境中單純的依靠babel-regisiter是不行的,執行renderToString(<App />)會報錯,非js資源無法處理前端
而webpack-isomorphic-tools就幫助咱們處理了這些非js資源,在客戶端webpack構建過程當中,webpack-isomorphic-tools做爲一個插件,生成了一份json文件,形如:node
有了這份映射文件,在同構的服務端,renderToString(<App />)執行的過程當中,就能夠正確的處理那些非js資源react
好比咱們有一個組件:webpack
const App =()=>{ return <img src={require('../common/img/1.png')}> } 同構的服務端調用renderToString(<App />),就生成正確的 <img src="static/media/1.3b00ac49.png">標籤
對webpack-isomorphic-tools的具體使用參見githubnginx
額,一一個說git
簡單總結就是github
renderToString(<Provider store={store}> <Router location={req.baseUrl} context={context}> <Routes /> </Router> </Provider>) //獲得填滿數據的標籤
注意,上面說的webpack-isomorphic-tools中生成的json文件中有js,css的對應關係,這裏我訪問那個json文件獲得js、css的路徑,拼到html中web
還要返回store中保存的狀態,供客戶端js createStore使用
<script> window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())} </script>
const sagaMiddleware = createSagaMiddleware() const store = createStore( reducer, window.__INITIAL_STATE__, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga)
在作同構的時候不能用BrowserRouter,要使用無狀態的StaticRouter,並結合location和context兩個屬性
有這樣的路由結構
<div className="main"> <Route exact path="/" render={() => <Redirect to="/home"></Redirect> }></Route> <Route path="/home" component={Home}></Route> <Route path="/detail/:id" component={Detail}></Route> <Route path="/user" component={User}></Route> <Route path="/reptile" component={Reptile}></Route> <Route path="/collect" component={Collect}></Route> </div> //默認跳到/home,其餘的該到哪到哪
server端的代碼要這樣
const context = {} const html = renderToString( <Provider store={store}> <Router location={req.baseUrl} context={context}> <Routes /> </Router> </Provider>) //<Route>中訪問/,重定向到/home路由時 if (context.url) { res.redirect('/home') return }
StaticRouter能夠根據request來的url來指定渲染哪一個組件,context.url指定重定向到的那個路由
也就是說,要是訪問 /,StaticRouter會給咱們重定向到/home,而且StaticRouter自動給context對象加了url,context.url就是重定向的/home,當不是重定向時,context.url是undefined
咱們還能夠本身寫邏輯 經過context來處理30二、404等。但這裏我不須要。。。。。,爲何呢?
我沒作全棧的同構,只服務端渲染了主頁,渲染一個和多個差很少,全都渲染的話就是在服務端要根據當前請求的路由來決定要發那些請求來填充Store
我對路由的處理流程上面的思惟導圖有說明,就是在nginx中多配一個代理。
對於訪問/、/home這兩個路由,代理到ssr服務,來吐首頁內容,api代理到後端服務,其餘的直接返回(也就是說若是在detail頁面或user頁面刷新了頁面仍是以前客戶端渲染那套)
上面說server端初始化數據的時候還有一個登錄問題沒說。
用戶初始訪問了服務端渲染的首頁,而後在客戶端轉到登陸頁面登錄了,從新回到首頁刷新了頁面,喔,又去請求了ssr服務,但服務端不知道當前用戶登陸了啊,仍是原來的流程,返回的__INITIAL_STATE__中仍是沒有用戶的我的信息和已登陸狀態
因此,在客戶端登錄後,要將用戶的token存到cookie中,這樣,在首頁就算用戶刷新了頁面,從新請求頁面請求中也會帶上cookie,在服務端,根據request.cookies中是否有token來決定發哪些請求填充store
if (auth) { //要是有token就去查用戶信息和是否登陸狀態(還查是否登陸是由於token有多是被篡改過的) promises = [ getMoviesList(store, auth), getCategory(store), checkLogin(store, auth), getUinfo(store, auth) ] } else { promises = [ getMoviesList(store), getCategory(store), ] } Promise.all(promises).then(x=>{ renderToString(<Provider store={store}></Provider>) })
到這一步,訪問域名,就可以正確展現服務端渲染的頁面,跳到別的路由,客戶端的js也能正常處理接下來的事,可是,服務端渲染頁面展現後,首頁那幾個ajax請求仍是觸發了,這是不必的。
原覺得這是react renderToString()生成的標籤和客戶端js hydrate()的有差別致使的,然而,實際上,js執行了,組件的生命週期該觸發仍是會觸發的,不僅是 attach event listeners to the existing markup
因此要手動避免
在App組件中 componentDidMount() { if (!window.__INITIAL_STATE__) { this.props.checkLogin() this.props.loadCategory() } } //噹噹前頁面是服務端返回的(由於window.__INITIAL_STATE__有初始狀態),初始的ajax就不觸發了
服務端渲染的坑仍是挺多的,這一個星期就搞它了。。。。這裏記錄一些比較重要的東西,具體細節有興趣的能夠看下代碼.最後,最重要的,喜歡的給個star,感謝。。。。。。。