Skip to content

Commit

Permalink
Implement svg icons lazy loading.
Browse files Browse the repository at this point in the history
  • Loading branch information
yurayarosh committed Sep 7, 2021
1 parent 4d73bd0 commit 613f4b6
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 74 deletions.
7 changes: 6 additions & 1 deletion generators/app/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ module.exports = [
name: 'sprites',
message: 'How will we handle icons?',
choices: [
{
name: 'Lazy loaded inline SVG',
value: 'inline-svg-lazy',
checked: true,
},
{
name: 'Inline SVG',
value: 'inline-svg',
checked: true,
checked: false,
},
{
name: 'SVG sprites',
Expand Down
5 changes: 3 additions & 2 deletions generators/app/templates/gulp/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const config = {
stylesGen: `${srcPath}/styles/generated`,<% } %>
js: `${srcPath}/js`,
img: `${srcPath}/img`,
svg: `${srcPath}/img/svg`,<% if (sprites.indexOf('sprite-svg') !== -1 || sprites.indexOf('png') !== -1 || sprites.indexOf('inline-svg') !== -1) { %>
svg: `${srcPath}/img/svg`,<% if (sprites.indexOf('sprite-svg') !== -1 || sprites.indexOf('png') !== -1 || sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
icons: `${srcPath}/icons`,<% } %><% if (sprites.indexOf('inline-svg') !== -1) { %>
iconsHTML: `${srcPath}/templates/icons`,<% } %>
fonts: `${srcPath}/fonts`,<% if (multilanguage) { %>
Expand All @@ -42,7 +42,8 @@ const config = {
html: destPath,
css: `${destPath}/css`,
js: `${destPath}/js`,
img: `${destPath}/img`,
img: `${destPath}/img`,<% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>
icons: `${destPath}/img/icons`,<% } %>
fonts: `${destPath}/fonts`,<% if (pwa) { %>
sw: `${destPath}/sw.js`,<% } %>
},
Expand Down
41 changes: 24 additions & 17 deletions generators/app/templates/gulp/tasks/svgicons.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import gulp from 'gulp'
import cheerio from 'gulp-cheerio'
import clean from 'gulp-cheerio-clean-svg'
import rename from 'gulp-rename'
import svgmin from 'gulp-svgmin'
import svgmin from 'gulp-svgmin'<% if (sprites.indexOf('inline-svg') !== -1) { %>
import rename from 'gulp-rename'<% } %>
import del from 'del'
import colors from 'ansi-colors'
import log from 'fancy-log'
import { extname, basename } from 'path'
import { src } from '../config'

import log from 'fancy-log'<% if (sprites.indexOf('inline-svg') !== -1) { %>
import { extname, basename } from 'path'<% } %><% if (sprites.indexOf('inline-svg') !== -1) { %>
import { src } from '../config'<% } %><% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>
import { src, dest } from '../config'<% } %>
<% if (sprites.indexOf('inline-svg') !== -1) { %>
gulp.task('svgicons:clean', () =>
del([`${src.iconsHTML}/*.html`]).then(paths => {
log('Deleted:', colors.magenta(paths.join('\n')))
})
)
)<% } %><% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>
gulp.task('svgicons:clean', () =>
del([`${dest.icons}/*.svg`]).then(paths => {
log('Deleted:', colors.magenta(paths.join('\n')))
})
)<% } %>

gulp.task('svgicons:create', () =>
gulp
Expand All @@ -32,10 +38,10 @@ gulp.task('svgicons:create', () =>
},
{
removeDoctype: true,
},
},<% if (sprites.indexOf('inline-svg') !== -1) { %>
{
removeXMLNS: true,
},
},<% } %>
{
convertStyleToAttrs: false,
},
Expand All @@ -51,9 +57,9 @@ gulp.task('svgicons:create', () =>
)
.pipe(
cheerio({
run: ($, file) => {
run($<% if (sprites.indexOf('inline-svg') !== -1) { %>, file<% } %>) {<% if (sprites.indexOf('inline-svg') !== -1) { %>
const { relative } = file
const iconName = basename(relative, extname(relative))
const iconName = basename(relative, extname(relative))<% } %>
const svg = $('svg')
const svgStyle = svg.attr('style')

Expand All @@ -68,21 +74,22 @@ gulp.task('svgicons:create', () =>
}
const height = '1em'
const width = `${(w / h).toFixed(3)}em`

svg.attr('class', `{% if mod %}{{ mod }} {% endif %}icon icon--${iconName}`)
<% if (sprites.indexOf('inline-svg') !== -1) { %>
svg.attr('class', `{% if mod %}{{ mod }} {% endif %}icon icon--${iconName}`)<% } %>
svg.attr('width', width)
svg.attr('height', height)
},
parserOptions: { xmlMode: false },
},<% if (sprites.indexOf('inline-svg') !== -1) { %>
parserOptions: { xmlMode: false },<% } %>
})
)
)<% if (sprites.indexOf('inline-svg') !== -1) { %>
.pipe(
rename({
prefix: '_',
extname: '.html',
})
)
.pipe(gulp.dest(src.iconsHTML))
.pipe(gulp.dest(src.iconsHTML))<% } %><% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>
.pipe(gulp.dest(dest.icons))<% } %>
)

