@prefetchru/prefetch 1.0.6 → 1.0.8
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 +614 -0
- package/prefetch.js +50 -14
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.8"
|
|
102
141
|
|
|
103
142
|
// Программная предзагрузка URL
|
|
104
143
|
Prefetch.preload('/catalog/product-123')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* prefetch.ru v1.0.8 (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:function(e){if("undefined"==typeof window||"undefined"==typeof document)return{version:"1.0.8",preload:function(){}};var t=new Set,n=new WeakMap,r=0,i=0,o=null,a=!1,c=!1,u=null,l=null,s=!1,f=null,d=null,h=!1,m=65,p=80,v=50,g=!1,w=!1,y=!1,x=!1,b="none",L=!1,E=!1,T=!1,S=!1;function O(){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&&A())&&(T=!0);else if(r){var i=parseInt(r,10);!isNaN(i)&&i>=0&&(m=i)}a&&(p=Math.max(60,Math.min(m||0,150))),(S="prefetchObserveDom"in t)||"bitrix"!==l&&"tilda"!==l||(S=!0),"tilda"===l&&m<100&&(m=100);var o={capture:!0,passive:!0};if(document.addEventListener("touchstart",P,o),E?document.addEventListener("mousedown",C,o):document.addEventListener("mouseover",N,o),T)(window.requestIdleCallback||function(e){setTimeout(e,1)})(U,{timeout:1500});S&&function(){if(H)return;(H=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)})})&&R&&(clearTimeout(Q),Q=setTimeout(z,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function A(){return!s&&("slow-2g"!==f&&"2g"!==f)}function k(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=e.timeStamp||Date.now();var t=k(e.target);if(I(t)){i&&(clearTimeout(i),i=0),o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null);var n=!1;o=function(){n=!0,i&&(clearTimeout(i),i=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}),i=setTimeout(function(){o&&(document.removeEventListener("touchmove",o,!0),document.removeEventListener("scroll",o,!0),o=null),i=0,n||q(t.href,t)},p)}}function N(e){if(!(r&&e.timeStamp&&e.timeStamp-r<2500)){var t=k(e.target);if(I(t)&&!n.has(t)){t.addEventListener("mouseleave",M,{passive:!0,once:!0});var i=setTimeout(function(){q(t.href,t),n.delete(t)},m);n.set(t,i)}}}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&&e.timeStamp&&e.timeStamp-r<2500)){var t=k(e.target);I(t)&&q(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(W(e.href)===W(location.href))return!1;var r=W(e.href);return!t.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var i=new URL(t,location.href);n=i.pathname||"",r=i.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;var o=/(^|\/)(add|delete|remove)(\/|$|\.)/i.test(n);return-1===t.indexOf("/login")&&-1===t.indexOf("/logout")&&-1===t.indexOf("/auth")&&-1===t.indexOf("/register")&&-1===t.indexOf("/cart")&&-1===t.indexOf("/basket")&&!o&&!/\.(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 W(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function q(e,n){if(A()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(e),i=W(r);if(!t.has(i)){t.size>=v&&t.delete(t.values().next().value),t.add(i);var o=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"!==o)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",d&&(r.nonce=d);var i={};i[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(i),n.appendChild(r)}(r,o),void(c||!h?D(r,i):B(r,i));c||!h?D(r,i):B(r,i)}}}function B(e,n){var r=document.head;if(r){var i=document.createElement("link");i.rel="prefetch",i.href=e,i.as="document";try{i.fetchPriority="low"}catch(e){}i.onerror=function(){t.delete(n)},r.appendChild(i)}}function D(e,n){if("function"==typeof fetch){var r=null,i=0;"undefined"!=typeof AbortController&&(r=new AbortController,i=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var o={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(o.signal=r.signal);try{fetch(e,o).then(function(e){i&&clearTimeout(i),e&&e.ok||t.delete(n)}).catch(function(){i&&clearTimeout(i),t.delete(n)})}catch(e){i&&clearTimeout(i),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 i=r.nonce||r.getAttribute("nonce")||null;if(i)return i;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 i=/Android/.test(r);(a=(c||i)&&Math.min(screen.width,screen.height)<768)&&(v=20);var o=r.match(/Chrome\/(\d+)/);o&&(u=parseInt(o[1],10));var l=navigator.connection;l&&(f=l.effectiveType,s=l.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",O):O()}();var R=null;function U(){R||(R=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(R.unobserve(e.target),I(e.target)&&q(e.target.href,e.target))})},{rootMargin:a?"100px":"200px"}),z())}function z(){R&&document.querySelectorAll("a").forEach(function(e){I(e)&&R.observe(e)})}var H=null,Q=null,X={version:"1.0.8",preload:function(e){q(e)}};return window.Prefetch=X,X}(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.8 - Мгновенная загрузка страниц
|
|
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,i=null,o=!1,a=!1,c=null,u=null,s=!1,l=null,
|
|
6
|
+
!function(){"use strict";var e=new Set,t=new WeakMap,n=0,r=0,i=null,o=!1,a=!1,c=null,u=null,s=!1,l=null,f=null,d=!1,h=65,m=80,p=50,v=!1,g=!1,w=!1,x=!1,y="none",L=!1,b=!1,E=!1,T=!1;function S(){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?(y="prerender",x=!0):"no"!==n&&(y="prefetch",x=!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||o&&O())&&(E=!0);else if(r){var i=parseInt(r,10);!isNaN(i)&&i>=0&&(h=i)}o&&(m=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",k,c),b?document.addEventListener("mousedown",M,c):document.addEventListener("mouseover",N,c),E)(window.requestIdleCallback||function(e){setTimeout(e,1)})(U,{timeout:1500});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)})})&&R&&(clearTimeout(H),H=setTimeout(B,100))})).observe(document.body,{childList:!0,subtree:!0})}()}}function O(){return!s&&("slow-2g"!==l&&"2g"!==l)}function A(e){return e?(e.nodeType&&1!==e.nodeType&&(e=e.parentElement),e&&"function"==typeof e.closest?e.closest("a"):null):null}function k(e){n=e.timeStamp||Date.now();var t=A(e.target);if(C(t)){r&&(clearTimeout(r),r=0),i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null);var o=!1;i=function(){o=!0,r&&(clearTimeout(r),r=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}),r=setTimeout(function(){i&&(document.removeEventListener("touchmove",i,!0),document.removeEventListener("scroll",i,!0),i=null),r=0,o||W(t.href,t)},m)}}function N(e){if(!(n&&e.timeStamp&&e.timeStamp-n<2500)){var r=A(e.target);if(C(r)&&!t.has(r)){r.addEventListener("mouseleave",P,{passive:!0,once:!0});var i=setTimeout(function(){W(r.href,r),t.delete(r)},h);t.set(r,i)}}}function P(e){var n=e.currentTarget;if(n){var r=t.get(n);r&&(clearTimeout(r),t.delete(n))}}function M(e){if(!("number"==typeof e.button&&2===e.button||n&&e.timeStamp&&e.timeStamp-n<2500)){var t=A(e.target);C(t)&&W(t.href,t)}}function C(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(I(t.href)===I(location.href))return!1;var r=I(t.href);return!e.has(r)&&(!!function(e){var t=e.href,n="",r="";try{var i=new URL(t,location.href);n=i.pathname||"",r=i.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;var o=/(^|\/)(add|delete|remove)(\/|$|\.)/i.test(n);return-1===t.indexOf("/login")&&-1===t.indexOf("/logout")&&-1===t.indexOf("/auth")&&-1===t.indexOf("/register")&&-1===t.indexOf("/cart")&&-1===t.indexOf("/basket")&&!o&&!/\.(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 I(e){try{var t=new URL(e,location.href);return t.origin+t.pathname+t.search}catch(t){return e}}function W(t,n){if(O()){var r=function(e){try{var t=new URL(e,location.href);return t.hash="",t.href}catch(t){return e}}(t),i=I(r);if(!e.has(i)){e.size>=p&&e.delete(e.values().next().value),e.add(i);var o=function(e){return x&&"none"!==y?"prerender"!==y?y:L||w||e&&e.dataset&&("prefetchPrerender"in e.dataset||"instantPrerender"in e.dataset)?"prerender":"prefetch":"none"}(n);if("none"!==o)return function(e,t){var n=document.head;if(!n)return;var r=document.createElement("script");r.type="speculationrules",f&&(r.nonce=f);var i={};i[t]=[{source:"list",urls:[e]}],r.textContent=JSON.stringify(i),n.appendChild(r)}(r,o),void(a||!d?D(r,i):q(r,i));a||!d?D(r,i):q(r,i)}}}function q(t,n){var r=document.head;if(r){var i=document.createElement("link");i.rel="prefetch",i.href=t,i.as="document";try{i.fetchPriority="low"}catch(e){}i.onerror=function(){e.delete(n)},r.appendChild(i)}}function D(t,n){if("function"==typeof fetch){var r=null,i=0;"undefined"!=typeof AbortController&&(r=new AbortController,i=setTimeout(function(){try{r.abort()}catch(e){}},5e3));var o={method:"GET",credentials:"same-origin",cache:"force-cache",headers:{Purpose:"prefetch"}};r&&(o.signal=r.signal);try{fetch(t,o).then(function(t){i&&clearTimeout(i),t&&t.ok||e.delete(n)}).catch(function(){i&&clearTimeout(i),e.delete(n)})}catch(t){i&&clearTimeout(i),e.delete(n)}}}!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);(o=(a||r)&&Math.min(screen.width,screen.height)<768)&&(p=20);var i=n.match(/Chrome\/(\d+)/);i&&(c=parseInt(i[1],10));var u=navigator.connection;u&&(l=u.effectiveType,s=u.saveData||!1),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",S):S()}();var R=null;function U(){R||(R=new IntersectionObserver(function(e){e.forEach(function(e){e.isIntersecting&&(R.unobserve(e.target),C(e.target)&&W(e.target.href,e.target))})},{rootMargin:o?"100px":"200px"}),B())}function B(){R&&document.querySelectorAll("a").forEach(function(e){C(e)&&R.observe(e)})}var z=null,H=null;window.Prefetch={version:"1.0.8",preload:function(e){W(e)}}}();
|
package/package.json
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prefetchru/prefetch",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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,614 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* prefetch.ru v1.0.8 (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 { version: '1.0.8', 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
|
+
// Mutation observer
|
|
202
|
+
if (observeDom) 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
|
+
lastTouchTime = e.timeStamp || Date.now()
|
|
227
|
+
|
|
228
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
229
|
+
if (!canPreload(a)) return
|
|
230
|
+
|
|
231
|
+
// задержка + отмена на scroll/touchmove
|
|
232
|
+
if (touchTimer) {
|
|
233
|
+
clearTimeout(touchTimer)
|
|
234
|
+
touchTimer = 0
|
|
235
|
+
}
|
|
236
|
+
if (touchCancel) {
|
|
237
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
238
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
239
|
+
touchCancel = null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
var cancelled = false
|
|
243
|
+
touchCancel = function () {
|
|
244
|
+
cancelled = true
|
|
245
|
+
if (touchTimer) {
|
|
246
|
+
clearTimeout(touchTimer)
|
|
247
|
+
touchTimer = 0
|
|
248
|
+
}
|
|
249
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
250
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
251
|
+
touchCancel = null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
document.addEventListener('touchmove', touchCancel, { capture: true, passive: true, once: true })
|
|
255
|
+
document.addEventListener('scroll', touchCancel, { capture: true, passive: true, once: true })
|
|
256
|
+
|
|
257
|
+
touchTimer = setTimeout(function () {
|
|
258
|
+
if (touchCancel) {
|
|
259
|
+
document.removeEventListener('touchmove', touchCancel, true)
|
|
260
|
+
document.removeEventListener('scroll', touchCancel, true)
|
|
261
|
+
touchCancel = null
|
|
262
|
+
}
|
|
263
|
+
touchTimer = 0
|
|
264
|
+
if (!cancelled) preload(a.href, a)
|
|
265
|
+
}, touchDelay)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onMouseOver(e) {
|
|
269
|
+
if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
|
|
270
|
+
|
|
271
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
272
|
+
if (!canPreload(a)) return
|
|
273
|
+
|
|
274
|
+
// v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
|
|
275
|
+
if (hoverTimers.has(a)) return
|
|
276
|
+
|
|
277
|
+
// mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
|
|
278
|
+
a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
|
|
279
|
+
|
|
280
|
+
var t = setTimeout(function () {
|
|
281
|
+
preload(a.href, a)
|
|
282
|
+
hoverTimers.delete(a)
|
|
283
|
+
}, hoverDelay)
|
|
284
|
+
hoverTimers.set(a, t)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function onMouseLeave(e) {
|
|
288
|
+
var a = e.currentTarget
|
|
289
|
+
if (!a) return
|
|
290
|
+
|
|
291
|
+
var t = hoverTimers.get(a)
|
|
292
|
+
if (t) {
|
|
293
|
+
clearTimeout(t)
|
|
294
|
+
hoverTimers.delete(a)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function onMouseDown(e) {
|
|
299
|
+
if (typeof e.button === 'number' && e.button === 2) return
|
|
300
|
+
if (lastTouchTime && e.timeStamp && e.timeStamp - lastTouchTime < 2500) return
|
|
301
|
+
|
|
302
|
+
var a = getAnchorFromEventTarget(e.target)
|
|
303
|
+
if (canPreload(a)) preload(a.href, a)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function canPreload(a) {
|
|
307
|
+
if (!a) return false
|
|
308
|
+
|
|
309
|
+
// v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
|
|
310
|
+
var hrefAttr = a.getAttribute('href')
|
|
311
|
+
if (hrefAttr === null || hrefAttr.trim() === '') return false
|
|
312
|
+
|
|
313
|
+
if (!a.href) return false
|
|
314
|
+
|
|
315
|
+
// Не навигация в текущей вкладке
|
|
316
|
+
if (a.target && a.target !== '_self') return false
|
|
317
|
+
if (a.hasAttribute('download')) return false
|
|
318
|
+
|
|
319
|
+
// Явный запрет
|
|
320
|
+
if ('noPrefetch' in a.dataset || 'prefetchNo' in a.dataset) return false
|
|
321
|
+
|
|
322
|
+
// Белый список
|
|
323
|
+
if (whitelist && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
324
|
+
|
|
325
|
+
// Протокол
|
|
326
|
+
if (a.protocol !== 'http:' && a.protocol !== 'https:') return false
|
|
327
|
+
if (a.protocol === 'http:' && location.protocol === 'https:') return false
|
|
328
|
+
|
|
329
|
+
// Внешние ссылки
|
|
330
|
+
if (a.origin !== location.origin) {
|
|
331
|
+
if (!allowExternal && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
332
|
+
if (!chromiumVer) return false
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Query string
|
|
336
|
+
if (a.search && !allowQuery && !('prefetch' in a.dataset) && !('instant' in a.dataset)) return false
|
|
337
|
+
|
|
338
|
+
// Якорь на той же странице
|
|
339
|
+
if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
|
|
340
|
+
|
|
341
|
+
// v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
|
|
342
|
+
if (urlKey(a.href) === urlKey(location.href)) return false
|
|
343
|
+
|
|
344
|
+
// Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
|
|
345
|
+
var key = urlKey(a.href)
|
|
346
|
+
if (preloaded.has(key)) return false
|
|
347
|
+
|
|
348
|
+
if (!checkPlatform(a)) return false
|
|
349
|
+
if (!checkAnalytics(a)) return false
|
|
350
|
+
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function checkPlatform(a) {
|
|
355
|
+
var href = a.href
|
|
356
|
+
|
|
357
|
+
// v1.0.7: для точных проверок /add /delete /remove используем pathname
|
|
358
|
+
var pathname = ''
|
|
359
|
+
var hash = ''
|
|
360
|
+
try {
|
|
361
|
+
var u = new URL(href, location.href)
|
|
362
|
+
pathname = u.pathname || ''
|
|
363
|
+
hash = u.hash || ''
|
|
364
|
+
} catch (e) {
|
|
365
|
+
pathname = ''
|
|
366
|
+
hash = ''
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (platform === 'bitrix' || platform === 'bitrix24') {
|
|
370
|
+
if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
|
|
371
|
+
if (a.classList.contains('bx-ajax')) return false
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (platform === 'tilda') {
|
|
375
|
+
if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// v1.0.7: /add /delete /remove — только как отдельный сегмент пути (или имя файла типа /delete.php)
|
|
379
|
+
var isActionPath = /(^|\/)(add|delete|remove)(\/|$|\.)/i.test(pathname)
|
|
380
|
+
|
|
381
|
+
if (
|
|
382
|
+
href.indexOf('/login') !== -1 ||
|
|
383
|
+
href.indexOf('/logout') !== -1 ||
|
|
384
|
+
href.indexOf('/auth') !== -1 ||
|
|
385
|
+
href.indexOf('/register') !== -1 ||
|
|
386
|
+
href.indexOf('/cart') !== -1 ||
|
|
387
|
+
href.indexOf('/basket') !== -1 ||
|
|
388
|
+
isActionPath
|
|
389
|
+
) return false
|
|
390
|
+
|
|
391
|
+
if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
|
|
392
|
+
|
|
393
|
+
return true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function checkAnalytics(a) {
|
|
397
|
+
var href = a.href
|
|
398
|
+
var cls = a.className || ''
|
|
399
|
+
|
|
400
|
+
// Извлекаем hostname для проверки доменов аналитики
|
|
401
|
+
var host = ''
|
|
402
|
+
try { host = new URL(href, location.href).hostname } catch (e) { host = '' }
|
|
403
|
+
|
|
404
|
+
if (cls.indexOf('ym-') !== -1) return false
|
|
405
|
+
if (host === 'mc.yandex.ru' || host === 'metrika.yandex.ru') return false
|
|
406
|
+
|
|
407
|
+
if (cls.indexOf('ga-') !== -1 || cls.indexOf('gtm-') !== -1) return false
|
|
408
|
+
if (host === 'google-analytics.com' || host.endsWith('.google-analytics.com')) return false
|
|
409
|
+
if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
|
|
410
|
+
|
|
411
|
+
if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
|
|
412
|
+
if (
|
|
413
|
+
host === 'matomo.org' ||
|
|
414
|
+
host.endsWith('.matomo.org') ||
|
|
415
|
+
host === 'piwik.org' ||
|
|
416
|
+
host.endsWith('.piwik.org')
|
|
417
|
+
) return false
|
|
418
|
+
|
|
419
|
+
return true
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Ключ для дедупликации: НЕ трогаем pathname (включая / на конце), только убираем hash
|
|
423
|
+
function urlKey(url) {
|
|
424
|
+
try {
|
|
425
|
+
var u = new URL(url, location.href)
|
|
426
|
+
return u.origin + u.pathname + u.search
|
|
427
|
+
} catch (e) {
|
|
428
|
+
return url
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// URL для реального запроса: абсолютный, без hash, без "улучшений"
|
|
433
|
+
function urlForRequest(url) {
|
|
434
|
+
try {
|
|
435
|
+
var u = new URL(url, location.href)
|
|
436
|
+
u.hash = ''
|
|
437
|
+
return u.href
|
|
438
|
+
} catch (e) {
|
|
439
|
+
return url
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function resolveSpecMode(a) {
|
|
444
|
+
if (!useSpecRules || specMode === 'none') return 'none'
|
|
445
|
+
if (specMode !== 'prerender') return specMode
|
|
446
|
+
|
|
447
|
+
// specMode === 'prerender'
|
|
448
|
+
if (prerenderAll) return 'prerender'
|
|
449
|
+
if (whitelist) return 'prerender' // ссылки и так явно размечены
|
|
450
|
+
|
|
451
|
+
// иначе prerender только по ссылке:
|
|
452
|
+
if (a && a.dataset && (('prefetchPrerender' in a.dataset) || ('instantPrerender' in a.dataset))) {
|
|
453
|
+
return 'prerender'
|
|
454
|
+
}
|
|
455
|
+
return 'prefetch'
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function preload(url, a) {
|
|
459
|
+
if (!isNetworkOk()) return
|
|
460
|
+
|
|
461
|
+
var requestUrl = urlForRequest(url)
|
|
462
|
+
var key = urlKey(requestUrl)
|
|
463
|
+
|
|
464
|
+
if (preloaded.has(key)) return
|
|
465
|
+
if (preloaded.size >= maxPreloads) {
|
|
466
|
+
preloaded.delete(preloaded.values().next().value)
|
|
467
|
+
}
|
|
468
|
+
preloaded.add(key)
|
|
469
|
+
|
|
470
|
+
var mode = resolveSpecMode(a)
|
|
471
|
+
|
|
472
|
+
if (mode !== 'none') {
|
|
473
|
+
preloadSpec(requestUrl, mode)
|
|
474
|
+
|
|
475
|
+
// Страховка: обычный prefetch (чтобы под CSP/ограничениями SpecRules всё равно грелся кэш)
|
|
476
|
+
if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
|
|
477
|
+
else preloadLink(requestUrl, key)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (isIOS || !supportsLinkPrefetch) preloadFetch(requestUrl, key)
|
|
482
|
+
else preloadLink(requestUrl, key)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function preloadSpec(url, mode) {
|
|
486
|
+
var head = document.head
|
|
487
|
+
if (!head) return
|
|
488
|
+
|
|
489
|
+
var s = document.createElement('script')
|
|
490
|
+
s.type = 'speculationrules'
|
|
491
|
+
if (scriptNonce) s.nonce = scriptNonce
|
|
492
|
+
|
|
493
|
+
var rules = {}
|
|
494
|
+
rules[mode] = [{ source: 'list', urls: [url] }]
|
|
495
|
+
s.textContent = JSON.stringify(rules)
|
|
496
|
+
head.appendChild(s)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function preloadLink(url, key) {
|
|
500
|
+
var head = document.head
|
|
501
|
+
if (!head) return
|
|
502
|
+
|
|
503
|
+
var l = document.createElement('link')
|
|
504
|
+
l.rel = 'prefetch'
|
|
505
|
+
l.href = url
|
|
506
|
+
l.as = 'document'
|
|
507
|
+
try { l.fetchPriority = 'low' } catch (e) {}
|
|
508
|
+
|
|
509
|
+
l.onerror = function () { preloaded.delete(key) }
|
|
510
|
+
head.appendChild(l)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function preloadFetch(url, key) {
|
|
514
|
+
if (typeof fetch !== 'function') return
|
|
515
|
+
|
|
516
|
+
var ctrl = null
|
|
517
|
+
var tid = 0
|
|
518
|
+
|
|
519
|
+
if (typeof AbortController !== 'undefined') {
|
|
520
|
+
ctrl = new AbortController()
|
|
521
|
+
tid = setTimeout(function () {
|
|
522
|
+
try { ctrl.abort() } catch (e) {}
|
|
523
|
+
}, 5000)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
var opts = {
|
|
527
|
+
method: 'GET',
|
|
528
|
+
credentials: 'same-origin',
|
|
529
|
+
cache: 'force-cache',
|
|
530
|
+
headers: { Purpose: 'prefetch' }
|
|
531
|
+
}
|
|
532
|
+
if (ctrl) opts.signal = ctrl.signal
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
fetch(url, opts)
|
|
536
|
+
.then(function (r) {
|
|
537
|
+
if (tid) clearTimeout(tid)
|
|
538
|
+
if (!r || !r.ok) preloaded.delete(key)
|
|
539
|
+
})
|
|
540
|
+
.catch(function () {
|
|
541
|
+
if (tid) clearTimeout(tid)
|
|
542
|
+
preloaded.delete(key)
|
|
543
|
+
})
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (tid) clearTimeout(tid)
|
|
546
|
+
preloaded.delete(key)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Viewport Observer
|
|
551
|
+
var vpObserver = null
|
|
552
|
+
|
|
553
|
+
function startViewportObserver() {
|
|
554
|
+
if (vpObserver) return
|
|
555
|
+
vpObserver = new IntersectionObserver(
|
|
556
|
+
function (entries) {
|
|
557
|
+
entries.forEach(function (entry) {
|
|
558
|
+
if (entry.isIntersecting) {
|
|
559
|
+
vpObserver.unobserve(entry.target)
|
|
560
|
+
if (canPreload(entry.target)) preload(entry.target.href, entry.target)
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
},
|
|
564
|
+
{ rootMargin: isMobile ? '100px' : '200px' }
|
|
565
|
+
)
|
|
566
|
+
observeLinks()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function observeLinks() {
|
|
570
|
+
if (!vpObserver) return
|
|
571
|
+
document.querySelectorAll('a').forEach(function (a) {
|
|
572
|
+
if (canPreload(a)) vpObserver.observe(a)
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Mutation Observer
|
|
577
|
+
var mutObserver = null
|
|
578
|
+
var mutTimer = null
|
|
579
|
+
|
|
580
|
+
function startMutationObserver() {
|
|
581
|
+
if (mutObserver) return
|
|
582
|
+
mutObserver = new MutationObserver(function (muts) {
|
|
583
|
+
var hasLinks = muts.some(function (m) {
|
|
584
|
+
return Array.from(m.addedNodes).some(function (n) {
|
|
585
|
+
return (
|
|
586
|
+
n.nodeType === 1 &&
|
|
587
|
+
(n.tagName === 'A' || (n.querySelectorAll && n.querySelectorAll('a').length))
|
|
588
|
+
)
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
if (hasLinks && vpObserver) {
|
|
592
|
+
clearTimeout(mutTimer)
|
|
593
|
+
mutTimer = setTimeout(observeLinks, 100)
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
mutObserver.observe(document.body, { childList: true, subtree: true })
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Минимальный публичный API
|
|
600
|
+
var api = {
|
|
601
|
+
version: '1.0.8',
|
|
602
|
+
preload: function (url) { preload(url) }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
window.Prefetch = api
|
|
606
|
+
return api
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Guard от двойной инициализации (если уже есть window.Prefetch — используем его)
|
|
610
|
+
var Prefetch =
|
|
611
|
+
(typeof window !== 'undefined' && window.Prefetch) ? window.Prefetch : createPrefetch(import.meta.url)
|
|
612
|
+
|
|
613
|
+
export { Prefetch }
|
|
614
|
+
export default Prefetch
|
package/prefetch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* prefetch.ru v1.0.
|
|
2
|
+
* prefetch.ru v1.0.8 - Мгновенная загрузка страниц
|
|
3
3
|
* © 2026 Сергей Макаров | MIT License
|
|
4
4
|
* https://prefetch.ru | https://github.com/prefetch-ru
|
|
5
5
|
*/
|
|
@@ -108,7 +108,12 @@
|
|
|
108
108
|
// <body data-prefetch-specrules="prerender">
|
|
109
109
|
// <body data-prefetch-specrules="no">
|
|
110
110
|
var hasSr = 'prefetchSpecrules' in ds || 'instantSpecrules' in ds
|
|
111
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
!isIOS &&
|
|
113
|
+
hasSr &&
|
|
114
|
+
HTMLScriptElement.supports &&
|
|
115
|
+
HTMLScriptElement.supports('speculationrules')
|
|
116
|
+
) {
|
|
112
117
|
var sr = ds.prefetchSpecrules || ds.instantSpecrules
|
|
113
118
|
if (sr === 'prerender') {
|
|
114
119
|
specMode = 'prerender'
|
|
@@ -238,7 +243,11 @@
|
|
|
238
243
|
var a = getAnchorFromEventTarget(e.target)
|
|
239
244
|
if (!canPreload(a)) return
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
// v1.0.7: защита от множественных mouseover по вложенным элементам (не плодим таймеры)
|
|
247
|
+
if (hoverTimers.has(a)) return
|
|
248
|
+
|
|
249
|
+
// mouseleave не срабатывает при перемещении внутри ссылки (в отличие от mouseout)
|
|
250
|
+
a.addEventListener('mouseleave', onMouseLeave, { passive: true, once: true })
|
|
242
251
|
|
|
243
252
|
var t = setTimeout(function () {
|
|
244
253
|
preload(a.href, a)
|
|
@@ -247,10 +256,9 @@
|
|
|
247
256
|
hoverTimers.set(a, t)
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
function
|
|
251
|
-
var a =
|
|
259
|
+
function onMouseLeave(e) {
|
|
260
|
+
var a = e.currentTarget
|
|
252
261
|
if (!a) return
|
|
253
|
-
if (e.relatedTarget && e.relatedTarget.closest && a === e.relatedTarget.closest('a')) return
|
|
254
262
|
|
|
255
263
|
var t = hoverTimers.get(a)
|
|
256
264
|
if (t) {
|
|
@@ -268,7 +276,13 @@
|
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
function canPreload(a) {
|
|
271
|
-
if (!a
|
|
279
|
+
if (!a) return false
|
|
280
|
+
|
|
281
|
+
// v1.0.7: исключаем <a href=""> и <a> без href (часто используются как кнопки)
|
|
282
|
+
var hrefAttr = a.getAttribute('href')
|
|
283
|
+
if (hrefAttr === null || hrefAttr.trim() === '') return false
|
|
284
|
+
|
|
285
|
+
if (!a.href) return false
|
|
272
286
|
|
|
273
287
|
// Не навигация в текущей вкладке
|
|
274
288
|
if (a.target && a.target !== '_self') return false
|
|
@@ -296,6 +310,9 @@
|
|
|
296
310
|
// Якорь на той же странице
|
|
297
311
|
if (a.hash && a.pathname + a.search === location.pathname + location.search) return false
|
|
298
312
|
|
|
313
|
+
// v1.0.7: не префетчим текущую страницу (в т.ч. для случаев вроде href="")
|
|
314
|
+
if (urlKey(a.href) === urlKey(location.href)) return false
|
|
315
|
+
|
|
299
316
|
// Уже загружено (ключ НЕ модифицирует реальный URL запроса!)
|
|
300
317
|
var key = urlKey(a.href)
|
|
301
318
|
if (preloaded.has(key)) return false
|
|
@@ -309,15 +326,31 @@
|
|
|
309
326
|
function checkPlatform(a) {
|
|
310
327
|
var href = a.href
|
|
311
328
|
|
|
329
|
+
// v1.0.7: для точных проверок /add /delete /remove используем pathname
|
|
330
|
+
var pathname = ''
|
|
331
|
+
var hash = ''
|
|
332
|
+
try {
|
|
333
|
+
var u = new URL(href, location.href)
|
|
334
|
+
pathname = u.pathname || ''
|
|
335
|
+
hash = u.hash || ''
|
|
336
|
+
} catch (e) {
|
|
337
|
+
pathname = ''
|
|
338
|
+
hash = ''
|
|
339
|
+
}
|
|
340
|
+
|
|
312
341
|
if (platform === 'bitrix' || platform === 'bitrix24') {
|
|
313
342
|
if (href.indexOf('/bitrix/') !== -1 || href.indexOf('sessid=') !== -1) return false
|
|
314
343
|
if (a.classList.contains('bx-ajax')) return false
|
|
315
344
|
}
|
|
316
345
|
|
|
317
346
|
if (platform === 'tilda') {
|
|
318
|
-
|
|
347
|
+
// Можно проверять и по href, но hash надёжнее/дешевле
|
|
348
|
+
if (hash.indexOf('#popup:') !== -1 || hash.indexOf('#rec') !== -1) return false
|
|
319
349
|
}
|
|
320
350
|
|
|
351
|
+
// v1.0.7: /add /delete /remove — только как отдельный сегмент пути (или имя файла типа /delete.php)
|
|
352
|
+
var isActionPath = /(^|\/)(add|delete|remove)(\/|$|\.)/i.test(pathname)
|
|
353
|
+
|
|
321
354
|
if (
|
|
322
355
|
href.indexOf('/login') !== -1 ||
|
|
323
356
|
href.indexOf('/logout') !== -1 ||
|
|
@@ -325,12 +358,10 @@
|
|
|
325
358
|
href.indexOf('/register') !== -1 ||
|
|
326
359
|
href.indexOf('/cart') !== -1 ||
|
|
327
360
|
href.indexOf('/basket') !== -1 ||
|
|
328
|
-
|
|
329
|
-
href.indexOf('/delete') !== -1 ||
|
|
330
|
-
href.indexOf('/remove') !== -1
|
|
361
|
+
isActionPath
|
|
331
362
|
) return false
|
|
332
363
|
|
|
333
|
-
if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)
|
|
364
|
+
if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|exe)($|\?)/i.test(href)) return false
|
|
334
365
|
|
|
335
366
|
return true
|
|
336
367
|
}
|
|
@@ -351,7 +382,12 @@
|
|
|
351
382
|
if (host === 'googletagmanager.com' || host.endsWith('.googletagmanager.com')) return false
|
|
352
383
|
|
|
353
384
|
if (cls.indexOf('piwik') !== -1 || cls.indexOf('matomo') !== -1) return false
|
|
354
|
-
if (
|
|
385
|
+
if (
|
|
386
|
+
host === 'matomo.org' ||
|
|
387
|
+
host.endsWith('.matomo.org') ||
|
|
388
|
+
host === 'piwik.org' ||
|
|
389
|
+
host.endsWith('.piwik.org')
|
|
390
|
+
) return false
|
|
355
391
|
|
|
356
392
|
return true
|
|
357
393
|
}
|
|
@@ -535,7 +571,7 @@
|
|
|
535
571
|
|
|
536
572
|
// Минимальный публичный API
|
|
537
573
|
window.Prefetch = {
|
|
538
|
-
version: '1.0.
|
|
574
|
+
version: '1.0.8',
|
|
539
575
|
preload: function (url) { preload(url) }
|
|
540
576
|
}
|
|
541
577
|
})()
|