Skip to content

Commit ef30466

Browse files
committed
Add Import Wrappers again
1 parent f954149 commit ef30466

File tree

3 files changed

+202
-17
lines changed

3 files changed

+202
-17
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,59 @@ Internally the [Intersection Observer API](https://developer.mozilla.org/en-US/d
196196

197197
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).
198198

199+
## Import Wrappers
200+
201+
> **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).
202+
203+
Additionally to the `<LazyHydrate>` wrapper component you can also use Import Wrappers to lazy load and hydrate certain components.
204+
205+
```html
206+
<template>
207+
<div class="ArticlePage">
208+
<ImageSlider/>
209+
<ArticleContent :content="article.content"/>
210+
<AdSlider/>
211+
<CommentForm :article-id="article.id"/>
212+
</div>
213+
</template>
214+
215+
<script>
216+
import {
217+
hydrateOnInteraction,
218+
hydrateSsrOnly,
219+
hydrateWhenIdle,
220+
hydrateWhenVisible,
221+
} from 'vue-lazy-hydration';
222+
223+
export default {
224+
components: {
225+
AdSlider: hydrateWhenVisible(
226+
() => import('./AdSlider.vue'),
227+
// Optional.
228+
{ observerOptions: { rootMargin: '100px' } },
229+
),
230+
ArticleContent: hydrateSsrOnly(
231+
() => import('./ArticleContent.vue'),
232+
{ ignoredProps: ['content'] },
233+
),
234+
CommentForm: hydrateOnInteraction(
235+
() => import('./CommentForm.vue'),
236+
// `focus` is the default event.
237+
{ event: 'focus', ignoredProps: ['articleId'] },
238+
),
239+
ImageSlider: hydrateWhenIdle(() => import('./ImageSlider.vue')),
240+
},
241+
// ...
242+
};
243+
</script>
244+
```
245+
246+
### Caveats
247+
248+
1. Properties passed to a wrapped component are rendered as an HTML attribute on the root element.
249+
E.g. `<ArticleContent :content="article.content"/>` would render to `<div class="ArticleContent" content="Lorem ipsum dolor ...">Lorem ipsum dolor ...</div>` as long as you don't provide `content` as an ignored property the way you can see in the example above.
250+
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.
251+
199252
## Benchmarks
200253

201254
### Without lazy hydration

src/LazyHydrate.js

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,111 @@
1+
import {
2+
createObserver,
3+
loadingComponentFactory,
4+
resolvableComponentFactory,
5+
} from './utils';
6+
17
const isServer = typeof window === `undefined`;
28
const isBrowser = !isServer;
39

