Robot Img

介绍

一个高效、灵活的图片 React 组件,自动实现图片懒加载功能,并能按需适配云厂商的图片处理功能。

npm 安装:

~ npm install @robot-img/react-img

推荐设置

如果你使用的是 Emotion 或是 styled-components ,推荐使用以下配置方式:

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupportedSync,
  createSrcTplOfAliOss,
  imgPool,
  ImgProps,
  useImg,
} from '@robot-img/react-img'

// 设置图片组件样式
const StyledImg = styled.div<{ $src: string }>`
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  transition: background-image 0.3s;
  ${(props) => props.$src && `background-image: url(${props.$src});`}
`

// 使用 useImg 自定义组件
const Img = React.forwardRef<HTMLDivElement, ImgProps<HTMLDivElement>>((props, ref) => {
  const { domProps, state, handleRef } = useImg(props, ref)
  return <StyledImg {...domProps} $src={state.src} ref={handleRef} />
})

// 具体使用时,设置对应的样式
const OssImg = styled(Img)`
  width: 200px;
  height: 160px;
`

function main() {
  // 根据云厂商来设置全局图片后缀,获取最适合的图片
  // 这里使用的阿里云的图片处理作为案例
  imgPool.reset({
    createSrcTpl: createSrcTplOfAliOss({
      // 判断浏览器是否支持 webp 格式图片
      webp: checkWebpSupportedSync(),
    }),
  })
  const imgSrc = '//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg'
  // 真实加载的图片为: ${imgSrc}?x-oss-process=image/resize,m_fill,w_400,h_320/format,webp
  ReactDOM.render(<OssImg src={imgSrc} />, document.getElementById('10-recommend'))
}

main()

默认设置

默认情况将只实现懒加载功能,而不对图片后缀做任何处理,这个时候无需做任何设置,可以直接使用:

import React from 'react'
import ReactDOM from 'react-dom'

import { Img } from '@robot-img/react-img'

function main() {
  const imgSrc = '//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg'
  // 图片后缀不做任何处理
  ReactDOM.render(
    <Img src={imgSrc} style={{ width: 200 }} />,
    document.getElementById('20-default')
  )
}

main()

使用 css 方案

当然,你也可以使用 css-module 或者 less\scss 等方案自行设置全局默认样式

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupported,
  createImgPool,
  createSrcTplOfAliOss,
  Img,
  ImgPoolContext,
} from '@robot-img/react-img'

const Container = styled.div`
  .div {
    width: 300px;
    height: 240px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .content {
    width: 160px;
    height: 100px;
    background-color: rgba(255, 255, 255, 0.5);
    text-align: center;
    line-height: 80px;
    padding: 10px;
  }
  .span {
    width: 50px;
    height: 30px;
    margin-right: 10px;
  }
  .img {
    width: 50px;
    height: 30px;
  }
`

async function main() {
  // 判断浏览器是否支持 webp 格式图片
  const webp = await checkWebpSupported()
  // 还是以阿里云为例
  const imgPool = createImgPool({
    createSrcTpl: createSrcTplOfAliOss({
      webp,
    }),
    globalVars: {
      className: 'css-style-img',
    },
  })
  // 可以用这个方法加一个默认样式
  /**
   * 根据 globalVars.className 设置一个全局默认样式
   * 默认样式为:
   * ```
   * .${globalVars.className} {
   *  transition: background-image .3s;
   *  background-size: cover;
   *  background-position: center;
   *  background-repeat: no-repeat;
   * }
   * span.${globalVars.className} {
   *  display: inline-block;
   * }
   * ```
   */
  imgPool.appendDefaultStyle()
  const imgSrc = '//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg'
  // 真实加载的图片为: ${imgSrc}?x-oss-process=image/resize,m_fill,w_400,h_320/format,webp
  ReactDOM.render(
    <Container>
      <ImgPoolContext.Provider value={imgPool}>
        <Img.Div src={imgSrc} className="div">
          <div className="content">
            <Img.Span src={imgSrc} className="span" />
            <Img src={imgSrc} className="img" />
          </div>
        </Img.Div>
      </ImgPoolContext.Provider>
    </Container>,
    document.getElementById('30-css')
  )
}

