Let’s face it, image optimization is hard. We want to make it effortless.
面对现实吧,图像优化非常困难。 我们希望毫不费力。
When we set out to build our React Component there were a few problems we wanted to solve:
当我们开始构建React组件时,我们要解决一些问题:
- Automatically decide image width for any device based on the parent container. - 根据父容器自动确定任何设备的图像宽度。 
- Use the best possible image format the user’s browser supports. - 使用用户浏览器支持的最佳图像格式。 
- Automatic image lazy loading. - 自动图像延迟加载。 
- Automatic low-quality image placeholders (LQIP). - 自动低质量图像占位符(LQIP)。 
Oh, and it had to be effortless for React Developers to use.
哦,React开发人员必须毫不费力地使用它。
这是我们想出的: (This is what we came up with:)
<Img src={ tueriImageId } alt='Alt Text' />Easy right? Let’s dive in.
容易吧? 让我们潜入。
计算图像尺寸: (Calculating the image size:)
Create a <figure /> element, detect the width and build an image URL:
 创建一个<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.clientWidththis.setState({width})}render() {// Destructure props and stateconst { src, alt, options = {}, ext = 'jpg' } = this.propsconst { width } = this.state// Create an empty query stringlet queryString = ''        // If width is specified, otherwise use auto-detected widthoptions['w'] = options['w'] || width// Loop through option object and build queryStringObject.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 nullwidth > 0 ? (<imgsrc={`https://cdn.tueri.io/${ src }/${ kebabCase(alt) }.${ ext }${ queryString }`}alt={ alt }/>) : null }</figure>)}
}export default ImgThis returns the following HTML:
这将返回以下HTML:
<figure><img src="https://cdn.tueri.io/tueriImageId/alt-text.jpg?w=autoCalculatedWidth" alt="Alt Text" />
</figure>使用最佳图像格式: (Use the best possible image format:)
Next, we needed to add support for detecting WebP images and having the Tueri service return the image in the WebP format:
接下来,我们需要添加支持以检测WebP图像并使Tueri服务以WebP格式返回图像:
class Img extends React.Component {constructor(props) {// ...this.window = typeof window !== 'undefined' && windowthis.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 URLif (!options['fm'] && this.isWebpSupported) {options['fm'] = 'webp'}// ...return (// ...)}
}// ...This returns the following HTML:
这将返回以下HTML:
<figure><img src="https://cdn.tueri.io/tueriImageId/alt-text.jpg?w=autoCalculatedWidth&fm=webp" alt="Alt Text" />
</figure>自动图像延迟加载: (Automatic image lazy loading:)
Now, we need to find out if the <figure /> element is in the viewport, plus we add a little buffer area so the images load just before being scrolled into view.
 现在,我们需要确定<figure />元素是否在视口中,此外,我们还要添加一些缓冲区,以便在滚动到视图之前加载图像。 
class Img extends React.Component {constructor(props) {// ...this.state = {// ...isInViewport: falselqipLoaded: 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 loadedif (this.imgRef.current && !this.state.lqipLoaded) {// Get the viewport heightconst windowHeight = this.window.innerHeight// Get the top position of the <figure /> elementconst 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 viewportif (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 nullisInViewport && width > 0 ? (<img onLoad={ () => { this.setState({ lqipLoaded: true }) } }// .../>) : null }</figure>)}
}export default Img自动低质量图像占位符(LQIP): (Automatic low-quality image placeholders (LQIP):)
Finally, when an image is in the viewport, we want to load a 1/10 size blurred image, then fade out the placeholder image when the full-size image is loaded:
最后,当图像在视口中时,我们要加载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 fullsizeconst 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 /> elementconst 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 LQIPif (fullsizeLoaded) {styles.lqip.opacity = 0}return(<figurestyle={ 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>)}
}// ...放在一起: (Putting it all together:)
Image optimization made effortless. Just swap out your regular <img /> elements for the Tueri <Img /> and never worry about optimization again.
 图像优化毫不费力。 只需将您的常规<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.clientWidththis.setState({width})this.handleViewport()this.window.addEventListener('scroll', this.handleViewport)}handleViewport() {if (this.imgRef.current && !this.state.lqipLoaded) {const windowHeight = this.window.innerHeightconst imageTopPosition = this.imgRef.current.getBoundingClientRect().topconst buffer = typeof this.props.buffer === 'number' && this.props.buffer > 1 && this.props.buffer < 10 ? this.props.buffer : 1.5if (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 stateconst { src, alt, options = {}, ext = 'jpg' } = this.propsconst { isInViewport, width, fullsizeLoaded } = this.state// Create an empty query stringlet queryString = ''// If width is specified, otherwise use auto-detected widthoptions['w'] = options['w'] || width// If a format has not been specified, detect webp supportif (!options['fm'] && this.isWebpSupported) {options['fm'] = 'webp'}// Loop through option prop and build queryStringObject.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 fullsizeconst 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 LQIPif (fullsizeLoaded) {styles.lqip.opacity = 0}const missingALt = 'ALT TEXT IS REQUIRED'return(// Return the CDN domain from the TueriProvider<TueriContext.Consumer>{({ domain }) => (<figurestyle={ 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实际观看: (See it in action:)
Try out a live demo on CodeSandbox:
在CodeSandbox上进行现场演示:
Originally published at Tueri.io
最初发表于Tueri.io
翻译自: https://www.freecodecamp.org/news/building-the-react-image-optimization-component-for-tueri-io/