Skip to content
Go back

react-photo-view 学习指南

Published:  at  10:22 PM

react-photo-view 学习指南

一款超精致的 React 图片预览组件库,适合作为学习 React 组件开发、交互设计和工程实践的优秀案例。


1. 项目整体架构

这是一个采用 Monorepo 结构管理的 React 图片预览组件库,代码组织清晰,模块职责分明。

react-photo-view/
├── packages/
│   └── react-photo-view/    # 核心库
│       └── src/
│           ├── components/  # UI 组件(图标、Portal等)
│           ├── hooks/        # 自定义 Hooks(核心逻辑)
│           └── utils/        # 工具函数(数学计算)

目录职责

目录职责包含文件
components/基础 UI 组件CloseIcon, ArrowLeft, ArrowRight, Spinner, SlidePortal, PreventScroll
hooks/核心交互逻辑15+ 个自定义 Hooks,管理状态、动画、手势等
utils/数学计算工具边缘检测、位置计算、旋转尺寸、设备检测等

2. 核心技术栈

技术用途学习价值
React Context状态管理深入理解 Provider/Consumer 模式
TypeScript类型安全完整的类型定义实践
自定义 Hooks逻辑复用Hooks 设计模式最佳实践
Touch/Pointer Events手势交互移动端交互开发
CSS Modules (LESS)样式管理样式隔离方案
Portal弹层渲染弹窗组件设计

3. 核心组件学习路径

入门阶段

1.1 index.ts — 导出入口

最简单的文件,理解库的导出方式:

import PhotoProvider from './PhotoProvider';
import PhotoView from './PhotoView';
import PhotoSlider from './PhotoSlider';

export { PhotoProvider, PhotoView, PhotoSlider };

学习要点:库的模块化导出设计


1.2 photo-context.ts — React Context 定义

这是整个库的状态通信桥梁:

import { createContext } from 'react';
import type { DataType } from './types';

export interface PhotoContextType {
  show: (key: number) => void;
  update: UpdateItemType;
  remove: (key: number) => void;
  nextId: () => number;
}

export default createContext<PhotoContextType>(undefined as unknown as PhotoContextType);

学习要点


1.3 types.ts — 类型定义(约 8.9KB)

这是整个库最详细的”功能文档”,建议通读一遍。

主要类型:

// 资源数据类型
export interface DataType {
  key: number | string; // 唯一标识
  src?: string; // 资源地址
  render?: (props) => React.ReactNode; // 自定义渲染
  overlay?: React.ReactNode; // 自定义覆盖节点
  width?: number; // 宽度
  height?: number; // 高度
  originRef?: React.MutableRefObject<HTMLElement | null>; // 触发元素
}

// Provider 配置
export interface PhotoProviderBase {
  loop?: boolean | number; // 循环预览
  speed?: (type) => number; // 动画速度
  easing?: (type) => string; // 动画缓动函数
  photoClosable?: boolean; // 图片点击可关闭
  maskClosable?: boolean; // 背景点击可关闭
  maskOpacity?: number | null; // 背景透明度
  pullClosable?: boolean; // 下拉可关闭
  bannerVisible?: boolean; // 导航条显示
  overlayRender?: (props) => React.ReactNode; // 自定义覆盖物
  toolbarRender?: (props) => React.ReactNode; // 自定义工具栏
  // ... 更多配置
}

学习要点


进阶阶段

1.4 PhotoProvider.tsx — 状态管理中心

这是整个应用的状态大脑,负责管理图片列表、当前索引、显示状态等。

核心代码结构:

export default function PhotoProvider({ children, onIndexChange, onVisibleChange, ...restProps }) {
  const [state, updateState] = useSetState(initialState);
  const uniqueIdRef = useRef(0);

  // 管理图片的方法
  const methods = useMethods({
    nextId() { return (uniqueIdRef.current += 1); },
    update(imageItem: DataType) { /* 更新图片 */ },
    remove(key: number) { /* 移除图片 */ },
    show(key: number) { /* 显示预览 */ },
  });

  // 切换和关闭的方法
  const fn = useMethods({
    close() { /* 关闭预览 */ },
    changeIndex(nextIndex: number) { /* 切换图片 */ },
  });

  const value = useMemo(() => ({ ...state, ...methods }), [state, methods]);

  return (
    <PhotoContext.Provider value={value}>
      {children}
      <PhotoSlider {...restProps} />
    </PhotoContext.Provider>
  );
}

学习要点


1.5 PhotoView.tsx — 单张图片组件

负责包装用户的图片元素,连接到 Provider。

核心代码结构:

