路由同构
路由分为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…