From ef30466a55dec2994c80a1abaaf68ac4392138ac Mon Sep 17 00:00:00 2001 From: Markus Oberlehner Date: Sat, 2 Mar 2019 10:30:53 +0100 Subject: [PATCH] Add Import Wrappers again --- README.md | 53 ++++++++++++++++++++ src/LazyHydrate.js | 117 ++++++++++++++++++++++++++++++++++++++------- src/utils.js | 49 +++++++++++++++++++ 3 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 src/utils.js diff --git a/README.md b/README.md index 3146a28..320ccf4 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,59 @@ Internally the [Intersection Observer API](https://developer.mozilla.org/en-US/d For a list of possible options please [take a look at the Intersection Observer API documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver). +## Import Wrappers + +> **Attention:** because of [a bug in Vue.js <= v2.6.7](https://github.com/vuejs/vue/pull/9572) Import Wrappers require that you have at least version **v2.6.8** of Vue.js installed otherwise they will not work correctly in certain situations (especially in combination with Vue Router). + +Additionally to the `` wrapper component you can also use Import Wrappers to lazy load and hydrate certain components. + +```html + + + +``` + +### Caveats + +1. Properties passed to a wrapped component are rendered as an HTML attribute on the root element. + E.g. `` would render to `
Lorem ipsum dolor ...
` as long as you don't provide `content` as an ignored property the way you can see in the example above. +2. When using `hydrateWhenVisible` and `hydrateOnInteraction` all instances of a certain component are immediately hydrated as soon as one of the instances becomes visible or is interacted with. + ## Benchmarks ### Without lazy hydration diff --git a/src/LazyHydrate.js b/src/LazyHydrate.js index 0e10dae..2ee09a7 100644 --- a/src/LazyHydrate.js +++ b/src/LazyHydrate.js @@ -1,28 +1,111 @@ +import { + createObserver, + loadingComponentFactory, + resolvableComponentFactory, +} from './utils'; + const isServer = typeof window === `undefined`; const isBrowser = !isServer; -const observers = new Map(); +export function hydrateWhenIdle(component, { ignoredProps }) { + if (isServer) return component; + + const resolvableComponent = resolvableComponentFactory(component); + const loading = loadingComponentFactory(resolvableComponent, { + props: ignoredProps, + mounted() { + // If `requestIdleCallback()` or `requestAnimationFrame()` + // is not supported, hydrate immediately. + if (!(`requestIdleCallback` in window) || !(`requestAnimationFrame` in window)) { + // eslint-disable-next-line no-underscore-dangle + resolvableComponent._resolve(); + return; + } + + const id = requestIdleCallback(() => { + // eslint-disable-next-line no-underscore-dangle + requestAnimationFrame(resolvableComponent._resolve); + }, { timeout: this.idleTimeout }); + const cleanup = () => cancelIdleCallback(id); + resolvableComponent.then(cleanup); + }, + }); -function createObserver(options) { - if (typeof IntersectionObserver === `undefined`) return null; + return () => ({ + component: resolvableComponent, + delay: 0, + loading, + }); +} - const optionKey = JSON.stringify(options); - if (observers.has(optionKey)) return observers.get(optionKey); +export function hydrateWhenVisible(component, { ignoredProps, observerOptions }) { + if (isServer) return component; - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - // Use `intersectionRatio` because of Edge 15's - // lack of support for `isIntersecting`. - // See: https://github.com/w3c/IntersectionObserver/issues/211 - const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0; - if (!isIntersecting || !entry.target.parentElement.hydrate) return; + const resolvableComponent = resolvableComponentFactory(component); + const observer = createObserver(observerOptions); - entry.target.parentElement.hydrate(); - }); - }, options); - observers.set(optionKey, observer); + const loading = loadingComponentFactory(resolvableComponent, { + props: ignoredProps, + mounted() { + // If Intersection Observer API is not supported, hydrate immediately. + if (!observer) { + // eslint-disable-next-line no-underscore-dangle + resolvableComponent._resolve(); + return; + } + + // eslint-disable-next-line no-underscore-dangle + this.$el.hydrate = resolvableComponent._resolve; + const cleanup = () => observer.unobserve(this.$el); + resolvableComponent.then(cleanup); + observer.observe(this.$el); + }, + }); + + return () => ({ + component: resolvableComponent, + delay: 0, + loading, + }); +} + +export function hydrateSsrOnly(component) { + if (isServer) return component; + + const resolvableComponent = resolvableComponentFactory(component); + const loading = loadingComponentFactory(resolvableComponent); + + return () => ({ + component: resolvableComponent, + delay: 0, + loading, + }); +} + +export function hydrateOnInteraction(component, { event = `focus`, ignoredProps }) { + if (isServer) return component; + + const resolvableComponent = resolvableComponentFactory(component); + const events = Array.isArray(event) ? event : [event]; + + const loading = loadingComponentFactory(resolvableComponent, { + props: ignoredProps, + mounted() { + events.forEach((eventName) => { + // eslint-disable-next-line no-underscore-dangle + this.$el.addEventListener(eventName, resolvableComponent._resolve, { + capture: true, + once: true, + }); + }); + }, + }); - return observer; + return () => ({ + component: resolvableComponent, + delay: 0, + loading, + }); } export default { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..65cb2bd --- /dev/null +++ b/src/utils.js @@ -0,0 +1,49 @@ +const observers = new Map(); + +export function createObserver(options) { + if (typeof IntersectionObserver === `undefined`) return null; + + const optionKey = JSON.stringify(options); + if (observers.has(optionKey)) return observers.get(optionKey); + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + // Use `intersectionRatio` because of Edge 15's + // lack of support for `isIntersecting`. + // See: https://github.com/w3c/IntersectionObserver/issues/211 + const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0; + if (!isIntersecting || !entry.target.hydrate) return; + entry.target.hydrate(); + }); + }, options); + observers.set(optionKey, observer); + + return observer; +} + +export function loadingComponentFactory(resolvableComponent, options) { + return { + render(h) { + const tag = this.$el ? this.$el.tagName : `div`; + + // eslint-disable-next-line no-underscore-dangle + if (!this.$el) resolvableComponent._resolve(); + + return h(tag); + }, + ...options, + }; +} + +export function resolvableComponentFactory(component) { + let resolve; + const promise = new Promise((newResolve) => { + resolve = newResolve; + }); + // eslint-disable-next-line no-underscore-dangle + promise._resolve = async () => { + resolve(typeof component === `function` ? await component() : component); + }; + + return promise; +}