Skip to content

Commit

Permalink
feat(images): implement ideal-image-loader
Browse files Browse the repository at this point in the history
  • Loading branch information
adrienharnay committed Nov 22, 2018
1 parent b5655df commit 3545b3a
Show file tree
Hide file tree
Showing 24 changed files with 932 additions and 635 deletions.
57 changes: 25 additions & 32 deletions README.md
Expand Up @@ -4,6 +4,10 @@ This starter pack is designed to be a simple solution to start a React project w

## Changelog

### 2.1

- Replace native `<img />` with `@brigad/ideal-image-loader`

### 2.0

- Webpack 4
Expand Down Expand Up @@ -306,39 +310,32 @@ As images are not crucial to the page and can be loaded when the client receives
```js
const rules = [
{
test: /.*\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)$/i,
test: /\.(jpe?g|png|svg|gif)$/i,
include,
exclude,
use: [
{
loader: 'url-loader',
loader: '@brigad/ideal-image-loader',
options: {
name: 'images/[name].[hash].[ext]',
limit: 1,
base64: IS_PRODUCTION,
svgoCleanUpIds: IS_PRODUCTION,
webp: IS_PRODUCTION ? undefined : false,
warnOnMissingSrcset: !IS_PRODUCTION,
},
},
({ resource }) => ({
loader: 'image-webpack-loader',
],
},
{
test: /\.(jpe?g|png|svg|gif)$/i,
include: exclude,
use: [
{
loader: 'file-loader',
options: {
bypassOnDebug: true,
mozjpeg: {
quality: 90,
},
pngquant: {
quality: '90-95',
speed: 1,
},
svgo: {
plugins: [
{
cleanupIDs: {
prefix: hash(path.relative(__dirname, resource)),
minify: true,
remove: true,
},
},
],
},
name: 'images/[name].[hash].[ext]',
},
}),
},
],
},
];
Expand All @@ -348,13 +345,9 @@ const rules = [

_For this rule, use the same config on the server and on the client, except for `emitFile: false` on the server_

Thanks to [url-loader](https://github.com/webpack-contrib/url-loader), all images will be emitted and the browser will load them.

_Did you say compression?_

Yes! While we're at it, we will use [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader) which provides a way to compress images at build time, so we can ensure our users only download the most optimized content.
Thanks to [file-loader](https://github.com/webpack-contrib/file-loader), all images will be emitted and the browser will load them.

And voila! Images are generated by the client build, and ignored by the server build (because the output would be the same).
And voilà! Images are generated by the client build, and ignored by the server build (because the output would be the same).

### Accessing them from a CDN

Expand Down Expand Up @@ -413,7 +406,7 @@ For every asset, we will want to have a hash based on its content, so that if ev
```js
const rules = {
{
loader: 'url-loader',
loader: 'file-loader',
options: {
name: 'images/[name].[hash].[ext]',
},
Expand Down
8 changes: 3 additions & 5 deletions package.json
Expand Up @@ -13,6 +13,7 @@
"compression": "1.7.3",
"consolidate": "0.15.1",
"express": "4.16.4",
"gatsby-image": "2.0.20",
"handlebars": "4.0.12",
"prop-types": "15.6.2",
"react": "16.6.3",
Expand All @@ -29,15 +30,14 @@
"@babel/preset-env": "7.1.6",
"@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.1.0",
"@brigad/ideal-image-loader": "1.0.0",
"autoprefixer": "9.3.1",
"babel-core": "7.0.0-bridge.0",
"babel-loader": "8.0.4",
"circular-dependency-plugin": "5.0.2",
"cross-env": "5.2.0",
"css-loader": "1.0.1",
"fast-stable-stringify": "1.0.0",
"file-loader": "2.0.0",
"image-webpack-loader": "3.4.2",
"mini-css-extract-plugin": "0.4.4",
"node-sass": "4.10.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
Expand All @@ -46,7 +46,6 @@
"sass-loader": "7.1.0",
"terser-webpack-plugin": "1.1.0",
"typescript": "3.1.6",
"url-loader": "1.1.2",
"webpack": "4.26.0",
"webpack-bundle-analyzer": "3.0.3",
"webpack-cli": "3.1.2",
Expand All @@ -57,8 +56,7 @@
"webpack-merge": "4.1.4",
"webpack-node-externals": "1.7.2",
"webpack-notifier": "1.7.0",
"webpackbar": "2.6.3",
"xxhashjs": "0.2.2"
"webpackbar": "2.6.3"
},
"scripts": {
"build": "cross-env NODE_ENV=production ASSETS_URL=https://your-cdn.com/ webpack --config webpack.config.js",
Expand Down
106 changes: 106 additions & 0 deletions src/components/image/js/Image.js
@@ -0,0 +1,106 @@
import GatsbyImage from 'gatsby-image';
import PropTypes from 'prop-types';
import React from 'react';

const Image = ({ src, alt, height, width, className, fluid }) => {
const x1 = src && src.x1 ? src.x1 : src;
const x2 = src && src.x2 ? src.x2 : null;
const x3 = src && src.x3 ? src.x3 : null;

const base64 = x1 && x1.preSrc ? x1.preSrc : '';

const src1 = x1 && x1.src ? x1.src : x1;
const src2 = x2 && x2.src ? x2.src : '';
const src3 = x3 && x3.src ? x3.src : '';
const webp1 = x1 && x1.webp ? x1.webp : '';
const webp2 = x2 && x2.webp ? x2.webp : '';
const webp3 = x3 && x3.webp ? x3.webp : '';

const src1FormattedFluid = src1 ? `${src1} ${width}w` : '';
const src2FormattedFluid = src2 ? `${src2} ${width * 2}w` : '';
const src3FormattedFluid = src3 ? `${src3} ${width * 3}w` : '';
const webp1FormattedFluid = webp1 ? `${webp1} ${width}w` : '';
const webp2FormattedFluid = webp2 ? `${webp2} ${width * 2}w` : '';
const webp3FormattedFluid = webp3 ? `${webp3} ${width * 3}w` : '';

const src1FormattedFixed = src1;
const src2FormattedFixed = src2 ? `${src2} 2x` : '';
const src3FormattedFixed = src3 ? `${src3} 3x` : '';
const webp1FormattedFixed = webp1;
const webp2FormattedFixed = webp2 ? `${webp2} 2x` : '';
const webp3FormattedFixed = webp3 ? `${webp3} 3x` : '';

const srcSet = (fluid
? [src1FormattedFluid, src2FormattedFluid, src3FormattedFluid]
: [src1FormattedFixed, src2FormattedFixed, src3FormattedFixed]
)
.filter(Boolean)
.join(', ');
const srcSetWebp = (fluid
? [webp1FormattedFluid, webp2FormattedFluid, webp3FormattedFluid]
: [webp1FormattedFixed, webp2FormattedFixed, webp3FormattedFixed]
)
.filter(Boolean)
.join(', ');

const imgObj = {
...(fluid
? {
aspectRatio: width / height,
sizes: `(max-width: ${width * 3}px) 100vw, ${width * 3}px`,
}
: {
height,
width,
}),
base64,
src: src2 || src1,
srcWebp: webp2 || webp1,
srcSet,
srcSetWebp,
};

return (
<GatsbyImage
fluid={fluid ? imgObj : null}
fixed={!fluid ? imgObj : null}
alt={alt}
className={className}
/>
);
};

Image.propTypes = {
src: PropTypes.oneOfType([
PropTypes.shape({
x1: PropTypes.shape({
src: PropTypes.string.isRequired,
preSrc: PropTypes.string,
webp: PropTypes.string,
}).isRequired,
x2: PropTypes.shape({
src: PropTypes.string.isRequired,
preSrc: PropTypes.string,
webp: PropTypes.string,
}).isRequired,
x3: PropTypes.shape({
src: PropTypes.string.isRequired,
preSrc: PropTypes.string,
webp: PropTypes.string,
}).isRequired,
}),
PropTypes.string,
]).isRequired,
alt: PropTypes.string.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
className: PropTypes.string,
fluid: PropTypes.bool,
};

Image.defaultProps = {
className: '',
fluid: false,
};

export default Image;
40 changes: 26 additions & 14 deletions src/views/home/css/Home.scss
@@ -1,20 +1,32 @@
.cover {
color: white;
background: no-repeat center bottom;
background-size: cover;
background-image: url('../img/cover.jpg');
.container {
position: relative;
height: 100vh;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;

> div {
margin-bottom: 10px;
.cover {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute !important;
height: 100% !important;
width: 100% !important;

> img {
height: 100% !important;
object-fit: cover;
}
}

.logo {
height: 100px;
.content {
position: absolute;
height: 100%;
width: 100%;
text-align: center;
color: white;

.title {
margin: 20% 0 10px;
text-shadow: 1px 0px 1px #CCCCCC, 0px 1px 1px #EEEEEE, 2px 1px 1px #CCCCCC, 1px 2px 1px #EEEEEE, 3px 2px 1px #CCCCCC, 2px 3px 1px #EEEEEE, 4px 3px 1px #CCCCCC, 3px 4px 1px #EEEEEE, 5px 4px 1px #CCCCCC, 4px 5px 1px #EEEEEE, 6px 5px 1px #CCCCCC, 5px 6px 1px #EEEEEE, 7px 6px 1px #CCCCCC;
}
}
}
Binary file modified src/views/home/img/cover.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/home/img/cover@2x.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/home/img/cover@3x.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/views/home/img/react-logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/home/img/react-logo@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/home/img/react-logo@3x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 25 additions & 12 deletions src/views/home/js/Home.js
@@ -1,24 +1,37 @@
import React from 'react';
import { Link } from 'react-router-dom';

import Image from 'src/components/image/js/Image';

import styles from '../css/Home.scss';

import Cover from '../img/cover.jpg';
import ReactLogoImage from '../img/react-logo.png';

const Home = () => {
return (
<div className={styles.cover}>
<div>
{'Home sweet home!'}
</div>
<div>
<img src={ReactLogoImage} alt="react_logo" className={styles.logo} />
</div>
<div>
<Link to="/page1">{'Page1'}</Link>
</div>
<div>
<Link to="/page2">{'Page2'}</Link>
<div className={styles.container}>
<Image
src={Cover}
alt="cover"
height={693}
width={1280}
className={styles.cover}
/>
<div className={styles.content}>
<h1 className={styles.title}>{'Home sweet home!'}</h1>
<div>
<Image
src={ReactLogoImage}
alt="react_logo"
height={100}
width={100}
/>
</div>
<div>
<Link to="/page1">{'Page 1'}</Link>{' '}
<Link to="/page2">{'Page 2'}</Link>
</div>
</div>
</div>
);
Expand Down
40 changes: 28 additions & 12 deletions src/views/page1/css/Page1.scss
@@ -1,16 +1,32 @@
.cover {
color: white;
background: no-repeat center bottom;
background-size: cover;
background-image: url('../img/cover.jpg');
.container {
position: relative;
height: 100vh;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;

> div {
margin-bottom: 10px;
.cover {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute !important;
height: 100% !important;
width: 100% !important;

> img {
height: 100% !important;
object-fit: cover;
}
}

.content {
position: absolute;
height: 100%;
width: 100%;
text-align: center;
color: white;

.title {
margin: 20% 0 10px;
text-shadow: 0 -1px 4px #FFF, 0 -2px 10px #ff0, 0 -10px 20px #ff8000, 0 -18px 40px #F00;
}
}
}
Binary file modified src/views/page1/img/cover.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/page1/img/cover@2x.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/views/page1/img/cover@3x.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3545b3a

Please sign in to comment.