const build = gulp => gulp.series('svgicons:clean', 'svgicons:create')
Expand Down
10 changes: 5 additions & 5 deletions generators/app/templates/gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import config from './gulp/config'
const getTaskBuild = task => require(`./gulp/tasks/${task}`).build(gulp)
const getTaskWatch = task => require(`./gulp/tasks/${task}`).watch(gulp)

gulp.task('clean', getTaskBuild('clean'))<% if (sprites.indexOf('inline-svg') !== -1) { %>
gulp.task('clean', getTaskBuild('clean'))<% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
gulp.task('svgicons', getTaskBuild('svgicons'))<% } %><% if (sprites.indexOf('png') !== -1) { %>
gulp.task('sprite-png', getTaskBuild('sprite-png'))<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
gulp.task('sprite:svg', () => getTaskBuild('sprite-svg'))<% } %>
Expand All @@ -17,7 +17,7 @@ gulp.task('list-pages', getTaskBuild('list-pages'))<% } %>
gulp.task('webpack', getTaskBuild('webpack'))<% if (pwa) { %>
gulp.task('sw', getTaskBuild('sw'))<% } %>

gulp.task('copy:watch', getTaskWatch('copy'))<% if (sprites.indexOf('inline-svg') !== -1) { %>
gulp.task('copy:watch', getTaskWatch('copy'))<% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
gulp.task('svgicons:watch', getTaskWatch('svgicons'))<% } %><% if (sprites.indexOf('png') !== -1) { %>
gulp.task('sprite-png:watch', getTaskWatch('sprite-png'))<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
gulp.task('sprite:svg:watch', getTaskWatch('sprite-svg'))<% } %>
Expand All @@ -43,7 +43,7 @@ gulp.task(
'build',
gulp.series(
setmodeProd,
'clean',<% if (sprites.indexOf('inline-svg') !== -1) { %>
'clean',<% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
'svgicons',<% } %><% if (sprites.indexOf('png') !== -1) { %>
'sprite-png',<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
'sprite:svg',<% } %>
Expand All @@ -61,7 +61,7 @@ gulp.task(
'build:dev',
gulp.series(
setmodeDev,
'clean',<% if (sprites.indexOf('inline-svg') !== -1) { %>
'clean',<% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
'svgicons',<% } %><% if (sprites.indexOf('png') !== -1) { %>
'sprite-png',<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
'sprite:svg',<% } %>
Expand All @@ -77,7 +77,7 @@ gulp.task(
gulp.task(
'watch',
gulp.parallel(
'copy:watch',<% if (sprites.indexOf('inline-svg') !== -1) { %>
'copy:watch',<% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
'svgicons:watch',<% } %><% if (sprites.indexOf('png') !== -1) { %>
'sprite-png:watch',<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
'sprite:svg:watch',<% } %>
Expand Down
4 changes: 2 additions & 2 deletions generators/app/templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@
"eslint-webpack-plugin": "^2.4.1",
"fancy-log": "^1.3.3",
"gulp-changed": "^4.0.3",
"gulp": "^4.0.2",<% if (sprites.indexOf('sprite-svg') !== -1 || sprites.indexOf('inline-svg') !== -1) { %>
"gulp": "^4.0.2",<% if (sprites.indexOf('sprite-svg') !== -1 || sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
"gulp-cheerio": "^1.0.0",<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
"gulp-svgstore": "^7.0.1",<% } %><% if (sprites.indexOf('inline-svg') !== -1) { %>
"gulp-svgstore": "^7.0.1",<% } %><% if (sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
"gulp-cheerio-clean-svg": "github:Hiswe/gulp-cheerio-clean-svg",<% } %>
"gulp-consolidate": "^0.2.0",<% if (multilanguage) { %>
"gulp-data": "^1.3.1",
Expand Down
109 changes: 75 additions & 34 deletions generators/app/templates/src/js/components/LazyLoader/LazyLoader.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { loadCSS } from 'fg-loadcss'
import lazyLoad from './lazyLoad'
import classNames from '../../classNames'
import { IS_LOADED, HAS_ERROR } from '../../constants'
import { supportsWoff2 } from '../../helpers'

const defaultConfig = {
className: 'lazy',
onLoad: () => {},
onError: () => {},
}
export default class LazyLoader {
constructor(selector = `.${classNames.lazy}`, options = {}) {
constructor(selector = '', options = {}) {
this.options = {
...defaultConfig,
...options,
selector,
}
}<% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>

this.loadedIcons = []<% } %>
}

isPicture({ parentNode }) {
Expand All @@ -26,10 +29,10 @@ export default class LazyLoader {

setAttributes(el) {
const {
dataset: { src, poster, srcset, backgroundImage: bgImage },
dataset: { src, poster, srcset, backgroundImage: bgImage, icon },
} = el

if (src) el.src = src
if (src && !icon) el.src = src
if (srcset) el.srcset = srcset
if (poster) el.setAttribute('poster', poster)
if (bgImage) el.style.backgroundImage = `url('${bgImage}')`
Expand All @@ -51,26 +54,67 @@ export default class LazyLoader {
if (sourceSrcset) source.srcset = sourceSrcset
})
}
}

handleLoad(el) {
const onLoad = ({ currentTarget }) => {
currentTarget.classList.add(`${this.options.className}--${IS_LOADED}`)
}<% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>

async loadIcon(image, resolve) {
const { src } = image.dataset
const duplicate = this.loadedIcons.find(({ src: s }) => s === src)

try {
const res = duplicate ? null : await fetch(src)
const data = duplicate ? duplicate.data : await res.text()

// If icon was loaded already, or fetch is successful.
if (duplicate || res.ok) {
const parser = new DOMParser()
const svg = parser.parseFromString(data, 'image/svg+xml').querySelector('svg')

if (image.id) svg.id = image.id
if (image.className) svg.classList = image.classList
svg.setAttribute('data-processed', true)

image.parentNode?.replaceChild(svg, image)

if (!duplicate) {
this.loadedIcons.push({
src,
data,
})
}
}
} catch (error) {
console.error(`Error loading "${src}" icon.`, error)
}

const onError = e => {
e.currentTarget.classList.add(`${this.options.className}--${HAS_ERROR}`)
if (this.options.onError) this.options.onError(e)
}
// Clear from duplications.
this.loadedIcons = this.loadedIcons.reduce((arr, item) => {
const dupl = arr.find(({ src: s }) => s === item.src)
return dupl ? arr : [...arr, item]
}, [])

if (el.tagName.toLowerCase() === 'video') {
el.onloadeddata = onLoad.bind(this)
}
if (el.tagName.toLowerCase() === 'img') {
el.onload = onLoad.bind(this)
}

el.onerror = onError.bind(this)
resolve()
}
<% } %>
handleLoad(el) {
return new Promise(resolve => {
const onLoad = e => {
e.currentTarget.classList.add(`${this.options.className}--${IS_LOADED}`)
this.options.onLoad?.(e)
resolve()
}

const onError = e => {
e.currentTarget.classList.add(`${this.options.className}--${HAS_ERROR}`)
this.options.onError?.(e)
resolve()
}
<% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>
if (el.dataset.icon) this.loadIcon(el, resolve)<% } %>
if (el.tagName.toLowerCase() === 'video') el.onloadeddata = onLoad
if (el.tagName.toLowerCase() === 'img' && !el.dataset.icon) el.onload = onLoad

el.onerror = onError
})
}

removeAttributes(el) {
Expand All @@ -84,32 +128,29 @@ export default class LazyLoader {
}
}

processElement(el) {
processElement = async el => {
this.setAttributes(el)
this.handleLoad(el)
await this.handleLoad(el)
this.removeAttributes(el)
}

setObserving() {
this.loader = lazyLoad({
process: this.processElement.bind(this),
...this.options,
})
this.loader.observe()
}

loadFonts() {
const PATH_TO_FONTS_CSS = '/css'
const fileName = 'data-woff.css'

if (!supportsWoff2) loadCSS(`${PATH_TO_FONTS_CSS}/data-woff.css`)
if (!supportsWoff2) loadCSS(`${PATH_TO_FONTS_CSS}/${fileName}`)
}

update() {
this.loader.update()
}

observe() {
this.setObserving()
this.loader = lazyLoad({
process: this.processElement,
...this.options,
})
this.loader.observe()
}

init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export default async app => {
if (!element) return

const { default: LazyLoader } = await import(/* webpackChunkName: "LazyLoader" */ './LazyLoader')
app.lazyLoader = new LazyLoader()
app.lazyLoader = new LazyLoader(`.${classNames.lazy}`)
app.lazyLoader.init()
}
5 changes: 1 addition & 4 deletions generators/app/templates/src/templates/mixins/_all.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@

{% from 'mixins/_btn.html' import btn %}
{% set btn = btn %}
<% if (sprites.indexOf('inline-svg') !== -1) { %>
<% if (sprites.indexOf('sprite-svg') !== -1 || sprites.indexOf('inline-svg') !== -1 || sprites.indexOf('inline-svg-lazy') !== -1) { %>
{% from 'mixins/_icon.html' import icon %}
{% set icon = icon %}
<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
{% from 'mixins/_icon.html' import iconSvg %}
{% set iconSvg = iconSvg %}
<% } %>
{% from 'mixins/_input.html' import input %}
{% set input = input %}
13 changes: 11 additions & 2 deletions generators/app/templates/src/templates/mixins/_icon.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
<% if (sprites.indexOf('inline-svg') !== -1) { %>{% macro icon(name, mod) %}
{% include 'icons/_'+ name +'.html' %}
{% endmacro %}<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>
{% macro iconSvg(name) %}
{% endmacro %}<% } %><% if (sprites.indexOf('sprite-svg') !== -1) { %>{% macro icon(name) %}
<svg class="icon-svg icon-svg-{{ name }}"><use xlink:href="img/sprite.svg#icon-svg-{{ name }}"></use></svg>
{% endmacro %}<% } %><% if (sprites.indexOf('inline-svg-lazy') !== -1) { %>{% macro icon(name, width = '', height = '') %}
<img
class="js-lazy icon icon--{{ name }}"
src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
alt=""
data-src="/img/icons/{{ name }}.svg"
data-icon="true"
{% if width %}width="{{ width }}"{% endif %}
{% if height %}height="{{ height }}"{% endif %}
/>
{% endmacro %}<% } %>
Loading

0 comments on commit 613f4b6

Please sign in to comment.