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 Img
This 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/