准备好回答这些细节(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的时间和事件发生的所在时区的双时区方案。
- 但是移动端,我们会进行简化,只显示事件发生的时区。
- 就是对一些信息的取舍。 比如说,我们在境外项目中,要展示多时区,如果是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>
);
}