-
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(preload): added Preload strategies module
- Loading branch information
Showing
22 changed files
with
431 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Preload | ||
|
||
Preload lazy-module strategies | ||
|
||
* PreloadSelectedStrategy - Deterministic pre-fetching based on `preload` value in Route Data. | ||
* PreloadViewportStrategy - Speculative pre-fetching based on links in `Viewport` | ||
* PredictivePreloadStrategy - Predictive pre-fetching based google analytics. Use `Guess.JS` | ||
|
||
> `PreloadViewportStrategy` same as @mgechev [ngx-quicklink](https://github.com/mgechev/ngx-quicklink) | ||
### Usecase | ||
|
||
- feature-1: load with main core bundle | ||
- feature-2: preload in background to be ready to use when user navigates to feature-2 | ||
- feature-3: only lazy load if the user navigates to feature-3 | ||
|
||
### Publish | ||
```bash | ||
# build | ||
ng build preload | ||
# replace your npm key | ||
export NPM_TOKEN="00000000-0000-0000-0000-000000000000" | ||
# publish | ||
npm publish dist/libs/preload --access public | ||
``` | ||
|
||
### TODO | ||
* make `PreloadViewportStrategy` customizable. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
name: 'preload', | ||
preset: '../../jest.config.js', | ||
coverageDirectory: '../../coverage/libs/preload', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", | ||
"dest": "../../dist/libs/preload", | ||
"lib": { | ||
"entryFile": "src/index.ts" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"name": "@ngx-starter-kit/preload", | ||
"version": "0.0.1", | ||
"peerDependencies": { | ||
"@angular/common": "^7.2.0", | ||
"@angular/core": "^7.2.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './lib/strategies/viewport/preload-viewport.module'; | ||
export * from './lib/strategies/viewport/preload-viewport-strategy.service'; | ||
export * from './lib/strategies/selected/preload-selected-strategy.service'; |
12 changes: 12 additions & 0 deletions
12
libs/preload/src/lib/strategies/selected/preload-selected-strategy.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { PreloadingStrategy, Route } from '@angular/router'; | ||
import { Observable, of } from 'rxjs'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class PreloadSelectedStrategy implements PreloadingStrategy { | ||
preload(route: Route, load: Function): Observable<any> { | ||
return route.data && route.data.preload ? load() : of(null); | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
libs/preload/src/lib/strategies/viewport/link-handler.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { LinkDirective } from './link.directive'; | ||
import { RouterPreloader } from '@angular/router'; | ||
import { PrefetchRegistryService } from './prefetch-registry.service'; | ||
|
||
type RequestIdleCallbackHandle = any; | ||
interface RequestIdleCallbackOptions { | ||
timeout: number; | ||
} | ||
interface RequestIdleCallbackDeadline { | ||
readonly didTimeout: boolean; | ||
timeRemaining: () => number; | ||
} | ||
|
||
declare global { | ||
interface Window { | ||
requestIdleCallback: ( | ||
callback: (deadline: RequestIdleCallbackDeadline) => void, | ||
opts?: RequestIdleCallbackOptions, | ||
) => RequestIdleCallbackHandle; | ||
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void; | ||
} | ||
} | ||
|
||
const requestIdleCallback = | ||
window.requestIdleCallback || | ||
function(cb: Function) { | ||
const start = Date.now(); | ||
return setTimeout(function() { | ||
cb({ | ||
didTimeout: false, | ||
timeRemaining: function() { | ||
return Math.max(0, 50 - (Date.now() - start)); | ||
}, | ||
}); | ||
}, 1); | ||
}; | ||
|
||
const cancelIdleCallback = window.cancelIdleCallback || clearTimeout; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class LinkHandlerService { | ||
private registerIdle: any; | ||
private unregisterIdle: any; | ||
private registerBuffer: Element[] = []; | ||
private unregisterBuffer: Element[] = []; | ||
private elementLink = new Map<Element, LinkDirective>(); | ||
private observer = new IntersectionObserver(entries => { | ||
entries.forEach(entry => { | ||
if (entry.isIntersecting) { | ||
const link = entry.target as HTMLAnchorElement; | ||
this.queue.add(this.elementLink.get(link).urlTree); | ||
this.observer.unobserve(link); | ||
requestIdleCallback(() => { | ||
this.loader.preload().subscribe(() => void 0); | ||
}); | ||
} | ||
}); | ||
}); | ||
|
||
constructor(private loader: RouterPreloader, private queue: PrefetchRegistryService) {} | ||
|
||
register(el: LinkDirective) { | ||
this.elementLink.set(el.element, el); | ||
cancelIdleCallback(this.registerIdle); | ||
this.registerBuffer.push(el.element); | ||
this.registerIdle = requestIdleCallback(() => { | ||
this.registerBuffer.forEach(e => this.observer.observe(e)); | ||
this.registerBuffer = []; | ||
}); | ||
} | ||
|
||
unregister(el: LinkDirective) { | ||
this.elementLink.delete(el.element); | ||
cancelIdleCallback(this.unregisterIdle); | ||
this.unregisterBuffer.push(el.element); | ||
this.unregisterIdle = window.requestIdleCallback(() => { | ||
this.unregisterBuffer.forEach(e => this.observer.unobserve(e)); | ||
this.unregisterBuffer = []; | ||
}); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
libs/preload/src/lib/strategies/viewport/link.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Directive, ElementRef, OnDestroy, OnInit, Optional } from '@angular/core'; | ||
import { RouterLink, RouterLinkWithHref } from '@angular/router'; | ||
import { LinkHandlerService } from './link-handler.service'; | ||
|
||
@Directive({ | ||
selector: '[routerLink]', | ||
}) | ||
export class LinkDirective implements OnInit, OnDestroy { | ||
private routerLink: RouterLink | RouterLinkWithHref; | ||
|
||
constructor( | ||
private linkHandler: LinkHandlerService, | ||
private el: ElementRef, | ||
@Optional() link: RouterLink, | ||
@Optional() linkWithHref: RouterLinkWithHref, | ||
) { | ||
this.routerLink = link || linkWithHref; | ||
} | ||
|
||
ngOnInit() { | ||
this.linkHandler.register(this); | ||
} | ||
|
||
ngOnDestroy() { | ||
this.linkHandler.unregister(this); | ||
} | ||
|
||
get element(): Element { | ||
return this.el.nativeElement; | ||
} | ||
|
||
get urlTree() { | ||
return this.routerLink.urlTree; | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
libs/preload/src/lib/strategies/viewport/prefetch-registry.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { Params, PRIMARY_OUTLET, Router, UrlSegment, UrlSegmentGroup, UrlTree } from '@angular/router'; | ||
import { Injectable } from '@angular/core'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class PrefetchRegistryService { | ||
private trees: UrlTree[] = []; | ||
constructor(private router: Router) {} | ||
|
||
add(tree: UrlTree) { | ||
this.trees.push(tree); | ||
} | ||
|
||
shouldPrefetch(url: string) { | ||
const tree = this.router.parseUrl(url); | ||
return this.trees.some(child => containsTree(child, tree)); | ||
} | ||
} | ||
|
||
function containsQueryParams(container: Params, containee: Params): boolean { | ||
// TODO: This does not handle array params correctly. | ||
return ( | ||
Object.keys(containee).length <= Object.keys(container).length && | ||
Object.keys(containee).every(key => containee[key] === container[key]) | ||
); | ||
} | ||
|
||
function containsTree(container: UrlTree, containee: UrlTree): boolean { | ||
return ( | ||
containsQueryParams(container.queryParams, containee.queryParams) && | ||
containsSegmentGroup(container.root, containee.root) | ||
); | ||
} | ||
|
||
function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean { | ||
return containsSegmentGroupHelper(container, containee, containee.segments); | ||
} | ||
|
||
function containsSegmentGroupHelper( | ||
container: UrlSegmentGroup, | ||
containee: UrlSegmentGroup, | ||
containeePaths: UrlSegment[], | ||
): boolean { | ||
if (container.segments.length > containeePaths.length) { | ||
const current = container.segments.slice(0, containeePaths.length); | ||
if (!equalPath(current, containeePaths)) { | ||
return false; | ||
} | ||
if (containee.hasChildren()) { | ||
return false; | ||
} | ||
return true; | ||
} else if (container.segments.length === containeePaths.length) { | ||
if (!equalPath(container.segments, containeePaths)) { | ||
return false; | ||
} | ||
for (const c in containee.children) { | ||
if (!container.children[c]) { | ||
return false; | ||
} | ||
if (!containsSegmentGroup(container.children[c], containee.children[c])) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} else { | ||
const current = containeePaths.slice(0, container.segments.length); | ||
const next = containeePaths.slice(container.segments.length); | ||
if (!equalPath(container.segments, current)) { | ||
return false; | ||
} | ||
if (!container.children[PRIMARY_OUTLET]) { | ||
return false; | ||
} | ||
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next); | ||
} | ||
} | ||
|
||
export function equalPath(as: UrlSegment[], bs: UrlSegment[]): boolean { | ||
if (as.length !== bs.length) { | ||
return false; | ||
} | ||
return as.every((a, i) => a.path === bs[i].path); | ||
} |
Oops, something went wrong.