main()

使用不同云厂商的图片

使用不同云厂商的图片处理功能按需使用图片,已有图片处理函数参考:

也可以通过 createSrcTplFactory 快速创建一个全局图片处理函数

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupportedSync,
  createImgPool,
  createSrcTplFactory,
  createSrcTplOfAliOss,
  createSrcTplOfKSYunKS3,
  createSrcTplOfTencent,
  Img,
  ImgPoolContext,
} from '@robot-img/react-img'

const Container = styled.div`
  .cloud-img {
    width: 100px;
    height: 80px;
    margin-bottom: 10px;
    margin-right: 10px;
  }
`

function main() {
  // 判断浏览器是否支持 webp 格式图片
  const webp = checkWebpSupportedSync()
  // 阿里云,参考:https://help.aliyun.com/document_detail/44688.html
  const imgPoolAliOss = createImgPool({
    createSrcTpl: createSrcTplOfAliOss({
      webp,
    }),
    globalVars: {
      className: 'cloud-img',
    },
  })
  // 金山云,详见:https://docs.ksyun.com/documents/886
  const imgPoolK3S = createImgPool({
    createSrcTpl: createSrcTplOfKSYunKS3({
      webp,
    }),
    globalVars: {
      className: 'cloud-img',
    },
  })
  // 腾讯云,详见:https://cloud.tencent.com/document/product/460/36541
  const imgPoolTencent = createImgPool({
    createSrcTpl: createSrcTplOfTencent({
      webp,
    }),
    globalVars: {
      className: 'cloud-img',
    },
  })
  // 自定义,以腾讯云为基础,比如自定义 webp 格式的质量
  const createCustomSrcTpl = createSrcTplFactory((globalVars) => ({ rect, src }) => {
    const configs: string[] = []
    if (rect.width && rect.height) {
      const w = Math.floor(globalVars.ratio * rect.width)
      const h = Math.floor(globalVars.ratio * rect.height)
      configs.push(`1/w/${w}/h/${h}`)
    }
    if (globalVars.webp) {
      configs.push('format/webp/quality/90')
    }
    if (configs.length < 1) {
      return src
    }

    return `${src}?imageView2/${configs.join('/')}`
  })
  const imgPoolCustom = createImgPool({
    createSrcTpl: createCustomSrcTpl({
      webp,
    }),
    globalVars: {
      className: 'cloud-img',
    },
  })
  ReactDOM.render(
    <Container>
      <ImgPoolContext.Provider value={imgPoolAliOss}>
        <Img src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
      </ImgPoolContext.Provider>
      <ImgPoolContext.Provider value={imgPoolK3S}>
        <Img src="//ks3-cn-beijing.ksyun.com/ks3-resources/suiyi.jpg" />
      </ImgPoolContext.Provider>
      <ImgPoolContext.Provider value={imgPoolTencent}>
        <Img src="//examples-1251000004.cos.ap-shanghai.myqcloud.com/sample.jpeg" />
      </ImgPoolContext.Provider>
      <ImgPoolContext.Provider value={imgPoolCustom}>
        <Img src="//examples-1251000004.cos.ap-shanghai.myqcloud.com/sample.jpeg" />
      </ImgPoolContext.Provider>
    </Container>,
    document.getElementById('40-cloud')
  )
}

main()

单组件自定义图片处理

云厂商可用的图片处理不仅仅只有缩放,也可以做一些特殊处理,比如缩放模式、高斯模糊。

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupportedSync,
  createImgPool,
  createSrcTplOfAliOss,
  Img,
  ImgPoolContext,
} from '@robot-img/react-img'

