MST
星途 面试题库

面试题:React列表渲染动画在SSR场景下的难点与解决方案

在使用React进行服务器端渲染(SSR)的项目中,需要为列表渲染添加动画效果。请分析在SSR场景下实现列表渲染动画会遇到哪些难点,例如如何避免动画闪烁、如何在服务器和客户端保持动画状态一致等,并给出详细的解决方案和示例代码。
25.9万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

难点分析

  1. 动画闪烁
    • 原因:在服务器端渲染时,HTML 会以静态形式生成,没有动画样式。当客户端 JavaScript 加载并初始化动画时,会出现从无动画到有动画的切换,从而导致闪烁。
    • 解决方案:在服务器端渲染时,预计算并添加初始动画状态的样式。例如,如果是淡入动画,在服务器端生成的 HTML 元素上添加 opacity: 0 等初始样式,然后在客户端通过 JavaScript 启动动画,从这个初始状态过渡到最终状态,避免突兀的变化。
  2. 服务器和客户端动画状态一致
    • 原因:服务器和客户端运行环境不同,服务器端渲染是一次性生成 HTML,而客户端是在浏览器环境下动态执行 JavaScript。如果不进行特殊处理,两边对动画状态的管理方式可能不同,导致不一致。
    • 解决方案:使用状态管理工具(如 Redux 或 MobX)来统一管理动画状态。在服务器端渲染时,根据初始数据计算并设置动画状态到状态管理工具中,在客户端渲染时,从状态管理工具中读取相同的动画状态,保证两边状态一致。同时,可以使用唯一标识(如列表项的 ID)来关联服务器和客户端的动画元素。

示例代码

以下以 React + Redux 为例,实现一个简单的列表淡入动画:

  1. 安装依赖
    npm install react react - dom redux react - redux
    
  2. 创建 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;
    
  3. 创建 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;
    
  4. 在服务器端渲染时
    • 在服务器端获取初始数据,计算动画状态并设置到 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}`);
    });
    
  5. 在客户端渲染时
    • 在客户端从 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 场景下实现了列表渲染动画,避免了动画闪烁并保持了服务器和客户端动画状态一致。