4-
const observers = new Map();
10+
export function hydrateWhenIdle(component, { ignoredProps }) {
11+
if (isServer) return component;
12+
13+
const resolvableComponent = resolvableComponentFactory(component);
14+
const loading = loadingComponentFactory(resolvableComponent, {
15+
props: ignoredProps,
16+
mounted() {
17+
// If `requestIdleCallback()` or `requestAnimationFrame()`
18+
// is not supported, hydrate immediately.
19+
if (!(`requestIdleCallback` in window) || !(`requestAnimationFrame` in window)) {
20+
// eslint-disable-next-line no-underscore-dangle
21+
resolvableComponent._resolve();
22+
return;
23+
}
24+
25+
const id = requestIdleCallback(() => {
26+
// eslint-disable-next-line no-underscore-dangle
27+
requestAnimationFrame(resolvableComponent._resolve);
28+
}, { timeout: this.idleTimeout });
29+
const cleanup = () => cancelIdleCallback(id);
30+
resolvableComponent.then(cleanup);
31+
},
32+
});
533

6-
function createObserver(options) {
7-
if (typeof IntersectionObserver === `undefined`) return null;
34+
return () => ({
35+
component: resolvableComponent,
36+
delay: 0,
37+
loading,
38+
});
39+
}
840

9-
const optionKey = JSON.stringify(options);
10-
if (observers.has(optionKey)) return observers.get(optionKey);
41+
export function hydrateWhenVisible(component, { ignoredProps, observerOptions }) {
42+
if (isServer) return component;
1143

12-
const observer = new IntersectionObserver((entries) => {
13-
entries.forEach((entry) => {
14-
// Use `intersectionRatio` because of Edge 15's
15-
// lack of support for `isIntersecting`.
16-
// See: https://github.com/w3c/IntersectionObserver/issues/211
17-
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
18-
if (!isIntersecting || !entry.target.parentElement.hydrate) return;
44+
const resolvableComponent = resolvableComponentFactory(component);
45+
const observer = createObserver(observerOptions);
1946

20-
entry.target.parentElement.hydrate();
21-
});
22-
}, options);
23-
observers.set(optionKey, observer);
47+
const loading = loadingComponentFactory(resolvableComponent, {
48+
props: ignoredProps,
49+
mounted() {
50+
// If Intersection Observer API is not supported, hydrate immediately.
51+
if (!observer) {
52+
// eslint-disable-next-line no-underscore-dangle
53+
resolvableComponent._resolve();
54+
return;
55+
}
56+
57+
// eslint-disable-next-line no-underscore-dangle
58+
this.$el.hydrate = resolvableComponent._resolve;
59+
const cleanup = () => observer.unobserve(this.$el);
60+
resolvableComponent.then(cleanup);
61+
observer.observe(this.$el);
62+
},
63+
});
64+
65+
return () => ({
66+
component: resolvableComponent,
67+
delay: 0,
68+
loading,
69+
});
70+
}
71+
72+
export function hydrateSsrOnly(component) {
73+
if (isServer) return component;
74+
75+
const resolvableComponent = resolvableComponentFactory(component);
76+
const loading = loadingComponentFactory(resolvableComponent);
77+
78+
return () => ({
79+
component: resolvableComponent,
80+
delay: 0,
81+
loading,
82+
});
83+
}
84+
85+
export function hydrateOnInteraction(component, { event = `focus`, ignoredProps }) {
86+
if (isServer) return component;
87+
88+
const resolvableComponent = resolvableComponentFactory(component);
89+
const events = Array.isArray(event) ? event : [event];
90+
91+
const loading = loadingComponentFactory(resolvableComponent, {
92+
props: ignoredProps,
93+
mounted() {
94+
events.forEach((eventName) => {
95+
// eslint-disable-next-line no-underscore-dangle
96+
this.$el.addEventListener(eventName, resolvableComponent._resolve, {
97+
capture: true,
98+
once: true,
99+
});
100+
});
101+
},
102+
});
24103

25-
return observer;
104+
return () => ({
105+
component: resolvableComponent,
106+
delay: 0,
107+
loading,
108+
});
26109
}
27110

28111
export default {

src/utils.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const observers = new Map();
2+
3+
export function createObserver(options) {
4+
if (typeof IntersectionObserver === `undefined`) return null;
5+
6+
const optionKey = JSON.stringify(options);
7+
if (observers.has(optionKey)) return observers.get(optionKey);
8+
9+
const observer = new IntersectionObserver((entries) => {
10+
entries.forEach((entry) => {
11+
// Use `intersectionRatio` because of Edge 15's
12+
// lack of support for `isIntersecting`.
13+
// See: https://github.com/w3c/IntersectionObserver/issues/211
14+
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
15+
if (!isIntersecting || !entry.target.hydrate) return;
16+
entry.target.hydrate();
17+
});
18+
}, options);
19+
observers.set(optionKey, observer);
20+
21+
return observer;
22+
}
23+
24+
export function loadingComponentFactory(resolvableComponent, options) {
25+
return {
26+
render(h) {
27+
const tag = this.$el ? this.$el.tagName : `div`;
28+
29+
// eslint-disable-next-line no-underscore-dangle
30+
if (!this.$el) resolvableComponent._resolve();
31+
32+
return h(tag);
33+
},
34+
...options,
35+
};
36+
}
37+
38+
export function resolvableComponentFactory(component) {
39+
let resolve;
40+
const promise = new Promise((newResolve) => {
41+
resolve = newResolve;
42+
});
43+
// eslint-disable-next-line no-underscore-dangle
44+
promise._resolve = async () => {
45+
resolve(typeof component === `function` ? await component() : component);
46+
};
47+
48+
return promise;
49+
}

0 commit comments

Comments
 (0)