const Container = styled.div`
  .ali-oss-img {
    width: 200px;
    height: 160px;
    background-size: contain;
    background-color: #ccc;
    background-repeat: no-repeat;
    background-position: center;
    transition: background-image 0.3s;
  }
`

function main() {
  // 阿里云,参考:https://help.aliyun.com/document_detail/44688.html
  const imgPoolAliOss = createImgPool({
    createSrcTpl: createSrcTplOfAliOss({
      webp: checkWebpSupportedSync(),
    }),
    globalVars: {
      className: 'ali-oss-img',
    },
  })

  ReactDOM.render(
    <Container>
      <ImgPoolContext.Provider value={imgPoolAliOss}>
        <Img.Div
          src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg"
          srcTpl={({ src, ratioWidth, ratioHeight, webp }) =>
            `${src}?x-oss-process=image/resize,m_lfit,w_${ratioWidth},h_${ratioHeight}${
              webp ? '/format,webp' : ''
            }/blur,r_3,s_2`
          }
        />
      </ImgPoolContext.Provider>
    </Container>,
    document.getElementById('45-tpl')
  )
}

main()

自定义图片容器

使用 ImgContainer 组件自定义图片容器

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import { Img, ImgContainer, imgPool } from '@robot-img/react-img'
import { checkWebpSupportedSync, createSrcTplOfAliOss } from '@robot-img/utils'

const Container = styled.div`
  .img-container {
    height: 300px;
    overflow: scroll;
  }
  .img {
    width: 200px;
    height: 160px;
    background-size: cover;
    margin-bottom: 10px;
  }
`

async function main() {
  const webp = checkWebpSupportedSync()
  imgPool.reset({
    createSrcTpl: createSrcTplOfAliOss({
      webp,
    }),
    globalVars: {
      className: 'robot-img',
    },
  })
  ReactDOM.render(
    <Container>
      <ImgContainer
        className="img-container"
        createSrcTpl={createSrcTplOfAliOss({
          webp,
        })}
        globalVars={{ className: 'robot-img' }}
      >
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
        <Img.Div className="img" src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg" />
      </ImgContainer>
    </Container>,
    document.getElementById('50-container')
  )
}

main()

自适应处理图片

图片组件会根据图片的面积自动适应,默认的判断方法:

function defaultShouldUpdate(newRect: DOMRect, oldRect: DOMRect) {
  // 当 width or height 变大 20% 时,才更新图片
  return newRect.width > oldRect.width * 1.2 || newRect.height > oldRect.height * 1.2
}

也可以通过给组件传 shouldUpdate 来定义图片是否需要更新,也可以通过

imgPool.reset({
  globalVars: { shouldUpdate: (newRect, oldRect) => true }
})

设置全家默认的 shouldUpdate

当前的 src: $

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupportedSync,
  createSrcTplOfAliOss,
  imgPool,
  ImgProps,
  useImg,
} from '@robot-img/react-img'

const Container = styled.div`
  .robot-img {
    background-size: cover;
    background-position: center;
    transition: background-image 0.3s;
    margin: 20px 0;
  }
  button {
    margin-right: 10px;
    user-select: none;
  }
`

const StyledImg = styled.div<{ $src: string }>`
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  transition: background-image 0.3s;
  ${(props) => props.$src && `background-image: url(${props.$src});`}
`

// 使用 useImg 自定义组件
const Img = React.forwardRef<HTMLDivElement, ImgProps<HTMLDivElement>>((props, ref) => {
  const { domProps, state, handleRef } = useImg(props, ref)
  return (
    <div>
      <StyledImg {...domProps} $src={state.src} ref={handleRef} />
      <p>当前的 src: ${state.src}</p>
    </div>
  )
})

