以前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,感谢。。。。。。。