const PhotoView: React.FC<PhotoViewProps> = ({
  src,
  render,
  overlay,
  width,
  height,
  triggers = ['onClick'],
  children,
}) => {
  const photoContext = useContext(PhotoContext);
  const key = useInitial(() => photoContext.nextId());
  const originRef = useRef<HTMLElement>(null);

  // 将 ref 传递给父组件
  useImperativeHandle((children as React.FunctionComponentElement<HTMLElement>)?.ref, () => originRef.current);

  // 注册到 Provider
  useEffect(() => {
    photoContext.update({ key, src, originRef, render: fn.render, overlay, width, height });
  }, [src]);

  // 克隆子组件,注入事件
  if (children) {
    return Children.only(cloneElement(children, { ...eventListeners, ref: originRef }));
  }
  return null;
};

学习要点


1.6 PhotoSlider.tsx — 预览主组件(核心)

这是最复杂的组件,负责处理图片预览的所有交互逻辑:

文件较大(约 13KB),建议分模块学习。

学习要点


4. Hooks 深入学习

这是本库最精华的部分,包含 15+ 个自定义 Hooks。

4.1 基础 Hooks

useSetState — 类 setState 实现

export default function useSetState<S extends Record<string, any>>(initialState: S) {
  return useReducer(
    (state: S, action: Partial<S> | ((state: S) => Partial<S>)) => ({
      ...state,
      ...(typeof action === 'function' ? action(state) : action),
    }),
    initialState,
  );
}

学习要点


useMethods — 持久化方法引用

export default function useMethods<T extends Record<string, (...args: any[]) => any>>(fn: T) {
  const { current } = useRef({
    fn,
    curr: undefined as T | undefined,
  });
  current.fn = fn;

  if (!current.curr) {
    const curr = Object.create(null);
    Object.keys(fn).forEach((key) => {
      curr[key] = (...args: unknown[]) => current.fn[key].call(current.fn, ...args);
    });
    current.curr = curr;
  }

  return current.curr as T;
}

解决的问题:当组件重渲染时,方法引用会变化,导致子组件不必要的重渲染。

学习要点


useInitial — 只执行一次

export default function useInitial<T>(fn: () => T): T {
  const initial = useRef(fn());
  return initial.current;
}

学习要点


4.2 交互 Hooks

useScrollPosition — 触摸滚动与物理效果

这是最核心的 Hook,实现了带物理效果的惯性滚动。

核心逻辑:

export default function useScrollPosition(callbackX, callbackY, callbackS) {
  return (x, y, lastX, lastY, width, height, scale, safeScale, lastScale, rotate, touchedTime) => {
    // 计算移动速度
    const moveTime = Date.now() - touchedTime;
    const speedX = (x - lastX) / moveTime;
    const speedY = (y - lastY) / moveTime;

    // 物理滚动逻辑...
    // 边缘回弹逻辑...
  };
}

学习要点


useTargetScale — 缩放计算

处理双指缩放的缩放比例计算。

学习要点


useContinuousTap — 连续点击处理

区分单击、双击、三击等操作。

学习要点


4.3 动画 Hooks

useAnimationVisible — 显示/隐藏动画

管理组件的入场和退场动画。

export default function useAnimationVisible(visible, onClose) {
  const [isVisible, setIsVisible] = useState(visible);
  const [animating, setAnimating] = useState(false);

  // 监听 visible 变化,触发动画
  useEffect(() => {
    if (visible) {
      setIsVisible(true);
    } else {
      setAnimating(true); // 开始退场动画
    }
  }, [visible]);

  // 动画结束回调
  const onAnimationEnd = () => {
    if (!visible) {
      setIsVisible(false);
      setAnimating(false);
    }
  };

  return { isVisible, animating, onAnimationEnd };
}

学习要点


useAnimationPosition — 位置动画

管理图片位置变化的动画。

学习要点


useAnimationOrigin — 来源动画(FLIP)

实现从缩略图到全屏的过渡动画(来源动画)。

学习要点


4.4 Hooks 一览表

Hook功能复杂度推荐度
useSetState类 setState⭐⭐⭐
useMethods持久化方法⭐⭐⭐⭐⭐
useInitial初始化一次⭐⭐⭐
useScrollPosition触摸滚动⭐⭐⭐⭐⭐⭐⭐⭐⭐
useTargetScale缩放计算⭐⭐⭐⭐⭐⭐⭐
useContinuousTap连续点击⭐⭐⭐⭐⭐
useAnimationVisible显示动画⭐⭐⭐⭐⭐
useAnimationPosition位置动画⭐⭐⭐⭐⭐⭐⭐
useAnimationOrigin来源动画⭐⭐⭐⭐⭐⭐⭐
useEventListener事件监听⭐⭐⭐⭐⭐
useDebounceCallback防抖回调⭐⭐⭐⭐⭐
useForkedVariable分叉变量⭐⭐⭐⭐

5. 工具函数学习

utils/
├── edgeHandle.ts                    # 边缘检测与处理
├── getPositionOnMoveOrScale.ts      # 移动/缩放时的位置计算
├── getRotateSize.ts                 # 旋转后的尺寸计算
├── getSuitableImageSize.ts          # 自适应图片尺寸
├── isTouchDevice.ts                 # 触摸设备检测
├── limitTarget.ts                   # 数值限制工具
└── getMultipleTouchPosition.ts      # 多指触摸位置

