Skip to content

准备好回答这些细节(Tell me more) 无论你选择哪个版本,面试时一定要能对下面的问题对答如流:

关于决策:

“你提到废弃 React Native,能具体讲讲当时 RN 方案遇到了什么瓶颈吗?” 回答思路:可以从几个角度说:1. 性能问题(特定场景下的渲染性能不佳);2. 维护成本(需要同时维护 Web 和 RN 两套代码,心智负担重,对人员技能要求高);3. 生态和兼容性(底层 SDK 升级困难,某些原生能力支持不佳);4. 体验一致性(无法做到和 Web 端像素级的体验对齐)。

关于技术方案:

你们的移动端适配具体是怎么做的?为什么选择 rem/vw 而不是其他方案?

回答思路:我们选择了 rem 方案,因为它可以基于根节点的 font-size 实现等比缩放,能够很好地还原设计稿的比例。我们编写了一个脚本,在应用初始化时根据设备的 devicePixelRatio 和视口宽度动态计算并设置 <html> 的 font-size。相比于 vw,rem 在处理一些需要固定大小的边框或字体时更灵活,兼容性也更好。

关于挑战:

“在把一个为 PC 设计的项目适配到移动端的过程中,你遇到的最大挑战是什么?”

回答思路:最大的挑战不仅仅是布局的改变,更是交互模式的重设计。

  • PC 端依赖鼠标的悬停(hover)、右键等操作,在移动端都需要重新设计为触摸(tap)、长按(long press)等。
  • 此外,PC 的大屏信息展示密度很高,在移动端小屏上需要对信息进行优先级排序和折叠,如何优雅地处理这些信息而不影响核心功能,是我们花费了大量时间讨论和设计的。
    • 就是对一些信息的取舍。 比如说,我们在境外项目中,要展示多时区,如果是web端,可以直接使用 utc+0的时间和事件发生的所在时区的双时区方案。
      • 但是移动端,我们会进行简化,只显示事件发生的时区。

统一封装:

jsx
import React, { useState, useRef, useCallback, useEffect } from 'react';

// ===================================================================
// 1. 核心:自定义 Hook,封装并统一交互逻辑
// ===================================================================
const useInteractive = ({
  onTap,
  onLongPress,
  onHoverStart,
  onHoverEnd,
  longPressDelay = 500, // 长按的延迟时间(毫秒)
}) => {
  const [isTouchDevice, setIsTouchDevice] = useState(false);
  const longPressTimer = useRef(null);

  // 在组件加载时检测一次是否为触摸设备
  useEffect(() => {
    // 这是一个简单的检测方法,在大多数情况下都有效
    const touchCheck = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
    setIsTouchDevice(touchCheck);
  }, []);

  const clearLongPressTimer = () => {
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current);
      longPressTimer.current = null;
    }
  };

  // --- 定义统一的行为触发器 ---
  
  const triggerTap = useCallback(() => {
    onTap?.(); // 如果定义了 onTap 回调,就执行它
  }, [onTap]);

  const triggerLongPress = useCallback(() => {
    onLongPress?.(); // 如果定义了 onLongPress 回调,就执行它
  }, [onLongPress]);


  // --- 根据设备类型,返回不同的事件处理器 ---

  if (isTouchDevice) {
    // --- 触摸设备事件处理器 ---
    return {
      onTouchStart: () => {
        // 当手指按下时,启动一个计时器,准备触发长按
        longPressTimer.current = setTimeout(triggerLongPress, longPressDelay);
      },
      onTouchEnd: () => {
        // 当手指抬起时,如果长按计时器还在(说明还没触发长按),
        // 就说明这是一个 "tap" 行为。
        if (longPressTimer.current) {
          triggerTap();
        }
        // 无论如何,都要清除计时器
        clearLongPressTimer();
      },
      onTouchMove: () => {
        // 如果手指在屏幕上移动(比如滚动页面),
        // 那就不应该触发长按或点击,所以直接取消计时器。
        clearLongPressTimer();
      },
    };
  } else {
    // --- PC 端(鼠标)事件处理器 ---
    return {
      // 将 PC 的单击映射为 "tap"
      onClick: triggerTap,
      
      // 将 PC 的鼠标右键点击映射为 "long press"
      onContextMenu: (e) => {
        e.preventDefault(); // 阻止浏览器默认的右键菜单
        triggerLongPress();
      },

      // PC 的 hover 行为
      onMouseEnter: () => onHoverStart?.(),
      onMouseLeave: () => onHoverEnd?.(),
    };
  }
};


// ===================================================================
// 2. 业务组件:它只关心抽象的交互,不关心平台
// ===================================================================
const InteractiveCard = ({ id, onCardTap, onCardLongPress }) => {
  const [status, setStatus] = useState('Default');
  const [bgColor, setBgColor] = useState('#41b883');

  // 使用我们的自定义 Hook
  const interactiveProps = useInteractive({
    onTap: () => {
      setStatus(`Tapped! (ID: ${id})`);
      onCardTap(id); // 通知父组件
    },
    onLongPress: () => {
      setStatus(`Long Pressed! (ID: ${id})`);
      onCardLongPress(id); // 通知父组件
    },
    onHoverStart: () => setBgColor('#3aa775'),
    onHoverEnd: () => setBgColor('#41b883'),
    longPressDelay: 400,
  });

  // 业务组件的 JSX 变得非常干净。
  // 它不知道什么是 onContextMenu 或 onTouchStart,
  // 它只是把从 Hook 拿到的 props 展开。
  return (
    <div
      {...interactiveProps} // ✨ 魔法发生在这里!✨
      style={{
        padding: '20px',
        backgroundColor: bgColor,
        color: 'white',
        borderRadius: '8px',
        textAlign: 'center',
        cursor: 'pointer',
        userSelect: 'none', // 防止长按时选中文本
        transition: 'background-color 0.2s',
      }}
    >
      <h3>Card {id}</h3>
      <p>Status: {status}</p>
    </div>
  );
};


