不能写死defaultValude,所以只能使用props的数据方案。在执行renderToString之前,提前准备好整个应用状态的所有数据。全局的数据管理方案可考虑redux和mobx等。
需要准备初始渲染数据,所以要精准获取当前地址将要渲染哪些组件。react-router-config和react-router同源配套,是个支持静态路由表配置的工具,提供了matchRoutes方法,可获得匹配的路由数组。
import { matchRoutes } from 'react-router-config' import loadable from '@loadable/component' const Root = loadable((props) => import('./pages/Root')) const Index = loadable(() => import("./pages/Index")) const Home = loadable(() => import("./pages/Home")) const routes = [ { path: 'https://www.jb51.net/', component: Root, routes: [ { path: '/index', component: Index, }, { path: '/home', component: Home, syncData () => {} routes: [] } ] } ] router.all('https://www.jb51.net/', async (url, next) => { const branch = matchRoutes(routes, url) })
组件的初始数据接口请求,最美的办法当然是定义在各自的class组件的静态方法中去,但是前提是组件不能被懒加载,不然获取不到组件class,当然也无法获取class static method了,很多使用@loadable/component(一个code split方案)库的开发者多次提issue,作者也明示无法支持。不支持懒加载是绝对不可能的了。所以委屈一下代码了,在需要的route对象中定义一个asyncData方法。
服务端
// routes.js { path: '/home', component: Home, asyncData (store, query) { const city = (query || '').split('=')[1] let promise = store.dispatch(fetchCityListAndTemperature(city || undefined)) let promise2 = store.dispatch(setRefetchFlag(false)) return Promise.all([promise, promise2]) return promise } }
// render.js import { matchRoutes } from 'react-router-config' import createStore from '../store/redux/index' const store = createStore() const branch = matchRoutes(routes, url) const promises = branch.map(({ route }) => { // 遍历所有匹配路由,预加载数据 return route.asyncData ? route.asyncData(store, query) : Promise.resolve(null) }) // 完成store的预加载数据初始化工作 await Promise.all(promises) // 获取最新的store const preloadedState = store.getState() const App = (props) => { return ( <Provider store={store}> <StaticRouter location={ctx.url} context={context}> <Layout /> </StaticRouter> </Provider> ) } // 数据准备好后,render整个应用 const html = renderToString(<App />) // 把预加载的数据挂载在`window`下返回,客户端自己去取 return <html> <head></head> <body> <div>${html}</div> <script> window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}; </script> </body> </html>
客户端
为保证两端的应用数据一致,客户端也要使用同一份数据初始化一次redux的store,再生成应用。如果两者的dom/数据不一致,导致浏览器接管的时候dom重新生成了一次,在开发模式下的时候,控制台会输出错误信息,开发体验完美。后续ajax的数据,在componentDidMount和事件中去执行,和服务端的逻辑天然剥离。
// 获取服务端提供的初始化数据 const preloadedState = window.__PRELOADED_STATE__ || undefined delete window.__PRELOADED_STATE__ // 客户端store初始化 const store = createStore(preloadedState) const App = () => { return ( <Provider store={store}> <BrowserRouter> <Layout /> </BrowserRouter> </Provider> ) } // loadableReady由@loadabel/component提供,在code split模式下使用 loadableReady().then(() => { ReactDom.hydrate(<App />, document.getElementById('app')) })
服务端调用的接口客户端也必须有。这就带来了如何避免重复请求的问题。我们知道componentDidMount方法只执行一次,如果服务器已经请求的数据带有一个标识,就可以根据这个标识决定是否在客户端需要发起一个新的请求了,需要注意的是判断完成后重置该标识。
import { connect } from 'react-redux' @connect( state => ({ refetchFlag: state.weather.refetchFlag, quality: state.weather.quality }), dispatch => ({ fetchCityListAndQuality: () => dispatch(fetchCityListAndQuality()), setRefetchFlag : () => dispatch(setRefetchFlag(true)) }) ) export default class Quality extends Component { componentDidMount () { const { location: { search }, refetchFlag, fetchCityListAndQuality, setRefetchFlag } = this.props const { location: city } = queryString.parse(search) refetchFlag ? fetchCityListAndQuality(city || undefined) : setRefetchFlag() } }
打包方案
客户端打包