5.1 重点工具函数

edgeHandle.ts — 边缘检测

export function computePositionEdge(position, scale, size, windowSize) {
  const scaledSize = size * scale;
  const edge = scaledSize - windowSize;

  // 触边判断
  if (scaledSize <= windowSize) {
    return [false, null]; // 未触边
  }

  if (position > 0) {
    return [true, 0]; // 触上边
  }

  if (position < -edge) {
    return [true, -edge]; // 触下边
  }

  return [false, null];
}

学习要点


getPositionOnMoveOrScale.ts — 缩放中心计算

计算缩放时的中心点位置,保证缩放后图片仍在视口内。

学习要点


getRotateSize.ts — 旋转尺寸计算

export default function getRotateSize(rotate: number, width: number, height: number) {
  const rad = (rotate * Math.PI) / 180;
  const cos = Math.abs(Math.cos(rad));
  const sin = Math.abs(Math.sin(rad));

  const newWidth = width * cos + height * sin;
  const newHeight = width * sin + height * cos;

  return [newWidth, newHeight];
}

学习要点


6. 关键设计模式

6.1 Context 模式

// 1. 定义 Context
export default createContext<PhotoContextType>(undefined as unknown as PhotoContextType);

// 2. Provider 提供者
<PhotoContext.Provider value={value}>
  {children}
</PhotoContext.Provider>

// 3. Consumer 消费者
const { show, update } = useContext(PhotoContext);

适用场景:跨组件通信、全局状态管理


6.2 useMethods 模式

解决的问题:防止方法引用变化导致子组件重渲染

// 不使用 useMethods:每次渲染 methods 引用都会变化
const methods = {
  show: () => {...},
  close: () => {...},
};

// 使用 useMethods:方法引用保持稳定
const methods = useMethods({
  show: () => {...},
  close: () => {...},
});

6.3 状态机模式

在 types.ts 中定义了动画状态:

export type EasingMode =
  | 0 // 未初始化
  | 1 // 进入:开始
  | 2 // 进入:动画开始
  | 3 // 进入:动画第二帧
  | 4 // 正常
  | 5; // 关闭

优点:状态流转清晰,便于调试


6.4 Portal 渲染模式

使用 React Portal 将组件渲染到 body 下:

import { createPortal } from 'react-dom';

export default function SlidePortal({ children, portalContainer }) {
  const container = portalContainer || document.body;
  return createPortal(children, container);
}

优点:避免 z-index 层级问题、样式隔离


7. 推荐学习步骤

7.1 实践路径

Step 1: 运行示例
  └─> pnpm dev
  └─> 查看效果,熟悉功能

Step 2: 阅读 types.ts
  └─> 了解所有 API 和类型定义
  └─> 这是最详细的功能文档

Step 3: 分析 PhotoProvider
  └─> 理解状态管理方式
  └─> 理解 Context 提供者模式

Step 4: 追踪 PhotoView → PhotoSlider
  └─> 理解组件协作关系
  └─> 理解数据流

Step 5: 深入 useScrollPosition
  └─> 学习手势处理核心逻辑
  └─> 理解物理滚动算法

Step 6: 研究动画 Hooks
  └─> 理解动画状态机
  └─> FLIP 动画技术

Step 7: 实现自己的功能扩展
  └─> 如:添加旋转按钮
  └─> 如:添加下载功能

7.2 自己实现一遍

建议按照以下顺序自己实现一个简化版本:

  1. 先实现最简单的图片显示
  2. 添加点击打开/关闭功能
  3. 实现图片切换(上一张/下一张)
  4. 添加触摸滑动
  5. 添加缩放功能
  6. 最后添加动画效果

这样能体会到作者的思考过程,比直接看完整代码收获更大!


8. 进阶话题

当你掌握基础后,可以深入研究:

8.1 物理滚动算法

文件:hooks/useScrollPosition.ts


8.2 双指缩放

文件:hooks/useTargetScale.ts


8.3 FLIP 动画

文件:hooks/useAnimationOrigin.tsx


8.4 Portal 渲染

文件:components/SlidePortal.tsx


8.5 SSR 支持

文件:hooks/useIsomorphicLayoutEffect.ts


9. 代码质量亮点

9.1 类型安全

9.2 性能优化

9.3 代码组织

9.4 错误处理


10. 在线资源


11. 总结

react-photo-view 是一个学习价值很高的项目,适合学习:

  1. React 组件开发 — 组件设计、状态管理、Context 使用
  2. 交互开发 — 手势处理、触摸事件、物理效果
  3. 动画实现 — 状态机、FLIP 技术、CSS 动画
  4. 工程实践 — TypeScript、代码组织、性能优化
  5. 用户体验 — 细节打磨、自然交互

建议按照推荐的学习路径循序渐进,多动手实践,最终一定能掌握这个优秀组件库的核心设计思想。


祝学习愉快!


Suggest Changes

Next Post
检查项目依赖的开源许可证