// ===================================================================
// 3. 应用主入口:消费业务组件
// ===================================================================
export default function App() {
  const handleCardTap = (id) => {
    console.log(`Parent received tap from card ${id}`);
    alert(`You tapped card ${id}!`);
  };

  const handleCardLongPress = (id) => {
    console.log(`Parent received long press from card ${id}`);
    alert(`You long pressed card ${id}!`);
  };

  return (
    <div style={{ maxWidth: '600px', margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>Unified Interaction Demo</h1>
      <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px', backgroundColor: '#f9f9f9', marginBottom: '20px' }}>
        <p><b>On PC / Desktop:</b></p>
        <ul>
          <li><b>Click:</b> Triggers "Tap" event.</li>
          <li><b>Hover:</b> Changes background color.</li>
          <li><b>Right-click:</b> Triggers "Long Press" event.</li>
        </ul>
        <p><b>On Mobile / Touch Device:</b></p>
        <ul>
          <li><b>Short Tap:</b> Triggers "Tap" event.</li>
          <li><b>Touch and Hold:</b> Triggers "Long Press" event.</li>
        </ul>
      </div>

      <div style={{ display: 'grid', gap: '20px' }}>
        <InteractiveCard id={1} onCardTap={handleCardTap} onCardLongPress={handleCardLongPress} />
        <InteractiveCard id={2} onCardTap={handleCardTap} onCardLongPress={handleCardLongPress} />
      </div>
    </div>
  );
}

封装esc和边缘右滑

jsx
import React, { useState, useEffect, useRef } from 'react';

// ===================================================================
// 1. 核心逻辑: 自定义 Hook `useGoBackGesture`
// ===================================================================
const useGoBackGesture = ({ onGoBack }) => {
  // --- Refs to store touch state without causing re-renders ---
  const touchStartPos = useRef(null); // 记录触摸开始的位置 {x, y}
  const isSwiping = useRef(false);     // 标记是否正在进行有效的滑动

  useEffect(() => {
    // --- 键盘事件处理 ---
    const handleKeyDown = (event) => {
      if (event.key === 'Escape') {
        onGoBack?.();
      }
    };

    // --- 触摸事件处理 ---
    const handleTouchStart = (event) => {
      const touch = event.touches[0];
      // 只在屏幕左侧边缘 (例如前 25px) 开始的触摸才被认为是潜在的返回手势
      if (touch.clientX < 25) {
        touchStartPos.current = { x: touch.clientX, y: touch.clientY };
        isSwiping.current = true; // 准备开始滑动
      }
    };

    const handleTouchMove = (event) => {
      if (!isSwiping.current || !touchStartPos.current) return;

      const touch = event.touches[0];
      const deltaX = touch.clientX - touchStartPos.current.x;
      const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);

      // 如果垂直滑动距离太大(比如用户是在上下滚动页面),则取消返回手势
      if (deltaY > 20) {
        isSwiping.current = false;
        return;
      }
      
      // 我们可以在这里根据 deltaX 更新 UI,比如让页面跟着手指稍微移动,以提供视觉反馈
      // (为保持例子简洁,此处省略)
    };

    const handleTouchEnd = (event) => {
      if (!isSwiping.current || !touchStartPos.current) return;
      
      const touch = event.changedTouches[0];
      const deltaX = touch.clientX - touchStartPos.current.x;

      // 只有当向右滑动距离超过一个阈值(比如 50px)时,才触发返回
      if (deltaX > 50) {
        onGoBack?.();
      }

      // 重置状态
      isSwiping.current = false;
      touchStartPos.current = null;
    };

    // --- 绑定全局事件监听器 ---
    document.addEventListener('keydown', handleKeyDown);
    window.addEventListener('touchstart', handleTouchStart);
    window.addEventListener('touchmove', handleTouchMove);
    window.addEventListener('touchend', handleTouchEnd);

    // --- 清理函数,在组件卸载时移除监听器,防止内存泄漏 ---
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('touchstart', handleTouchStart);
      window.removeEventListener('touchmove', handleTouchMove);
      window.removeEventListener('touchend', handleTouchEnd);
    };
  }, [onGoBack]); // 依赖项数组确保 onGoBack 更新时,能使用最新的回调
};


// ===================================================================
// 2. 业务组件: 使用 Hook 的全屏模态框
// ===================================================================
const FullScreenModal = ({ isOpen, onClose }) => {
  // ✨ 使用我们的自定义 Hook ✨
  // 它不需要返回任何 props,因为它处理的是全局事件
  useGoBackGesture({ onGoBack: onClose });

  if (!isOpen) {
    return null;
  }

  return (
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
      backgroundColor: 'rgba(0, 0, 0, 0.7)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: 'white'
    }}>
      <div style={{ padding: '30px', backgroundColor: '#333', borderRadius: '10px', textAlign: 'center' }}>
        <h2>This is a Modal</h2>
        <p>Press 'Esc' or swipe right from the left edge to close.</p>
        <button onClick={onClose} style={{ marginTop: '20px', padding: '10px 20px' }}>
          Or Click Me to Close
        </button>
      </div>
    </div>
  );
};


// ===================================================================
// 3. 应用主入口
// ===================================================================
export default function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Unified "Go Back" Gesture Demo</h1>
      <p>Click the button below to open a modal.</p>
      <button onClick={() => setIsModalOpen(true)}>
        Open Modal
      </button>

      <FullScreenModal 
        isOpen={isModalOpen} 
        onClose={() => setIsModalOpen(false)} 
      />
    </div>
  );
}
本站访客数 人次 本站总访问量