@prefetchru/prefetch 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/prefetch.esm.min.js +6 -0
- package/dist/prefetch.min.js +2 -2
- package/package.json +15 -3
- package/prefetch.esm.js +620 -0
- package/prefetch.js +26 -19
package/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# prefetch.ru
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@prefetchru/prefetch)
|
|
4
|
+
[](https://www.npmjs.com/package/@prefetchru/prefetch)
|
|
5
|
+
[](https://bundlephobia.com/package/@prefetchru/prefetch)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/prefetch-ru/prefetch/releases)
|
|
8
|
+
[](https://github.com/prefetch-ru/prefetch/commits)
|
|
9
|
+
[](https://prefetch.ru)
|
|
10
|
+
|
|
3
11
|
**Instant page loading for Russian web**
|
|
4
12
|
|
|
5
13
|
Prefetch pages 65ms before user clicks. Pre-loads links on hover with smart fallbacks for iOS/Safari, optimized for Russian browsers (Yandex.Browser, Atom), CMS platforms (1C-Bitrix, Tilda), and e-commerce. Works everywhere with zero configuration — just add one script tag.
|
|
@@ -32,7 +40,38 @@ npm install @prefetchru/prefetch
|
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
```javascript
|
|
43
|
+
// ESM (рекомендуется)
|
|
35
44
|
import '@prefetchru/prefetch'
|
|
45
|
+
|
|
46
|
+
// CommonJS
|
|
47
|
+
require('@prefetchru/prefetch')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### ESM версия
|
|
51
|
+
|
|
52
|
+
npm пакет включает ESM версию с поддержкой SSR и автоматическим определением CSP nonce:
|
|
53
|
+
|
|
54
|
+
| Файл | Описание |
|
|
55
|
+
|------|----------|
|
|
56
|
+
| `prefetch.js` | IIFE версия (классический `<script>`) |
|
|
57
|
+
| `prefetch.esm.js` | ESM версия (для бандлеров и `<script type="module">`) |
|
|
58
|
+
| `dist/prefetch.min.js` | Минифицированная IIFE |
|
|
59
|
+
| `dist/prefetch.esm.min.js` | Минифицированная ESM |
|
|
60
|
+
|
|
61
|
+
**Важно о CSP nonce:**
|
|
62
|
+
|
|
63
|
+
ESM версия автоматически определяет `nonce` через `import.meta.url`, но это работает только при прямом подключении:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<!-- ✅ nonce определится автоматически -->
|
|
67
|
+
<script type="module" src="prefetch.esm.js" nonce="abc123"></script>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
При импорте из бандлера (Vite, Webpack, Rollup) `import.meta.url` указывает на бандл, а не на исходный скрипт. В этом случае передавайте nonce через `data-prefetch-nonce`:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<!-- При использовании через бандлер -->
|
|
74
|
+
<body data-prefetch-nonce="{{RANDOM_NONCE}}">
|
|
36
75
|
```
|
|
37
76
|
|
|
38
77
|
### CDN
|
|
@@ -98,7 +137,7 @@ import '@prefetchru/prefetch'
|
|
|
98
137
|
// Доступен объект window.Prefetch после загрузки скрипта
|
|
99
138
|
|
|
100
139
|
// Версия библиотеки
|
|
101
|
-
console.log(Prefetch.version) // "1.0.
|
|
140
|
+
console.log(Prefetch.version) // "1.0.9"
|
|
102
141
|
|
|
103
142
|
// Программная предзагрузка URL
|
|
104
143
|
Prefetch.preload('/catalog/product-123')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* prefetch.ru v1.0.9 (ESM) - Мгновенная загрузка страниц
|
|
3
|
+
* © 2026 Сергей Макаров | MIT License
|
|
4
|
+
* https://prefetch.ru | https://github.com/prefetch-ru
|
|
5
|
+
*/
|
|
6
|
+
var e="undefined"!=typeof window&&window.Prefetch&&window.Prefetch.__prefetchRu?window.Prefetch:function(e){if("undefined"==typeof window||"undefined"==typeof document)return{__prefetchRu:!0,version:"1.0.9",preload:function(){}};var t=new Set,n=new WeakMap,r=0,o=0,i=null,a=!1,c=!1,u=null,l=null,s=!1,f=null,d=null,h=!1,p=65,m=80,v=50,g=!1,w=!1,y=!1,x=!1,b="none",L=!1,E=!1,T=!1,A=!1;function S(){var e=document.body;if(e){l=void 0!==window.BX?"bitrix":void 0!==window.B24||void 0!==window.BX24?"bitrix24":document.querySelector(".t-records")||void 0!==window.Tilda?"tilda":null;var t=e.dataset;if(g="prefetchAllowQueryString"in t||"instantAllowQueryString"in t,w="prefetchAllowExternalLinks"in t||"instantAllowExternalLinks"in t,y="prefetchWhitelist"in t||"instantWhitelist"in t,t.prefetchNonce&&(d=t.prefetchNonce),!d&&t.instantNonce&&(d=t.instantNonce),!c&&("prefetchSpecrules"in t||"instantSpecrules"in t)&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){var n=t.prefetchSpecrules||t.instantSpecrules;"prerender"===n?(b="prerender",x=!0):"no"!==n&&(b="prefetch",x=!0)}L="prefetchPrerenderAll"in t||"instantPrerenderAll"in t;var r=t.prefetchIntensity||t.instantIntensity;if("mousedown"===r)E=!0;else if("viewport"===r||"viewport-all"===r)("viewport-all"===r||a&&N())&&(T=!0);else if(r){var o=parseInt(r,10);!isNaN(o)&&o>=0&&(p=o)}a&&(m=Math.max(60,Math.min(p||0,150))),(A="prefetchObserveDom"in t)||"bitrix"!==l&&"tilda"!==l||(A=!0),"tilda"===l&&p<100&&(p=100);var i={capture:!0,passive:!0};if(document.addEventListener("touchstart",P,i),E?document.addEventListener("mousedown",C,i):document.addEventListener("mouseover",k,i),T)(window.requestIdleCallback||function(e){setTimeout(e,1)})(B,{timeout:1500});A&&T&&function(){if(z)return;(z=new MutationObserver(function(e){e.some(function(e){return Array.from(e.addedNodes).some(function(e){return 1===e.nodeType&&("A"===e.tagName||e.querySelectorAll&&e.querySelectorAll("a").length)})})&&q&&(clearTimeout(H),H=setTimeout(U,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function N(){return!s&&("slow-2g"!==f&&"2g"!==f)}function O(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function P(e){r=Date.now();var t=O(e.target);if(I(t)){o&&(clearTimeout(o),o=0),i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null);var n=!1;i=function(){n=!0,o&&(clearTimeout(o),o=0),document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null},document.addEventListener("touchmove",i,{capture:!0,passive:!0,once:!0}),document.addEventListener("scroll",i,{capture:!0,passive:!0,once:!0}),o=setTimeout(function(){i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null),o=0,n||W(t.href,t)},m)}}function k(e){if(!(r&&Date.now()-r<2500)){var t=O(e.target);if(I(t)&&!n.has(t)){t.addEventListener("mouseleave",M,{passive:!0,once:!0});var o=setTimeout(function(){W(t.href,t),n.delete(t)},p);n.set(t,o)}}}function M(e){var t=e.currentTarget;if(t){var r=n.get(t);r&&(clearTimeout(r),n.delete(t))}}function C(e){if(!("number"==typeof e.button&&2===e.button||r&&Date.now()-r<2500)){var t=O(e.target);I(t)&&W(t.href,t)}}function I(e){if(!e)return!1;var n=e.getAttribute("href");if(null===n||""===n.trim())return!1;if(!e.href)return!1;if(e.target&&"_self"!==e.target)return!1;if(e.hasAttribute("download"))return!1;if("noPrefetch"in e.dataset||"prefetchNo"in e.dataset)return!1;if(y&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if("http:"!==e.protocol&&"https:"!==e.protocol)return!1;if("http:"===e.protocol&&"https:"===location.protocol)return!1;if(e.origin!==location.origin){if(!w&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if(!u)return!1}if(e.search&&!g&&!("prefetch"in e.dataset)&&!("instant"in e.dataset))return!1;if(e.hash&&e.pathname+e.search===location.pathname+location.search)return!1;if(R(e.href)===R(location.href))return!1;var r=R(e.href);return!t.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var o=new URL(t,location.href);n=o.pathname||"",r=o.hash||""}catch(e){n="",r=""}if("bitrix"===l||"bitrix24"===l){if(-1!==t.indexOf("/bitrix/")||-1!==t.indexOf("sessid="))return!1;if(e.classList.contains("bx-ajax"))return!1}if("tilda"===l&&(-1!==r.indexOf("#popup:")||-1!==r.indexOf("#rec")))return!1;return!/(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(n)&&!/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(t)}(e)&&!!function(e){var t=e.href,n=e.className||"",r="";try{r=new URL(t,location.href).hostname}catch(e){r=""}return-1===n.indexOf("ym-")&&("mc.yandex.ru"!==r&&"metrika.yandex.ru"!==r&&(-1===n.indexOf("ga-")&&-1===n.indexOf("gtm-")&&("google-analytics.com"!==r&&!r.endsWith(".google-analytics.com")&&("googletagmanager.com"!==r&&!r.endsWith(".googletagmanager.com")&&(-1===n.indexOf("piwik")&&-1===n.indexOf("matomo")&&("matomo.org"!==r&&!r.endsWith(".matomo.org")&&"piwik.org"!==r&&!r.endsWith(".piwik.org")))))))}(e))}function R(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function W(e,n){if(N()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(e),o=R(r);if(!t.has(o)){t.size>=v&&t.delete(t.values().next().value),t.add(o);var i=function(e){return x&&"none"!==b?"prerender"!==b?b:L||y||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==i)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",d&&(r.nonce=d);var o={};o[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(o),n.appendChild(r)}(r,i),void(c||!h?D(r,o):_(r,o));c||!h?D(r,o):_(r,o)}}}function _(e,n){var r=document.head;if(r){var o=document.createElement("link");o.rel="prefetch",o.href=e,o.as="document";try{o.fetchPriority="low"}catch(e){}o.onload=i,o.onerror=function(){t.delete(n),i()},r.appendChild(o)}function i(){o.onload=o.onerror=null,o.parentNode&&o.parentNode.removeChild(o)}}function D(e,n){if("function"==typeof fetch){var r=null,o=0;"undefined"!=typeof AbortController&&(r=new AbortController,o=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var i={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(i.signal=r.signal);try{fetch(e,i).then(function(e){o&&clearTimeout(o),e&&e.ok||t.delete(n)}).catch(function(){o&&clearTimeout(o),t.delete(n)})}catch(e){o&&clearTimeout(o),t.delete(n)}}}!function(){if(!(d=function(e){try{if(!e)return null;for(var t=document.getElementsByTagName("script"),n=0;n<t.length;n++){var r=t[n];if(r&&r.src&&r.src===e){var o=r.nonce||r.getAttribute("nonce")||null;if(o)return o;break}}}catch(e){}return null}(e)))try{var t=document.currentScript;t&&t.nonce&&(d=t.nonce)}catch(e){}try{var n=document.createElement("link");n.relList&&"function"==typeof n.relList.supports&&(h=n.relList.supports("prefetch"))}catch(e){}var r=navigator.userAgent;c=/iPad|iPhone/.test(r)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;var o=/Android/.test(r);(a=(c||o)&&Math.min(screen.width,screen.height)<768)&&(v=20);var i=r.match(/Chrome\/(\d+)/);i&&(u=parseInt(i[1],10));var l=navigator.connection;l&&(f=l.effectiveType,s=l.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",S):S()}();var q=null;function B(){q||(q=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(q.unobserve(e.target),I(e.target)&&W(e.target.href,e.target))})},{rootMargin:a?"100px":"200px"}),U())}function U(){q&&document.querySelectorAll("a").forEach(function(e){I(e)&&q.observe(e)})}var z=null,H=null,Q={__prefetchRu:!0,version:"1.0.9",preload:function(e){W(e)}};return window.Prefetch=Q,Q}(import.meta.url);export{e as Prefetch};export default e;
|
package/dist/prefetch.min.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* prefetch.ru v1.0.
|
|
2
|
+
* prefetch.ru v1.0.9 - Мгновенная загрузка страниц
|
|
3
3
|
* © 2026 Сергей Макаров | MIT License
|
|
4
4
|
* https://prefetch.ru | https://github.com/prefetch-ru
|
|
5
5
|
*/
|
|
6
|
-
!function(){"use strict";var e=new Set,t=new WeakMap,n=0,r=0,
|
|
6
|
+
!function(){"use strict";if(!window.Prefetch||!window.Prefetch.__prefetchRu){var e=new Set,t=new WeakMap,n=0,r=0,o=null,i=!1,a=!1,c=null,u=null,s=!1,l=null,f=null,d=!1,h=65,p=80,m=50,v=!1,g=!1,w=!1,y=!1,x="none",L=!1,b=!1,E=!1,T=!1;!function(){try{var e=document.currentScript;e&&e.nonce&&(f=e.nonce)}catch(e){}try{var t=document.createElement("link");t.relList&&"function"==typeof t.relList.supports&&(d=t.relList.supports("prefetch"))}catch(e){}var n=navigator.userAgent;a=/iPad|iPhone/.test(n)||"MacIntel"===navigator.platform&&navigator.maxTouchPoints>1;var r=/Android/.test(n);(i=(a||r)&&Math.min(screen.width,screen.height)<768)&&(m=20);var o=n.match(/Chrome\/(\d+)/);o&&(c=parseInt(o[1],10));var u=navigator.connection;u&&(l=u.effectiveType,s=u.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",N):N()}();var A=null,S=null,O=null;window.Prefetch={__prefetchRu:!0,version:"1.0.9",preload:function(e){q(e)}}}function N(){var e=document.body;if(e){u=void 0!==window.BX?"bitrix":void 0!==window.B24||void 0!==window.BX24?"bitrix24":document.querySelector(".t-records")||void 0!==window.Tilda?"tilda":null;var t=e.dataset;if(v="prefetchAllowQueryString"in t||"instantAllowQueryString"in t,g="prefetchAllowExternalLinks"in t||"instantAllowExternalLinks"in t,w="prefetchWhitelist"in t||"instantWhitelist"in t,t.prefetchNonce&&(f=t.prefetchNonce),!f&&t.instantNonce&&(f=t.instantNonce),!a&&("prefetchSpecrules"in t||"instantSpecrules"in t)&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){var n=t.prefetchSpecrules||t.instantSpecrules;"prerender"===n?(x="prerender",y=!0):"no"!==n&&(x="prefetch",y=!0)}L="prefetchPrerenderAll"in t||"instantPrerenderAll"in t;var r=t.prefetchIntensity||t.instantIntensity;if("mousedown"===r)b=!0;else if("viewport"===r||"viewport-all"===r)("viewport-all"===r||i&&P())&&(E=!0);else if(r){var o=parseInt(r,10);!isNaN(o)&&o>=0&&(h=o)}i&&(p=Math.max(60,Math.min(h||0,150))),(T="prefetchObserveDom"in t)||"bitrix"!==u&&"tilda"!==u||(T=!0),"tilda"===u&&h<100&&(h=100);var c={capture:!0,passive:!0};if(document.addEventListener("touchstart",M,c),b?document.addEventListener("mousedown",W,c):document.addEventListener("mouseover",C,c),E)(window.requestIdleCallback||function(e){setTimeout(e,1)})(B,{timeout:1500});T&&E&&function(){if(S)return;(S=new MutationObserver(function(e){e.some(function(e){return Array.from(e.addedNodes).some(function(e){return 1===e.nodeType&&("A"===e.tagName||e.querySelectorAll&&e.querySelectorAll("a").length)})})&&A&&(clearTimeout(O),O=setTimeout(z,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function P(){return!s&&("slow-2g"!==l&&"2g"!==l)}function k(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function M(e){n=Date.now();var t=k(e.target);if(D(t)){r&&(clearTimeout(r),r=0),o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null);var i=!1;o=function(){i=!0,r&&(clearTimeout(r),r=0),document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null},document.addEventListener("touchmove",o,{capture:!0,passive:!0,once:!0}),document.addEventListener("scroll",o,{capture:!0,passive:!0,once:!0}),r=setTimeout(function(){o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null),r=0,i||q(t.href,t)},p)}}function C(e){if(!(n&&Date.now()-n<2500)){var r=k(e.target);if(D(r)&&!t.has(r)){r.addEventListener("mouseleave",I,{passive:!0,once:!0});var o=setTimeout(function(){q(r.href,r),t.delete(r)},h);t.set(r,o)}}}function I(e){var n=e.currentTarget;if(n){var r=t.get(n);r&&(clearTimeout(r),t.delete(n))}}function W(e){if(!("number"==typeof e.button&&2===e.button||n&&Date.now()-n<2500)){var t=k(e.target);D(t)&&q(t.href,t)}}function D(t){if(!t)return!1;var n=t.getAttribute("href");if(null===n||""===n.trim())return!1;if(!t.href)return!1;if(t.target&&"_self"!==t.target)return!1;if(t.hasAttribute("download"))return!1;if("noPrefetch"in t.dataset||"prefetchNo"in t.dataset)return!1;if(w&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if("http:"!==t.protocol&&"https:"!==t.protocol)return!1;if("http:"===t.protocol&&"https:"===location.protocol)return!1;if(t.origin!==location.origin){if(!g&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(!c)return!1}if(t.search&&!v&&!("prefetch"in t.dataset)&&!("instant"in t.dataset))return!1;if(t.hash&&t.pathname+t.search===location.pathname+location.search)return!1;if(R(t.href)===R(location.href))return!1;var r=R(t.href);return!e.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var o=new URL(t,location.href);n=o.pathname||"",r=o.hash||""}catch(e){n="",r=""}if("bitrix"===u||"bitrix24"===u){if(-1!==t.indexOf("/bitrix/")||-1!==t.indexOf("sessid="))return!1;if(e.classList.contains("bx-ajax"))return!1}if("tilda"===u&&(-1!==r.indexOf("#popup:")||-1!==r.indexOf("#rec")))return!1;return!/(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(n)&&!/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(t)}(t)&&!!function(e){var t=e.href,n=e.className||"",r="";try{r=new URL(t,location.href).hostname}catch(e){r=""}return-1===n.indexOf("ym-")&&("mc.yandex.ru"!==r&&"metrika.yandex.ru"!==r&&(-1===n.indexOf("ga-")&&-1===n.indexOf("gtm-")&&("google-analytics.com"!==r&&!r.endsWith(".google-analytics.com")&&("googletagmanager.com"!==r&&!r.endsWith(".googletagmanager.com")&&(-1===n.indexOf("piwik")&&-1===n.indexOf("matomo")&&("matomo.org"!==r&&!r.endsWith(".matomo.org")&&"piwik.org"!==r&&!r.endsWith(".piwik.org")))))))}(t))}function R(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function q(t,n){if(P()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(t),o=R(r);if(!e.has(o)){e.size>=m&&e.delete(e.values().next().value),e.add(o);var i=function(e){return y&&"none"!==x?"prerender"!==x?x:L||w||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==i)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",f&&(r.nonce=f);var o={};o[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(o),n.appendChild(r)}(r,i),void(a||!d?U(r,o):_(r,o));a||!d?U(r,o):_(r,o)}}}function _(t,n){var r=document.head;if(r){var o=document.createElement("link");o.rel="prefetch",o.href=t,o.as="document";try{o.fetchPriority="low"}catch(e){}o.onload=i,o.onerror=function(){e.delete(n),i()},r.appendChild(o)}function i(){o.onload=o.onerror=null,o.parentNode&&o.parentNode.removeChild(o)}}function U(t,n){if("function"==typeof fetch){var r=null,o=0;"undefined"!=typeof AbortController&&(r=new AbortController,o=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var i={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(i.signal=r.signal);try{fetch(t,i).then(function(t){o&&clearTimeout(o),t&&t.ok||e.delete(n)}).catch(function(){o&&clearTimeout(o),e.delete(n)})}catch(t){o&&clearTimeout(o),e.delete(n)}}}function B(){A||(A=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(A.unobserve(e.target),D(e.target)&&q(e.target.href,e.target))})},{rootMargin:i?"100px":"200px"}),z())}function z(){A&&document.querySelectorAll("a").forEach(function(e){D(e)&&A.observe(e)})}}();
|
package/package.json
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prefetchru/prefetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Мгновенная загрузка страниц для российского веба. Instant page loading for Russian web.",
|
|
5
5
|
"main": "prefetch.js",
|
|
6
|
-
"module": "prefetch.js",
|
|
6
|
+
"module": "prefetch.esm.js",
|
|
7
7
|
"browser": "dist/prefetch.min.js",
|
|
8
8
|
"unpkg": "dist/prefetch.min.js",
|
|
9
9
|
"jsdelivr": "dist/prefetch.min.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./prefetch.esm.js",
|
|
13
|
+
"require": "./prefetch.js",
|
|
14
|
+
"browser": "./dist/prefetch.min.js"
|
|
15
|
+
},
|
|
16
|
+
"./esm": "./prefetch.esm.js",
|
|
17
|
+
"./esm/min": "./dist/prefetch.esm.min.js"
|
|
18
|
+
},
|
|
10
19
|
"files": [
|
|
11
20
|
"prefetch.js",
|
|
12
|
-
"
|
|
21
|
+
"prefetch.esm.js",
|
|
22
|
+
"dist/prefetch.min.js",
|
|
23
|
+
"dist/prefetch.esm.min.js",
|
|
24
|
+
"README.md"
|
|
13
25
|
],
|
|
14
26
|
"scripts": {
|
|
15
27
|
"minify": "node build.js",
|
package/prefetch.esm.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* prefetch.ru v1.0.9 (ESM) - Мгновенная загрузка страниц
|
|
3
|
+
* © 2026 Сергей Макаров | MIT License
|
|
4
|
+
* https://prefetch.ru | https://github.com/prefetch-ru
|
|
5
|
+
*/
|
|
6
|
+
function createPrefetch(importMetaUrl) {
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
// SSR/Non-browser guard
|
|
10
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
11
|
+
return { __prefetchRu: true, version: '1.0.9', preload: function () {} }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Состояние
|
|
15
|
+
var preloaded = new Set()
|
|
16
|
+
var hoverTimers = new WeakMap()
|
|
17
|
+
|
|
18
|
+
var lastTouchTime = 0
|
|
19
|
+
var touchTimer = 0
|
|
20
|
+
var touchCancel = null
|
|
21
|
+
|
|
22
|
+
var isMobile = false
|
|
23
|
+
var isIOS = false
|
|
24
|
+
var chromiumVer = null
|
|
25
|
+
var platform = null
|
|
26
|
+
var saveData = false
|
|
27
|
+
var connType = null
|
|
28
|
+
|
|
29
|
+
// CSP / поддержка
|
|
30
|
+
var scriptNonce = null
|
|
31
|
+
var supportsLinkPrefetch = false
|
|
32
|
+
|
|
33
|
+
// Настройки
|
|
34
|
+
var hoverDelay = 65
|
|
35
|
+
var touchDelay = 80
|
|
36
|
+
var maxPreloads = 50
|
|
37
|
+
var allowQuery = false
|
|
38
|
+
var allowExternal = false
|
|
39
|
+
var whitelist = false
|
|
40
|
+
|
|
41
|
+
var useSpecRules = false
|
|
42
|
+
var specMode = 'none' // 'none' | 'prefetch' | 'prerender'
|
|
43
|
+
var prerenderAll = false
|
|
44
|
+
|
|
45
|
+
var mousedownMode = false
|
|
46
|
+
var viewportMode = false
|
|
47
|
+
var observeDom = false
|
|
48
|
+
|
|
49
|
+
function detectNonceFromImportMeta(metaUrl) {
|
|
50
|
+
try {
|
|
51
|
+
if (!metaUrl) return null
|
|
52
|
+
// script.src и import.meta.url обычно оба абсолютные → можно сравнивать напрямую
|
|
53
|
+
var scripts = document.getElementsByTagName('script')
|
|
54
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
55
|
+
var s = scripts[i]
|
|
56
|
+
if (!s || !s.src) continue
|
|
57
|
+
if (s.src === metaUrl) {
|
|
58
|
+
var n = s.nonce || s.getAttribute('nonce') || null
|
|
59
|
+
if (n) return n
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Инициализация
|
|
68
|
+
;(function init() {
|
|
69
|
+
// CSP nonce (ESM): через import.meta.url → находим <script type="module" src="..."> и читаем nonce
|
|
70
|
+
scriptNonce = detectNonceFromImportMeta(importMetaUrl)
|
|
71
|
+
|
|
72
|
+
// fallback: на случай окружений, где currentScript всё же доступен
|
|
73
|
+
if (!scriptNonce) {
|
|
74
|
+
try {
|
|
75
|
+
var cs = document.currentScript
|
|
76
|
+
if (cs && cs.nonce) scriptNonce = cs.nonce
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// rel=prefetch support
|
|
81
|
+
try {
|
|
82
|
+
var l = document.createElement('link')
|
|
83
|
+
if (l.relList && typeof l.relList.supports === 'function') {
|
|
84
|
+
supportsLinkPrefetch = l.relList.supports('prefetch')
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {}
|
|
87
|
+
|
|
88
|
+
var ua = navigator.userAgent
|
|
89
|
+
|
|
90
|
+
// Определяем устройство
|
|
91
|
+
isIOS =
|
|
92
|
+
/iPad|iPhone/.test(ua) ||
|
|
93
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
|
94
|
+
var isAndroid = /Android/.test(ua)
|
|
95
|
+
isMobile = (isIOS || isAndroid) && Math.min(screen.width, screen.height) < 768
|
|
96
|
+
if (isMobile) maxPreloads = 20
|
|
97
|
+
|
|
98
|
+
// Chromium версия (для внешних ссылок)
|
|
99
|
+
var cm = ua.match(/Chrome\/(\d+)/)
|
|
100
|
+
if (cm) chromiumVer = parseInt(cm[1], 10)
|
|
101
|
+
|
|
102
|
+
// Сеть
|
|
103
|
+
var conn = navigator.connection
|
|
104
|
+
if (conn) {
|
|
105
|
+
connType = conn.effectiveType
|
|
106
|
+
saveData = conn.saveData || false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Ждём DOM
|
|
110
|
+
if (document.readyState === 'loading') {
|
|
111
|
+
document.addEventListener('DOMContentLoaded', setup)
|
|
112
|
+
} else {
|
|
113
|
+
setup()
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
|
|
117
|
+
function setup() {
|
|
118
|
+
var body = document.body
|
|
119
|
+
if (!body) return
|
|
120
|
+
|
|
121
|
+
platform = detectPlatform()
|
|
122
|
+
|
|
123
|
+
// Читаем конфигурацию
|
|
124
|
+
var ds = body.dataset
|
|
125
|
+
allowQuery = 'prefetchAllowQueryString' in ds || 'instantAllowQueryString' in ds
|
|
126
|
+
allowExternal = 'prefetchAllowExternalLinks' in ds || 'instantAllowExternalLinks' in ds
|
|
127
|
+
whitelist = 'prefetchWhitelist' in ds || 'instantWhitelist' in ds
|
|
128
|
+
|
|
129
|
+
// CSP nonce override через data-*
|
|
130
|
+
// <body data-prefetch-nonce="...">
|
|
131
|
+
if (ds.prefetchNonce) scriptNonce = ds.prefetchNonce
|
|
132
|
+
if (!scriptNonce && ds.instantNonce) scriptNonce = ds.instantNonce
|
|
133
|
+
|
|
134
|
+
// Speculation Rules — opt-in по наличию атрибута:
|
|
135
|
+
// <body data-prefetch-specrules> (prefetch)
|
|
136
|
+
// <body data-prefetch-specrules="prerender">
|
|
137
|
+
// <body data-prefetch-specrules="no">
|
|
138
|
+
var hasSr = 'prefetchSpecrules' in ds || 'instantSpecrules' in ds
|
|
139
|
+
if (
|
|
140
|
+
!isIOS &&
|
|
141
|
+
hasSr &&
|
|
142
|
+
HTMLScriptElement.supports &&
|
|
143
|
+
HTMLScriptElement.supports('speculationrules')
|
|
144
|
+
) {
|
|
145
|
+
var sr = ds.prefetchSpecrules || ds.instantSpecrules
|
|
146
|
+
if (sr === 'prerender') {
|
|
147
|
+
specMode = 'prerender'
|
|
148
|
+
useSpecRules = true
|
|
149
|
+
} else if (sr !== 'no') {
|
|
150
|
+
specMode = 'prefetch'
|
|
151
|
+
useSpecRules = true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Разрешить "глобальный" prerender без whitelist (не рекомендуется, но бывает нужно)
|
|
156
|
+
// <body data-prefetch-prerender-all>
|
|
157
|
+
prerenderAll = 'prefetchPrerenderAll' in ds || 'instantPrerenderAll' in ds
|
|
158
|
+
|
|
159
|
+
// Интенсивность
|
|
160
|
+
var intensity = ds.prefetchIntensity || ds.instantIntensity
|
|
161
|
+
if (intensity === 'mousedown') {
|
|
162
|
+
mousedownMode = true
|
|
163
|
+
} else if (intensity === 'viewport' || intensity === 'viewport-all') {
|
|
164
|
+
if (intensity === 'viewport-all' || (isMobile && isNetworkOk())) {
|
|
165
|
+
viewportMode = true
|
|
166
|
+
}
|
|
167
|
+
} else if (intensity) {
|
|
168
|
+
var d = parseInt(intensity, 10)
|
|
169
|
+
if (!isNaN(d) && d >= 0) hoverDelay = d
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// На мобильных делаем touch-предзагрузку менее агрессивной
|
|
173
|
+
if (isMobile) {
|
|
174
|
+
touchDelay = Math.max(60, Math.min(hoverDelay || 0, 150))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// DOM observer для SPA
|
|
178
|
+
observeDom = 'prefetchObserveDom' in ds
|
|
179
|
+
if (!observeDom && (platform === 'bitrix' || platform === 'tilda')) {
|
|
180
|
+
observeDom = true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Tilda — увеличиваем задержку из-за popup-ов
|
|
184
|
+
if (platform === 'tilda' && hoverDelay < 100) hoverDelay = 100
|
|
185
|
+
|
|
186
|
+
// События
|
|
187
|
+
var opts = { capture: true, passive: true }
|
|
188
|
+
document.addEventListener('touchstart', onTouchStart, opts)
|
|
189
|
+
if (!mousedownMode) {
|
|
190
|
+
document.addEventListener('mouseover', onMouseOver, opts)
|
|
191
|
+
} else {
|
|
192
|
+
document.addEventListener('mousedown', onMouseDown, opts)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Viewport observer
|
|
196
|
+
if (viewportMode) {
|
|
197
|
+
var rIC = window.requestIdleCallback || function (cb) { setTimeout(cb, 1) }
|
|
198
|
+
rIC(startViewportObserver, { timeout: 1500 })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// v1.0.9: MutationObserver нужен только для viewport режима (отслеживать новые ссылки)
|
|
202
|
+
if (observeDom && viewportMode) startMutationObserver()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function detectPlatform() {
|
|
206
|
+
if (typeof window.BX !== 'undefined') return 'bitrix'
|
|
207
|
+
if (typeof window.B24 !== 'undefined' || typeof window.BX24 !== 'undefined') return 'bitrix24'
|
|
208
|
+
if (document.querySelector('.t-records') || typeof window.Tilda !== 'undefined') return 'tilda'
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isNetworkOk() {
|
|
213
|
+
if (saveData) return false
|
|
214
|
+
if (connType === 'slow-2g' || connType === '2g') return false
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getAnchorFromEventTarget(t) {
|
|
219
|
+
if (!t) return null
|
|
220
|
+
if (t.nodeType && t.nodeType !== 1) t = t.parentElement
|
|
221
|
+
if (!t || typeof t.closest !== 'function') return null
|
|
222
|
+
return t.closest('a')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function onTouchStart(e) {
|
|
226
|
+
// v1.0.9: используем Date.now() для единой шкалы времени
|
|
227
|
+
lastTouchTime = Date.now()
|
|
228
|
+
|
|
229
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
230
|
+
if (!canPreload(a)) return
|
|
231
|
+
|
|
232
|
+
// задержка + отмена на scroll/touchmove
|
|
233
|
+
if (touchTimer) {
|
|
234
|
+
clearTimeout(touchTimer)
|
|
235
|
+
touchTimer = 0
|
|
236
|
+
}
|
|
237
|
+
if (touchCancel) {
|
|
238
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
239
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
240
|
+
touchCancel = null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var cancelled = false
|
|
244
|
+
touchCancel = function () {
|
|
245
|
+
cancelled = true
|
|
246
|
+
if (touchTimer) {
|
|
247
|
+
clearTimeout(touchTimer)
|
|
248
|
+
touchTimer = 0
|
|
249
|
+
}
|
|
250
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
251
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
252
|
+
touchCancel = null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('touchmove', touchCancel, { capture: true, passive: true, once: true })
|
|
256
|
+
document.addEventListener('scroll', touchCancel, { capture: true, passive: true, once: true })
|
|
257
|
+
|
|
258
|
+
touchTimer = setTimeout(function () {
|
|
259
|
+
if (touchCancel) {
|
|
260
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
261
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
262
|
+
touchCancel = null
|
|
263
|
+
}
|
|
264
|
+
touchTimer = 0
|
|
265
|
+
if (!cancelled) preload(a.href, a)
|
|
266
|
+
}, touchDelay)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function onMouseOver(e) {
|
|
270
|
+
// v1.0.9: единая шкала времени Date.now()
|
|
271
|
+
if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
|
|
272
|
+
|
|
273
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
274
|
+
if (!canPreload(a)) return
|
|
275
|
+
|
|
276
|
+
// v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
|
|
277
|
+
if (hoverTimers.has(a)) return
|
|
278
|
+
|
|
279
|
+
// mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
|
|
280
|
+
a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
|
|
281
|
+
|
|
282
|
+
var t = setTimeout(function () {
|
|
283
|
+
preload(a.href, a)
|
|
284
|
+
hoverTimers.delete(a)
|
|
285
|
+
}, hoverDelay)
|
|
286
|
+
hoverTimers.set(a, t)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function onMouseLeave(e) {
|
|
290
|
+
var a = e.currentTarget
|
|
291
|
+
if (!a) return
|
|
292
|
+
|
|
293
|
+
var t = hoverTimers.get(a)
|
|
294
|
+
if (t) {
|
|
295
|
+
clearTimeout(t)
|
|
296
|
+
hoverTimers.delete(a)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function onMouseDown(e) {
|
|
301
|
+
if (typeof e.button === 'number' && e.button === 2) return
|
|
302
|
+
// v1.0.9: единая шкала времени Date.now()
|
|
303
|
+
if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
|
|
304
|
+
|
|
305
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
306
|
+
if (canPreload(a)) preload(a.href, a)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function canPreload(a) {
|
|
310
|
+
if (!a) return false
|
|
311
|
+
|
|
312
|
+
// v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
|
|
313
|
+
var hrefAttr = a.getAttribute('href')
|
|
314
|
+
if (hrefAttr === null || hrefAttr.trim() === '') return false
|
|
315
|
+
|
|
316
|
+
if (!a.href) return false
|
|
317
|
+
|
|
318
|
+
// Не навигация в текущей вкладке
|
|
319
|
+
if (a.target && a.target !== '_self') return false
|
|
320
|
+
if (a.hasAttribute('download')) return false
|
|
321
|
+
|
|
322
|
+
// Явный запрет
|
|
323
|
+
if ('noPrefetch' in a.dataset || 'prefetchNo' in a.dataset) return false
|
|
324
|
+
|
|
325
|
+
// Белый список
|
|
326
|
+
if (whitelist && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
327
|
+
|
|
328
|
+
// Протокол
|
|
329
|
+
if (a.protocol !== 'http:' && a.protocol !== 'https:') return false
|
|
330
|
+
if (a.protocol === 'http:' && location.protocol === 'https:') return false
|
|
331
|
+
|
|
332
|
+
// Внешние ссылки
|
|
333
|
+
if (a.origin !== location.origin) {
|
|
334
|
+
if (!allowExternal && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
335
|
+
if (!chromiumVer) return false
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Query string
|
|
339
|
+
if (a.search && !allowQuery && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
340
|
+
|
|
341
|
+
// Якорь на той же странице
|
|
342
|
+
if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
|
|
343
|
+
|
|
344
|
+
// v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
|
|
345
|
+
if (urlKey(a.href) === urlKey(location.href)) return false
|
|
346
|
+
|
|
347
|
+
// Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
|
|
348
|
+
var key = urlKey(a.href)
|
|
349
|
+
if (preloaded.has(key)) return false
|
|
350
|
+
|
|
351
|
+
if (!checkPlatform(a)) return false
|
|
352
|
+
if (!checkAnalytics(a)) return false
|
|
353
|
+
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function checkPlatform(a) {
|
|
358
|
+
var href = a.href
|
|
359
|
+
|
|
360
|
+
// v1.0.7: для точных проверок /add /delete /remove используем pathname
|
|
361
|
+
var pathname = ''
|
|
362
|
+
var hash = ''
|
|
363
|
+
try {
|
|
364
|
+
var u = new URL(href, location.href)
|
|
365
|
+
pathname = u.pathname || ''
|
|
366
|
+
hash = u.hash || ''
|
|
367
|
+
} catch (e) {
|
|
368
|
+
pathname = ''
|
|
369
|
+
hash = ''
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (platform === 'bitrix' || platform === 'bitrix24') {
|
|
373
|
+
if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
|
|
374
|
+
if (a.classList.contains('bx-ajax')) return false
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (platform === 'tilda') {
|
|
378
|
+
if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// v1.0.9: все опасные пути проверяем по сегментам pathname, не по подстроке href
|
|
382
|
+
// Это исправляет ложные блокировки /author, /cartoon, /authentication и т.д.
|
|
383
|
+
var isDangerousPath = /(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(pathname)
|
|
384
|
+
|
|
385
|
+
if (isDangerousPath) return false
|
|
386
|
+
|
|
387
|
+
if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
|
|
388
|
+
|
|
389
|
+
return true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkAnalytics(a) {
|
|
393
|
+
var href = a.href
|
|
394
|
+
var cls = a.className || ''
|
|
395
|
+
|
|
396
|
+
// Извлекаем hostname для проверки доменов аналитики
|
|
397
|
+
var host = ''
|
|
398
|
+
try { host = new URL(href, location.href).hostname } catch (e) { host = '' }
|
|
399
|
+
|
|
400
|
+
if (cls.indexOf('ym-') !== -1) return false
|
|
401
|
+
if (host === 'mc.yandex.ru' || host === 'metrika.yandex.ru') return false
|
|
402
|
+
|
|
403
|
+
if (cls.indexOf('ga-') !== -1 || cls.indexOf('gtm-') !== -1) return false
|
|
404
|
+
if (host === 'google-analytics.com' || host.endsWith('.google-analytics.com')) return false
|
|
405
|
+
if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
|
|
406
|
+
|
|
407
|
+
if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
|
|
408
|
+
if (
|
|
409
|
+
host === 'matomo.org' ||
|
|
410
|
+
host.endsWith('.matomo.org') ||
|
|
411
|
+
host === 'piwik.org' ||
|
|
412
|
+
host.endsWith('.piwik.org')
|
|
413
|
+
) return false
|
|
414
|
+
|
|
415
|
+
return true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Ключ для дедупликации: НЕ трогаем pathname (включая / на конце), только убираем hash
|
|
419
|
+
function urlKey(url) {
|
|
420
|
+
try {
|
|
421
|
+
var u = new URL(url, location.href)
|
|
422
|
+
return u.origin + u.pathname + u.search
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return url
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// URL для реального запроса: абсолютный, без hash, без "улучшений"
|
|
429
|
+
function urlForRequest(url) {
|
|
430
|
+
try {
|
|
431
|
+
var u = new URL(url, location.href)
|
|
432
|
+
u.hash = ''
|
|
433
|
+
return u.href
|
|
434
|
+
} catch (e) {
|
|
435
|
+
return url
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolveSpecMode(a) {
|
|
440
|
+
if (!useSpecRules || specMode === 'none') return 'none'
|
|
441
|
+
if (specMode !== 'prerender') return specMode
|
|
442
|
+
|
|
443
|
+
// specMode === 'prerender'
|
|
444
|
+
if (prerenderAll) return 'prerender'
|
|
445
|
+
if (whitelist) return 'prerender' // ссылки и так явно размечены
|
|
446
|
+
|
|
447
|
+
// иначе prerender только по ссылке:
|
|
448
|
+
if (a && a.dataset && (('prefetchPrerender' in a.dataset) || ('instantPrerender' in a.dataset))) {
|
|
449
|
+
return 'prerender'
|
|
450
|
+
}
|
|
451
|
+
return 'prefetch'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function preload(url, a) {
|
|
455
|
+
if (!isNetworkOk()) return
|
|
456
|
+
|
|
457
|
+
var requestUrl = urlForRequest(url)
|
|
458
|
+
var key = urlKey(requestUrl)
|
|
459
|
+
|
|
460
|
+
if (preloaded.has(key)) return
|
|
461
|
+
if (preloaded.size >= maxPreloads) {
|
|
462
|
+
preloaded.delete(preloaded.values().next().value)
|
|
463
|
+
}
|
|
464
|
+
preloaded.add(key)
|
|
465
|
+
|
|
466
|
+
var mode = resolveSpecMode(a)
|
|
467
|
+
|
|
468
|
+
if (mode !== 'none') {
|
|
469
|
+
preloadSpec(requestUrl, mode)
|
|
470
|
+
|
|
471
|
+
// Страховка: обычный prefetch (чтобы под CSP/ограничениями SpecRules всё равно грелся кэш)
|
|
472
|
+
if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
|
|
473
|
+
else preloadLink(requestUrl, key)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
|
|
478
|
+
else preloadLink(requestUrl, key)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function preloadSpec(url, mode) {
|
|
482
|
+
var head = document.head
|
|
483
|
+
if (!head) return
|
|
484
|
+
|
|
485
|
+
var s = document.createElement('script')
|
|
486
|
+
s.type = 'speculationrules'
|
|
487
|
+
if (scriptNonce) s.nonce = scriptNonce
|
|
488
|
+
|
|
489
|
+
var rules = {}
|
|
490
|
+
rules[mode] = [{ source: 'list', urls: [url] }]
|
|
491
|
+
s.textContent = JSON.stringify(rules)
|
|
492
|
+
head.appendChild(s)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function preloadLink(url, key) {
|
|
496
|
+
var head = document.head
|
|
497
|
+
if (!head) return
|
|
498
|
+
|
|
499
|
+
var l = document.createElement('link')
|
|
500
|
+
l.rel = 'prefetch'
|
|
501
|
+
l.href = url
|
|
502
|
+
l.as = 'document'
|
|
503
|
+
try { l.fetchPriority = 'low' } catch (e) {}
|
|
504
|
+
|
|
505
|
+
// v1.0.9: удаляем link после загрузки, чтобы не раздувать DOM
|
|
506
|
+
function cleanup() {
|
|
507
|
+
l.onload = l.onerror = null
|
|
508
|
+
if (l.parentNode) l.parentNode.removeChild(l)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
l.onload = cleanup
|
|
512
|
+
l.onerror = function () { preloaded.delete(key); cleanup() }
|
|
513
|
+
head.appendChild(l)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function preloadFetch(url, key) {
|
|
517
|
+
if (typeof fetch !== 'function') return
|
|
518
|
+
|
|
519
|
+
var ctrl = null
|
|
520
|
+
var tid = 0
|
|
521
|
+
|
|
522
|
+
if (typeof AbortController !== 'undefined') {
|
|
523
|
+
ctrl = new AbortController()
|
|
524
|
+
tid = setTimeout(function () {
|
|
525
|
+
try { ctrl.abort() } catch (e) {}
|
|
526
|
+
}, 5000)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
var opts = {
|
|
530
|
+
method: 'GET',
|
|
531
|
+
credentials: 'same-origin',
|
|
532
|
+
cache: 'force-cache',
|
|
533
|
+
headers: { Purpose: 'prefetch' }
|
|
534
|
+
}
|
|
535
|
+
if (ctrl) opts.signal = ctrl.signal
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
fetch(url, opts)
|
|
539
|
+
.then(function (r) {
|
|
540
|
+
if (tid) clearTimeout(tid)
|
|
541
|
+
if (!r || !r.ok) preloaded.delete(key)
|
|
542
|
+
})
|
|
543
|
+
.catch(function () {
|
|
544
|
+
if (tid) clearTimeout(tid)
|
|
545
|
+
preloaded.delete(key)
|
|
546
|
+
})
|
|
547
|
+
} catch (e) {
|
|
548
|
+
if (tid) clearTimeout(tid)
|
|
549
|
+
preloaded.delete(key)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Viewport Observer
|
|
554
|
+
var vpObserver = null
|
|
555
|
+
|
|
556
|
+
function startViewportObserver() {
|
|
557
|
+
if (vpObserver) return
|
|
558
|
+
vpObserver = new IntersectionObserver(
|
|
559
|
+
function (entries) {
|
|
560
|
+
entries.forEach(function (entry) {
|
|
561
|
+
if (entry.isIntersecting) {
|
|
562
|
+
vpObserver.unobserve(entry.target)
|
|
563
|
+
if (canPreload(entry.target)) preload(entry.target.href, entry.target)
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
},
|
|
567
|
+
{ rootMargin: isMobile ? '100px' : '200px' }
|
|
568
|
+
)
|
|
569
|
+
observeLinks()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function observeLinks() {
|
|
573
|
+
if (!vpObserver) return
|
|
574
|
+
document.querySelectorAll('a').forEach(function (a) {
|
|
575
|
+
if (canPreload(a)) vpObserver.observe(a)
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Mutation Observer
|
|
580
|
+
var mutObserver = null
|
|
581
|
+
var mutTimer = null
|
|
582
|
+
|
|
583
|
+
function startMutationObserver() {
|
|
584
|
+
if (mutObserver) return
|
|
585
|
+
mutObserver = new MutationObserver(function (muts) {
|
|
586
|
+
var hasLinks = muts.some(function (m) {
|
|
587
|
+
return Array.from(m.addedNodes).some(function (n) {
|
|
588
|
+
return (
|
|
589
|
+
n.nodeType === 1 &&
|
|
590
|
+
(n.tagName === 'A' || (n.querySelectorAll && n.querySelectorAll('a').length))
|
|
591
|
+
)
|
|
592
|
+
})
|
|
593
|
+
})
|
|
594
|
+
if (hasLinks && vpObserver) {
|
|
595
|
+
clearTimeout(mutTimer)
|
|
596
|
+
mutTimer = setTimeout(observeLinks, 100)
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
mutObserver.observe(document.body, { childList: true, subtree: true })
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Минимальный публичный API
|
|
603
|
+
var api = {
|
|
604
|
+
__prefetchRu: true,
|
|
605
|
+
version: '1.0.9',
|
|
606
|
+
preload: function (url) { preload(url) }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
window.Prefetch = api
|
|
610
|
+
return api
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// v1.0.9: Guard от двойной инициализации (проверяем маркер __prefetchRu)
|
|
614
|
+
var Prefetch =
|
|
615
|
+
(typeof window !== 'undefined' && window.Prefetch && window.Prefetch.__prefetchRu)
|
|
616
|
+
? window.Prefetch
|
|
617
|
+
: createPrefetch(import.meta.url)
|
|
618
|
+
|
|
619
|
+
export { Prefetch }
|
|
620
|
+
export default Prefetch
|
package/prefetch.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* prefetch.ru v1.0.
|
|
2
|
+
* prefetch.ru v1.0.9 - Мгновенная загрузка страниц
|
|
3
3
|
* © 2026 Сергей Макаров | MIT License
|
|
4
4
|
* https://prefetch.ru | https://github.com/prefetch-ru
|
|
5
5
|
*/
|
|
6
6
|
;(function () {
|
|
7
7
|
'use strict'
|
|
8
8
|
|
|
9
|
+
// v1.0.9: guard от двойной инициализации
|
|
10
|
+
if (window.Prefetch && window.Prefetch.__prefetchRu) return
|
|
11
|
+
|
|
9
12
|
// Состояние
|
|
10
13
|
var preloaded = new Set()
|
|
11
14
|
var hoverTimers = new WeakMap()
|
|
@@ -170,8 +173,8 @@
|
|
|
170
173
|
rIC(startViewportObserver, { timeout: 1500 })
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
//
|
|
174
|
-
if (observeDom) startMutationObserver()
|
|
176
|
+
// v1.0.9: MutationObserver нужен только для viewport режима (отслеживать новые ссылки)
|
|
177
|
+
if (observeDom && viewportMode) startMutationObserver()
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
function detectPlatform() {
|
|
@@ -195,7 +198,8 @@
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
function onTouchStart(e) {
|
|
198
|
-
|
|
201
|
+
// v1.0.9: используем Date.now() для единой шкалы времени
|
|
202
|
+
lastTouchTime = Date.now()
|
|
199
203
|
|
|
200
204
|
var a = getAnchorFromEventTarget(e.target)
|
|
201
205
|
if (!canPreload(a)) return
|
|
@@ -238,7 +242,8 @@
|
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
function onMouseOver(e) {
|
|
241
|
-
|
|
245
|
+
// v1.0.9: единая шкала времени Date.now()
|
|
246
|
+
if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
|
|
242
247
|
|
|
243
248
|
var a = getAnchorFromEventTarget(e.target)
|
|
244
249
|
if (!canPreload(a)) return
|
|
@@ -269,7 +274,8 @@
|
|
|
269
274
|
|
|
270
275
|
function onMouseDown(e) {
|
|
271
276
|
if (typeof e.button === 'number' && e.button === 2) return
|
|
272
|
-
|
|
277
|
+
// v1.0.9: единая шкала времени Date.now()
|
|
278
|
+
if (lastTouchTime && Date.now() - lastTouchTime < 2500) return
|
|
273
279
|
|
|
274
280
|
var a = getAnchorFromEventTarget(e.target)
|
|
275
281
|
if (canPreload(a)) preload(a.href, a)
|
|
@@ -348,18 +354,11 @@
|
|
|
348
354
|
if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
|
|
349
355
|
}
|
|
350
356
|
|
|
351
|
-
// v1.0.
|
|
352
|
-
|
|
357
|
+
// v1.0.9: все опасные пути проверяем по сегментам pathname, не по подстроке href
|
|
358
|
+
// Это исправляет ложные блокировки /author, /cartoon, /authentication и т.д.
|
|
359
|
+
var isDangerousPath = /(^|\/)(login|logout|auth|register|cart|basket|add|delete|remove)(\/|$|\.)/i.test(pathname)
|
|
353
360
|
|
|
354
|
-
if (
|
|
355
|
-
href.indexOf('/login') !== -1 ||
|
|
356
|
-
href.indexOf('/logout') !== -1 ||
|
|
357
|
-
href.indexOf('/auth') !== -1 ||
|
|
358
|
-
href.indexOf('/register') !== -1 ||
|
|
359
|
-
href.indexOf('/cart') !== -1 ||
|
|
360
|
-
href.indexOf('/basket') !== -1 ||
|
|
361
|
-
isActionPath
|
|
362
|
-
) return false
|
|
361
|
+
if (isDangerousPath) return false
|
|
363
362
|
|
|
364
363
|
if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
|
|
365
364
|
|
|
@@ -479,7 +478,14 @@
|
|
|
479
478
|
l.as = 'document'
|
|
480
479
|
try { l.fetchPriority = 'low' } catch (e) {}
|
|
481
480
|
|
|
482
|
-
|
|
481
|
+
// v1.0.9: удаляем link после загрузки, чтобы не раздувать DOM
|
|
482
|
+
function cleanup() {
|
|
483
|
+
l.onload = l.onerror = null
|
|
484
|
+
if (l.parentNode) l.parentNode.removeChild(l)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
l.onload = cleanup
|
|
488
|
+
l.onerror = function () { preloaded.delete(key); cleanup() }
|
|
483
489
|
head.appendChild(l)
|
|
484
490
|
}
|
|
485
491
|
|
|
@@ -571,7 +577,8 @@
|
|
|
571
577
|
|
|
572
578
|
// Минимальный публичный API
|
|
573
579
|
window.Prefetch = {
|
|
574
|
-
|
|
580
|
+
__prefetchRu: true,
|
|
581
|
+
version: '1.0.9',
|
|
575
582
|
preload: function (url) { preload(url) }
|
|
576
583
|
}
|
|
577
584
|
})()
|