React 图片预加载组件的实现

现代 Web app 在处理图片的时候一般会采用加载 <img> 元素自动加载对应的图片资源的方式,但在某些特定需求下,需要在进入正式页面前就进行图片加载,这个加载过程中也同时需要进行 loading 页面的进度计数操作。因此,本文着重探讨在 React 语境下的图片预加载组件的实现。

图片预加载原理

在 javascript 中,对于图片的预加载,一般这样处理:

// 创建 img 对象
const img = new Image()

// 定义 onload 事件函数
img.onload = () => {
  // 完成加载后,对 img 对象进行操作
}

// 对 src 属性赋值
img.src = 'path/to/image'

在 React 的组件中也将这样处理,但更优雅一点的,我们封装一个 Promise :

const imgLoadPromise = (src) => {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => {
      resolve(img)
    }
    img.onerror = (err) => {
      reject(err)
    }
    img.src = src
  })
}

来一个手搓的 Provider

状态管理是个好东西,很多人说到 React 的状态管理都会想到 Redux 。但是在拥有了 Hooks 之后,一些简单的状态管理就不需要额外的依赖其他库。

多的就不说了,直接 show code :

// loading state

import { createContext, useReducer } from 'react'

// 状态初始化数据
const initialState = {
  loadingShow: true, // 是否显示 loading 页面
  loadingPercent: 0, // 预加载进度,单位 %
  sourceCount: 0,    // 需要加载的图片数量
  sourceLoaded: 0,   // 已经加载的图片数量
}

// 状态管理 reducer
const reducer = (state, action) => {
switch (action.type) {
    case 'hideLoading':
      return {
        ...state,
        loadingShow: false,
      }
    case 'updateLoading':
      return {
        ...state,
        loadingPercent: action.payload,
      }
    case 'countSource':
      return {
        ...state,
        sourceCount: state.sourceCount + 1,
      }
    case 'loadSource':
      const loaded = state.sourceLoaded + 1
      const percent = state.sourceCount
        ? Math.floor((loaded / state.sourceCount) * 100)
        : 0
      return {
        ...state,
        sourceLoaded: loaded,
        loadingPercent: percent,
        loadingShow: percent < 100,
      }
    default:
      throw new Error(`no such action: ${action.type}`)
  }
}

export const Context = createContext(
  state: initialState,
  dispatch: () => null,
)

const Provider = ({children}) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <Context.Provider value={[state, dispatch]}>{children}</Context.Provider>
  )
}

export default Provider

开始撸组件了

前文中所描述的 Provider 主要是为了统计全局的预加载状态,准备好之后,可以开始进行组件的编写:

import { useContext, useEffect, useState } from 'react'
import { Context } from 'loadingState'

const Preload = ({src}) => {
  const [img, setImg] = useState(null)
  const [state, dispatch] = useContext(Context)

  useEffect(() => {
    dispatch('countSource')
    imgLoadPromise(src).then(img => {
      setImg(img)
      dispatch('loadSource')
    })
  })

  return img
}

export default Preload

组件的使用

在根组件中可以用 Provider 组件,将所有的需要进行状态管理的组件包裹在其中:

<Provider>
  <Preload src="path/to/image" />
</Provider>

可以监控一下 state 中图片的加载进度。

篇后

此种预加载图片资源的方式只适合整个交互流程都在同一个页面的 SPA 中,如果是需要对于图片资源组件卸载的场景就不适用了。

对于后面的场景将在以后的文中进行探讨。