react ssr 路由同构与数据同构

452 阅读3分钟

路由同构

路由分为client路由与server路由

server路由根据每次不通的请求路径返回不同的组装页面,client路由则是在首屏渲染之后的页面中跳转,通过react-router-dom封装好的路由跳转方式(内部对pushState,replaceState方法进行了封装),这样在不请求server的情况下实现client端的路由跳转。

react-router-dom已经帮我们做好了双端端路由

BrowserRouter: client端使用的路由,可以拿到url所以不需要我们注入url

StaticRouter: 无法拿到url所以需要我们在server端注入url, 提供了locatiion参数注入

不同的端需要使用不通的组件,我们可以利用webpack.DefinePlugin注入不同的常量进行控制,并且webpack会在打包的时候对dead code进行tree shaking,这样打包后的文件在俩端也不会出现多余的代码 具体实现如下:

type Props = {
  path?: string
}
let isClient = process.env.client
let RouerWrapper = isClient ? BrowserRouter : StaticRouter

let Wrapper:React.FC<Props> = (props) => {

  return <>
    <RouerWrapper {...!isClient&&{location:props.path}}>
        <APP />
    </RouerWrapper>
  </>
}

server路径注入

let Docs: React.FC<{path?:string}> = ({path}) => <html>
  <head>
    <link rel="icon" href="data:image/ico;base64,aWNv" />
  </head>
  <body>
    <div id='root'>
        <Wrapper path={path}></Wrapper>
    </div>
    <script src="index.js"></script>
  </body>
</html>
exportdefault (ctx: Context, next: Next) => {
  const html = renderToString(<Docs path={ctx.request.url}/>)
  ctx.body = html

  return next()
}

直接访问user

路由跳转到user效果,client路由跳转,没有请求server

数据同构

数据同构用到的库有react-redux,@reduxjs/toolkit,redux只是一种单向数据流模型,保存了一份store,数据不能被直接修改,同过dispatch(action)的方式去修改store,所以这种模式即使放在server端也同样相当于存了一份store用dispatch去需改store而已,然后再将这份store注入到根,让数据从外层注入,内部组件便可以拿到这样一份store。 首先创建一个包含异步chunk的slice

let getUserData = createAsyncThunk('user/getUser',async() => {
  let { data } = await getUser()
  return data
})

let userReducer = createSlice({
  name: 'user',
  initialState: [],
  reducers: {},
  extraReducers: (build) => {
    build.addCase(getUserData.fulfilled,(state, action) => {
        return action.payload
    })
  }
})
export {
  getUserData,
  userReducer
}

创建一个store文件,导出clientStore与serverStore(如果server端不区分用户),要区分用户的话,单例的Store得改为storeFactory函数去创建不同的实例。server返回的初始数据放入到了window.state,需要在configureStore中初始化初始数据

let clientStore = process.env.client ? configureStore({
  reducer: {
    [userReducer.name]: userReducer.reducer
  },
  // 数据初始化
  preloadedState: (windowas Window & typeof globalThis & {"__state__":any}).__state__
})
: null

process.env.client && deletewindow["__state__"]

let serverStore = configureStore({
  reducer: {
    [userReducer.name]: userReducer.reducer
  }
})

let storeFactory = () => configureStore({
  reducer: {
    [userReducer.name]: userReducer.reducer
  }
})

接下来提供一个provider从根注入数据

type Props = {
  path?: string,
  store: ReturnType<typeof storeFactory>
}
let isClient = process.env.client
let RouerWrapper = isClient ? BrowserRouter : StaticRouter

let Wrapper:React.FC<Props> = ({store:serverStore, path}) => {

  return <>
    <Provider store={isClient ? clientStore : serverStore}>
      <RouerWrapper {...!isClient&&{location:path}}>
        <APP />
      </RouerWrapper>
    </Provider>
  </>
}

下面再把获取初始数据的函数挂在路由配置上并在server端根据请求路径去匹配并更新serverStore

interface ItemRoute {
  path: string,
  element: ReactNode,
  loadData?: (store: Store,params?:any) =>any
}
let router:ItemRoute[]= [
  {
    path: '/',
    element: <Index />
  },
  {
    path: '/user',
    element: <User />,
    loadData: (store) => {
      return store.dispatch(getUserData())
    }
  },
  {
    path: '*',
    element: 'not found'
  }
]

server端匹配并更新store注入

let Docs: React.FC<{ path?: string, state: any, store: ReturnType<typeof storeFactory> }> = ({ path, state, store }) => <html>
  <head>
      <link rel="icon" href="data:image/ico;base64,aWNv" />
  </head>
  <body>
      <div id='root'>
          <Wrapper store={store} path={path}></Wrapper>
      </div>
      <script dangerouslySetInnerHTML={{__html:`window.__state__ = ${JSON.stringify(state)}`}}>
      </script>
      <script src="index.js"></script>
  </body>
</html>
export default async (ctx: Context, next: Next) => {
  let [route] = matchRoutes<ItemRoute>(router, ctx.request.url)
  let serverStore = storeFactory()
  if(route.route.loadData) {
      await route.route.loadData(serverStore)
  }
  // renderToString params is the hydrateRoot container and children
  const html = renderToString(<Docs store={serverStore} state={serverStore.getState()} path={ctx.request.url} />)
  ctx.body = html
  return next()
}

因为/user在请求server时候才会注入数据,先请求index再在client端跳转/user的时候是没有注入store的,所以还要给User加个Effect去判断有无数据去请求数据。

直接请求user能拿到数据

​​代码仓库:gitee.com/summarize/r…