function App() {
  const [ratio, setRatio] = React.useState(1)
  const handleAdd = React.useCallback(() => {
    setRatio((oldRatio) => Math.min(3, oldRatio * 1.05))
  }, [])
  const handleCut = React.useCallback(() => {
    setRatio((oldRatio) => oldRatio * 0.95)
  }, [])
  const imgStyle = React.useMemo(
    () => ({
      width: 100 * ratio,
      height: 60 * ratio,
    }),
    [ratio]
  )

  return (
    <Container>
      <button onClick={handleAdd}>宽高变大10%</button>
      <button onClick={handleCut}>宽高变小10%</button>
      <Img
        style={imgStyle}
        lazy="resize"
        src="//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg"
      />
    </Container>
  )
}

function main() {
  const webp = checkWebpSupportedSync()
  imgPool.reset({
    createSrcTpl: createSrcTplOfAliOss({
      webp,
    }),
    globalVars: {
      className: 'robot-img',
    },
  })
  ReactDOM.render(<App />, document.getElementById('60-resize'))
}

main()

全局默认设置

图片组件的属性 defaultSrcerrorSrcstatusClassNamePrefixloadingType 都可以通过 imgPool.globalVars 来设置。

注意: defaultSrcerrorSrc 不会进行图片处理功能

import React from 'react'
import ReactDOM from 'react-dom'

import styled from '@emotion/styled'
import {
  checkWebpSupportedSync,
  createImgPool,
  createSrcTplOfAliOss,
  Img,
  ImgPoolContext,
  waitImgLoaded,
} from '@robot-img/react-img'

const Container = styled.div`
  .ali-oss-img {
    width: 200px;
    height: 160px;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    transition: background-image 0.3s;
    border: 1px solid #ccc;

    &-loading {
      border-color: blue;
      background-size: 64px 64px;
    }
    &-loaded {
      border-color: green;
    }
    &-error {
      border-color: red;
      background-size: 64px 64px;
    }
  }
  button {
    margin-top: 10px;
    margin-right: 10px;
    user-select: none;
  }
`

function Demo() {
  const resolve = React.useRef<(args?: any) => void>(() => {})
  const reject = React.useRef<(args?: any) => void>(() => {})
  const wait = React.useCallback(
    () =>
      new Promise((r, rj) => {
        resolve.current = r
        reject.current = rj
      }),
    []
  )
  const prepareImg: typeof waitImgLoaded = React.useMemo(() => {
    return async (imgSrc, crossOrigin) => {
      const img = await waitImgLoaded(imgSrc, crossOrigin)
      await wait()
      return img
    }
  }, [wait])
  const handleResolve = React.useCallback(() => {
    resolve.current()
  }, [])
  const handleReject = React.useCallback(() => {
    reject.current()
  }, [])
  const [src, setSrc] = React.useState('')
  const handleSrc = React.useCallback(() => {
    setSrc('//image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg')
  }, [])
  const handleClear = React.useCallback(() => {
    setSrc('')
  }, [])
  return (
    <div>
      <Img.Div src={src} prepareImg={prepareImg} />
      <button onClick={src ? handleClear : handleSrc}>{src ? 'Clear img' : 'Load img'}</button>
      <button onClick={handleResolve}>Mock success</button>
      <button onClick={handleReject}>Mock reject</button>
    </div>
  )
}

async function main() {
  // 阿里云,参考:https://help.aliyun.com/document_detail/44688.html
  const imgPoolAliOss = createImgPool({
    createSrcTpl: createSrcTplOfAliOss({
      webp: checkWebpSupportedSync(),
    }),
    globalVars: {
      className: 'ali-oss-img',
      statusClassNamePrefix: 'ali-oss-img-',
      defaultSrc: './imgs/picture.png',
      errorSrc: './imgs/error.png',
      loadingType: 'src',
    },
  })

  ReactDOM.render(
    <Container>
      <ImgPoolContext.Provider value={imgPoolAliOss}>
        <Demo />
      </ImgPoolContext.Provider>
    </Container>,
    document.getElementById('70-globals')
  )
}

main()