发布于:2021-02-03 09:30:20
0
41
0
为Tueri.io构建React Image Optimization组件
面对现实吧,图像优化很难。我们想让它变得轻松。
当我们着手构建React组件时,有几个问题需要解决:
根据父容器自动决定任何设备的图像宽度。
使用用户浏览器支持的最佳图像格式。
自动图像延迟加载。
自动低质量图像占位符(LQIP)。
哦,React开发人员必须毫不费力地使用它。
我们得出的结论是:
<Img src={ tueriImageId } alt='Alt Text' />
很简单吧?我们接着看下去。
计算图像尺寸
创建一个<figure />元素,检测宽度并构建图像URL:
class Img extends React.Component {
constructor(props) {
super(props)
this.state = {
width: 0
}
this.imgRef = React.createRef()
}
componentDidMount() {
const width = this.imgRef.current.clientWidth
this.setState({
width
})
}
render() {
// Destructure props and state
const { src, alt, options = {}, ext = 'jpg' } = this.props
const { width } = this.state
// Create an empty query string
let queryString = ''
// If width is specified, otherwise use auto-detected width
options['w'] = options['w'] || width
// Loop through option object and build queryString
Object.keys(options).map((option, i) => {
return queryString += `${i < 1 ? '?' : '&'}${option}=${options[option]}`
})
return(
<figure ref={this.imgRef}>
{
// If the container width has been set, display the image else null
width > 0 ? (
<img
src={`https://cdn.tueri.io/${ src }/${ kebabCase(alt) }.${ ext }${ queryString }`}
alt={ alt }
/>
) : null
}
</figure>
)
}
}
export default Img
这将返回以下HTML:
<figure>
<img
src="https://cdn.tueri.io/tueriImageId/alt-text.jpg?w=autoCalculatedWidth"
alt="Alt Text"
/>
</figure>
使用最佳图像格式
接下来,我们需要添加对检测WebP图像和让Tueri服务以WebP格式返回图像的支持:
class Img extends React.Component {
constructor(props) {
// ...
this.window = typeof window !== 'undefined' && window
this.isWebpSupported = this.isWebpSupported.bind(this)
}
// ...
isWebpSupported() {
if (!this.window.createImageBitmap) {
return false;
}
return true;
}
render() {
// ...
// If a format has not been specified, detect webp support
// Set the fm (format) option in the image URL
if (!options['fm'] && this.isWebpSupported) {
options['fm'] = 'webp'
}
// ...
return (
// ...
)
}
}
// ...
这将返回以下HTML:
<figure>
<img
src="https://cdn.tueri.io/tueriImageId/alt-text.jpg?w=autoCalculatedWidth&fm=webp"
alt="Alt Text"
/>
</figure>
自动图像延迟加载
现在,我们需要找出<figure />元素是否在视口中,再加上我们添加了一个小缓冲区,以便在滚动到视图之前加载图像。
class Img extends React.Component {
constructor(props) {
// ...
this.state = {
// ...
isInViewport: false
lqipLoaded: false
}
// ...
this.handleViewport = this.handleViewport.bind(this)
}
componentDidMount() {
// ...
this.handleViewport()
this.window.addEventListener('scroll', this.handleViewport)
}
handleViewport() {
// Only run if the image has not already been loaded
if (this.imgRef.current && !this.state.lqipLoaded) {
// Get the viewport height
const windowHeight = this.window.innerHeight
// Get the top position of the <figure /> element
const imageTopPosition = this.imgRef.current.getBoundingClientRect().top
// Multiply the viewport * buffer (default buffer: 1.5)
const buffer = typeof this.props.buffer === 'number' && this.props.buffer > 1 && this.props.buffer < 10 ? this.props.buffer : 1.5
// If <figure /> is in viewport
if (windowHeight * buffer > imageTopPosition) {
this.setState({
isInViewport: true
})
}
}
}
// ...
componentWillUnmount() {
this.window.removeEventListener('scroll', this.handleViewport)
}
render() {
// Destructure props and state
// ...
const { isInViewport, width } = this.state
// ...
return (
<figure ref={this.imgRef}>
{
// If the container width has been set, display the image else null
isInViewport && width > 0 ? (
<img
onLoad={ () => { this.setState({ lqipLoaded: true }) } }
// ...
/>
) : null
}
</figure>
)
}
}
export default Img
自动低质量图像占位符(LQIP)
最后,当图像在视口中时,我们希望加载1/10大小的模糊图像,然后在加载全尺寸图像时淡出占位符图像:
class Img extends React.Component {
constructor(props) {
// ...
this.state = {
// ...
fullsizeLoaded: false
}
// ...
}
// ...
render() {
// Destructure props and state
// ...
const { isInViewport, width, fullsizeLoaded } = this.state
// ...
// Modify the queryString for the LQIP image: replace the width param with a value 1/10 the fullsize
const lqipQueryString = queryString.replace(`w=${ width }`, `w=${ Math.round(width * 0.1) }`)
// Set the default styles. The full size image should be absolutely positioned within the <figure /> element
const styles = {
figure: {
position: 'relative',
margin: 0
},
lqip: {
width: '100%',
filter: 'blur(5px)',
opacity: 1,
transition: 'all 0.5s ease-in'
},
fullsize: {
position: 'absolute',
top: '0px',
left: '0px',
transition: 'all 0.5s ease-in'
}
}
// When the fullsize image is loaded, fade out the LQIP
if (fullsizeLoaded) {
styles.lqip.opacity = 0
}
return(
<figure
style={ styles.figure }
// ...
>
{
isInViewport && width > 0 ? (
<React.Fragment>
{/* Load fullsize image in background */}
<img
onLoad={ () => { this.setState({ fullsizeLoaded: true }) } }
style={ styles.fullsize }
src={`https://cdn.tueri.io/${ src }/${ kebabCase(alt) }.${ ext }${ queryString }`}
alt={ alt }
/>
{/* Load LQIP in foreground */}
<img
onLoad={ () => { this.setState({ lqipLoaded: true }) } }
style={ styles.lqip }
src={`https://cdn.tueri.io/${ src }/${ kebabCase(alt) }.${ ext }${ lqipQueryString }`}
alt={ alt }
/>
</React.Fragment>
) : null
}
</figure>
)
}
}
// ...
综合起来
图像优化毫不费力。只需将您的常规<img />元素换成Tueri <Img />,再也不用担心优化。
import React from 'react'
import PropTypes from 'prop-types'
import { TueriContext } from './Provider'
import kebabCase from 'lodash.kebabcase'
class Img extends React.Component {
constructor(props) {
super(props)
this.state = {
isInViewport: false,
width: 0,
height: 0,
lqipLoaded: false,
fullsizeLoaded: false
}
this.imgRef = React.createRef()
this.window = typeof window !== 'undefined' && window
this.handleViewport = this.handleViewport.bind(this)
this.isWebpSupported = this.isWebpSupported.bind(this)
}
componentDidMount() {
const width = this.imgRef.current.clientWidth
this.setState({
width
})
this.handleViewport()
this.window.addEventListener('scroll', this.handleViewport)
}
handleViewport() {
if (this.imgRef.current && !this.state.lqipLoaded) {
const windowHeight = this.window.innerHeight
const imageTopPosition = this.imgRef.current.getBoundingClientRect().top
const buffer = typeof this.props.buffer === 'number' && this.props.buffer > 1 && this.props.buffer < 10 ? this.props.buffer : 1.5
if (windowHeight * buffer > imageTopPosition) {
this.setState({
isInViewport: true
})
}
}
}
isWebpSupported() {
if (!this.window.createImageBitmap) {
return false;
}
return true;
}
componentWillUnmount() {
this.window.removeEventListener('scroll', this.handleViewport)
}
render() {
// Destructure props and state
const { src, alt, options = {}, ext = 'jpg' } = this.props
const { isInViewport, width, fullsizeLoaded } = this.state
// Create an empty query string
let queryString = ''
// If width is specified, otherwise use auto-detected width
options['w'] = options['w'] || width
// If a format has not been specified, detect webp support
if (!options['fm'] && this.isWebpSupported) {
options['fm'] = 'webp'
}
// Loop through option prop and build queryString
Object.keys(options).map((option, i) => {
return queryString += `${i < 1 ? '?' : '&'}${option}=${options[option]}`
})
// Modify the queryString for the LQIP image: replace the width param with a value 1/10 the fullsize
const lqipQueryString = queryString.replace(`w=${ width }`, `w=${ Math.round(width * 0.1) }`)
const styles = {
figure: {
position: 'relative',
margin: 0
},
lqip: {
width: '100%',
filter: 'blur(5px)',
opacity: 1,
transition: 'all 0.5s ease-in'
},
fullsize: {
position: 'absolute',
top: '0px',
left: '0px',
transition: 'all 0.5s ease-in'
}
}
// When the fullsize image is loaded, fade out the LQIP
if (fullsizeLoaded) {
styles.lqip.opacity = 0
}
const missingALt = 'ALT TEXT IS REQUIRED'
return(
// Return the CDN domain from the TueriProvider
<TueriContext.Consumer>
{({ domain }) => (
<figure
style={ styles.figure }
ref={this.imgRef}
>
{
//
isInViewport && width > 0 ? (
<React.Fragment>
{/* Load fullsize image in background */}
<img
onLoad={ () => { this.setState({ fullsizeLoaded: true }) } }
style={ styles.fullsize }
src={`${ domain }/${ src }/${ kebabCase(alt || missingALt) }.${ ext }${ queryString }`}
alt={ alt || missingALt }
/>
{/* Load LQIP in foreground */}
<img
onLoad={ () => { this.setState({ lqipLoaded: true }) } }
style={ styles.lqip }
src={`${ domain }/${ src }/${ kebabCase(alt || missingALt) }.${ ext }${ lqipQueryString }`}
alt={ alt || missingALt }
/>
</React.Fragment>
) : null
}
</figure>
)}
</TueriContext.Consumer>
)
}
}
Img.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
options: PropTypes.object,
ext: PropTypes.string,
buffer: PropTypes.number
}
export default Img
作者介绍