面试题答案
一键面试难点分析
- 动画闪烁:
- 原因:在服务器端渲染时,HTML 会以静态形式生成,没有动画样式。当客户端 JavaScript 加载并初始化动画时,会出现从无动画到有动画的切换,从而导致闪烁。
- 解决方案:在服务器端渲染时,预计算并添加初始动画状态的样式。例如,如果是淡入动画,在服务器端生成的 HTML 元素上添加
opacity: 0
等初始样式,然后在客户端通过 JavaScript 启动动画,从这个初始状态过渡到最终状态,避免突兀的变化。
- 服务器和客户端动画状态一致:
- 原因:服务器和客户端运行环境不同,服务器端渲染是一次性生成 HTML,而客户端是在浏览器环境下动态执行 JavaScript。如果不进行特殊处理,两边对动画状态的管理方式可能不同,导致不一致。
- 解决方案:使用状态管理工具(如 Redux 或 MobX)来统一管理动画状态。在服务器端渲染时,根据初始数据计算并设置动画状态到状态管理工具中,在客户端渲染时,从状态管理工具中读取相同的动画状态,保证两边状态一致。同时,可以使用唯一标识(如列表项的 ID)来关联服务器和客户端的动画元素。
示例代码
以下以 React + Redux 为例,实现一个简单的列表淡入动画:
- 安装依赖:
npm install react react - dom redux react - redux
- 创建 Redux 相关文件:
- actions.js:
const SET_LIST_ANIMATION = 'SET_LIST_ANIMATION'; export const setListAnimation = (animationStates) => ({ type: SET_LIST_ANIMATION, payload: animationStates });
- reducer.js:
const initialState = { listAnimationStates: [] }; const listAnimationReducer = (state = initialState, action) => { switch (action.type) { case SET_LIST_ANIMATION: return { ...state, listAnimationStates: action.payload }; default: return state; } }; export default listAnimationReducer;
- store.js:
import { createStore } from'redux'; import listAnimationReducer from './reducer'; const store = createStore(listAnimationReducer); export default store;
- 创建 React 组件:
- ListItem.js:
import React from'react'; import { useSelector } from'react - redux'; const ListItem = ({ item, index }) => { const animationStates = useSelector(state => state.listAnimationStates); const animationState = animationStates[index]; return ( <div style={{ opacity: animationState? animationState.opacity : 0, animation: 'fadeIn 1s ease - in - out forwards', animationDelay: `${index * 0.1}s` }} > {item} </div> ); }; export default ListItem;
- List.js:
import React from'react'; import ListItem from './ListItem'; import { useDispatch } from'react - redux'; import { setListAnimation } from './actions'; const List = () => { const dispatch = useDispatch(); const items = ['Item 1', 'Item 2', 'Item 3']; const animationStates = items.map(() => ({ opacity: 0 })); React.useEffect(() => { dispatch(setListAnimation(animationStates)); setTimeout(() => { const newAnimationStates = animationStates.map(state => ({...state, opacity: 1 })); dispatch(setListAnimation(newAnimationStates)); }, 100); }, []); return ( <div> {items.map((item, index) => ( <ListItem key={index} item={item} index={index} /> ))} </div> ); }; export default List;
- 在服务器端渲染时:
- 在服务器端获取初始数据,计算动画状态并设置到 Redux store 中,然后将 store 中的状态序列化后传递给客户端。
- 例如,在 Node.js 服务器端:
import express from 'express'; import React from'react'; import { renderToString } from'react - dom/server'; import { Provider } from'react - redux'; import store from './store'; import List from './List'; const app = express(); app.get('*', (req, res) => { const items = ['Item 1', 'Item 2', 'Item 3']; const animationStates = items.map(() => ({ opacity: 0 })); store.dispatch(setListAnimation(animationStates)); const html = renderToString( <Provider store={store}> <List /> </Provider> ); const preloadedState = store.getState(); res.send(` <!DOCTYPE html> <html> <head> <title>SSR List Animation</title> </head> <body> <div id="root">${html}</div> <script> window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')} </script> <script src="/client - bundle.js"></script> </body> </html> `); }); const port = 3000; app.listen(port, () => { console.log(`Server running on port ${port}`); });
- 在客户端渲染时:
- 在客户端从
window.__PRELOADED_STATE__
中读取预加载的状态,并初始化 Redux store。 - client.js:
import React from'react'; import { hydrate } from'react - dom'; import { Provider } from'react - redux'; import store from './store'; import List from './List'; const preloadedState = window.__PRELOADED_STATE__; const newStore = store; newStore.replaceReducer(preloadedState); hydrate( <Provider store={newStore}> <List /> </Provider>, document.getElementById('root') );
- 在客户端从
这样,通过预计算初始动画样式和使用 Redux 管理动画状态,在 SSR 场景下实现了列表渲染动画,避免了动画闪烁并保持了服务器和客户端动画状态一致。