Список пользователей

1
Админ
Постов: 211
2
VIP
Постов: 72
3
Элита
Постов: 50
4
Проверенные
Постов: 35
5
VIP
Постов: 35
6
Проверенные
Постов: 32
7
Пользователи
Постов: 31
8
Проверенные
Постов: 29

  • Страница 1 из 1
  • 1
Stylus Engine UCOZ: закреплённый парсер горячих тем планшета
Дата: Среда, 18.02.2026, 18:41 | Сообщение # 1 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:211
         Регистрация:20.10.2016

Переосмысление архитектуры

Логика важнее самого кода

В последнее время я активно занимаюсь разработкой скриптов и за этот период многое переосмыслил. Набрался опыта, глубже разобрался в деталях и пришёл к главному выводу: ценность заключается не столько в самом коде, сколько в логике, которая в него заложена. Чем лучше продумана структура и организация скрипта, тем стабильнее, быстрее и эффективнее он выполняет свои задачи.

Одна из самых распространённых ошибок в моих ранних решениях была типичной в самом начале я закладывал множественные проверки, и только после этого происходил вывод данных. На первый взгляд это выглядело логично: сначала убедиться, что всё работает корректно, а уже затем показывать результат. Однако именно такой подход и оказался ключевой проблемой.

Когда скрипт при каждом запуске начинает проверять объём данных, изменения в темах или появление новых записей, на это неизбежно тратится время. Эти задержки приводили к тому, что интерфейс не успевал своевременно отображать информацию: блок на главной странице открывался с опозданием, мог мерцать, дёргаться или подвисать. Подобные мелкие, но постоянные сбои разрушали саму идею плавного и визуально аккуратного отображения контента.



В результате я полностью пересмотрел архитектуру и логику работы скрипта. В основу был положен принцип минимальных действий и максимальной скорости. Теперь скрипт использует данные блока «горячих тем» как исходную точку и сразу выводит первую тему из списка, не выполняя лишних проверок всех элементов. Полный перебор списка не имеет практического смысла он увеличивает нагрузку, замедляет работу и снижает эффективность.

После загрузки первой темы её данные сохраняются в локальном хранилище (LocalStorage). Далее работает механизм сравнения: при каждой загрузке страницы скрипт в фоновом режиме проверяет, изменился ли заголовок первой темы. Если изменений нет, скрипт фактически «засыпает» и не расходует ресурсы.

Если же первая тема обновляется, скрипт мгновенно очищает старый кэш и подгружает новые данные. Всё происходит быстро и незаметно для пользователя: интерфейс остаётся стабильным, ничего не дёргается и не мерцает — просто в определённый момент новость плавно сменяется новой.

Архитектура и принцип работы NF_CAROUSEL_V5

Скрипт NF_CAROUSEL_V5 представляет собой асинхронный клиентский модуль, предназначенный для отображения карусели «горячих тем» форума с минимальной нагрузкой на интерфейс и сеть. В основе лежит стратегия cache-first + lazy-update, обеспечивающая быстрый рендер, плавность интерфейса и отсутствие лишних перерисовок.

Защита от повторного запуска

В начале скрипт устанавливает глобальную блокировку:

window.__NF_LOCK__ предотвращает повторную инициализацию.

Гарантируется один экземпляр скрипта на странице.

Это исключает дублирование рендера, сетевых запросов и обработчиков событий.

Конфигурация и параметры

Скрипт использует фиксированные параметры:

Источник данных — страница форума (BASE_FORUM_URL)

Кэш карточек — CACHE_KEY

Кэш первой темы — TOPIC_KEY

Максимум карточек — MAX_CARDS

Геометрия карусели — CARD_WIDTH, VISIBLE

Конфигурация отделена от логики, что упрощает масштабирование и изменение поведения.

Cache-First стратегия отображения

При запуске скрипт сначала пытается отобразить сохранённые данные:

Чтение из LocalStorage

Мгновенный рендер без сетевых запросов

Интерфейс появляется сразу, без задержек

Это устраняет мерцание, пустые состояния и блокировку UI.

Поиск горячих тем

Скрипт загружает HTML страницы форума и извлекает темы:

Анализируются строки таблицы (tr)

Фильтрация по признаку «горячая тема» или наличию пагинации

Извлекаются:

ссылка

заголовок

иконка

автор

просмотры

Ограничение — MAX_CARDS

На этом этапе формируется базовый список тем без глубокого анализа.

Обогащение данных темы (ENRICH)

Для каждой найденной темы выполняется асинхронное расширение данных:

Загружается страница темы

Извлекается:

текст первого поста (очищенный от HTML)

первая найденная картинка

аватар автора

имя автора

При ошибке используются fallback-данные

Таким образом каждая карточка содержит полный набор визуальных и текстовых данных.

Механизм умного обновления (Lazy Update)

Ключевая оптимизация скрипта — проверка только первой горячей темы.

Алгоритм:

Загружается страница форума

Извлекается первая горячая тема

Сравнивается её заголовок с сохранённым в LocalStorage

Если заголовок совпадает — обновление не выполняется

Если отличается:

очищается кэш

заново загружаются карточки

выполняется рендер

Это устраняет ненужные загрузки и снижает сетевую активность.

Рендер карусели

Карусель строится динамически:

Создаются DOM-карточки

Вставляются:

изображение

заголовок

текст

автор + аватар

просмотры

Используется flex-контейнер и translateX для смещения

Реализована навигация стрелками

Рендер выполняется только при необходимости.

Watchdog-механизм восстановления

Фоновый таймер каждые 8 секунд проверяет:

Если карусель пуста — восстанавливает её из кэша

Это защита от:

случайной очистки DOM

конфликтов скриптов

динамических перерисовок страницы

Устойчивость и отказоустойчивость

Скрипт учитывает:

offline-режим

ошибки загрузки

повреждённый кэш

отсутствие DOM-элементов

сетевые сбои

Во всех случаях используется graceful fallback.

Архитектурные особенности

Скрипт реализует:

Cache-First Rendering

Lazy Update Strategy

Minimal DOM Repaint

Fault-Tolerant Fetch

Progressive Data Enrichment

Single Instance Lock

Soft Background Validation

Это обеспечивает:

быстрый первый рендер

отсутствие лагов интерфейса

минимальную сетевую нагрузку

стабильную работу

NF_CAROUSEL_V5 — это оптимизированный клиентский модуль отображения динамического контента, построенный на принципах минимальных действий,кэширования и выборочного обновления. Скрипт обеспечивает плавный пользовательский опыт, быструю загрузку и устойчивость к ошибкам, при этом избегая лишних вычислений и повторных перерисовок интерфейса.



Исходной код

Код
<!-- <body> -->
<section class="sect flex-grow-1">
<div class="sect__header d-flex">
<h2 class="sect__title flex-grow-1">Горячая тема

<img src="https://jordan.moy.su/forumimages/3droom.png"
alt="3D Room Icon"
style="height:24px; margin-left:-2px; vertical-align:middle;">

</h2>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap&subset=cyrillic" rel="stylesheet">

<style>
/* ===================== Неоновая обёртка ===================== */
.nf-dark-border {
position: relative;
border: 1px solid #9D4EDD;
padding: 30px;
background-color: #0a0710;
color: #fff;
width: 850px;
margin: 40px auto;
border-radius: 12px;
box-shadow:
0 0 25px rgba(157, 78, 221, 0.7),
0 0 50px rgba(157, 78, 221, 0.5),
inset 0 0 20px rgba(157, 78, 221, 0.25);
animation: nf-neonGlow 3s ease-in-out infinite alternate;
overflow: visible;
}

@keyframes nf-neonGlow {
from {
box-shadow:
0 0 15px rgba(157, 78, 221, 0.5),
0 0 30px rgba(157, 78, 221, 0.3),
inset 0 0 12px rgba(157, 78, 221, 0.15);
}
to {
box-shadow:
0 0 35px rgba(157, 78, 221, 0.9),
0 0 70px rgba(157, 78, 221, 0.7),
inset 0 0 25px rgba(157, 78, 221, 0.3);
}
}

/* ===================== Карусель ===================== */
.nf-carousel-container {
position: relative;
width: 100%;
overflow: hidden;
}

.nf-carousel-track {
display: flex;
transition: transform 0.5s ease;
}

/* ===================== Карточка ===================== */
.nf-card {
flex: 0 0 100%;
background: linear-gradient(145deg, #1c1122, #2a1735);
border-radius: 14px;
overflow: hidden;
display: flex;
text-decoration: none;
color: #fff;
cursor: pointer;
position: relative;
transition: box-shadow 0.3s ease, filter 0.3s ease;
min-height: 250px;
aspect-ratio: 16 / 9;
}

.nf-card:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
filter: brightness(1.45);
}

/* ===================== Изображение ===================== */
.nf-card-img {
width: 55%;
height: 100%;
overflow: hidden;
}

.nf-card-img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}

.nf-card:hover .nf-card-img img {
transform: scale(1.03);
}

/* ===================== Контент карточки ===================== */
.nf-card-content {
padding: 25px 30px;
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 45%;
font-family: 'Inter', 'Roboto', 'Segoe UI', sans-serif;
}

.nf-card-title {
font-weight: 700;
font-size: 24px;
line-height: 1.3;
margin-bottom: 16px;
color: #ffdcff;
text-shadow: 0 0 12px rgba(157, 78, 221, 0.7);
word-break: break-word;
}

.nf-card-desc {
display: block;
text-align: left;
line-height: 1.6;
font-size: 17px;
font-weight: 400;
color: #e0d8ff;
letter-spacing: 0.3px;
max-height: calc(12 * 1.6em);
overflow: hidden;
word-break: break-word;
hyphens: auto;
padding-right: 4px;
font-family: 'Inter', 'Roboto', 'Segoe UI', sans-serif; /* Модный шрифт */
}

/* ===================== Автор ===================== */
.nf-card-meta {
position: absolute;
bottom: 15px;
right: 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
z-index: 5;
font-family: 'Inter', 'Roboto', 'Segoe UI', sans-serif;
font-weight: 500;
font-size: 14px;
color: #d7a7ff;
}

.nf-card-meta img {
width: 55px;
height: 55px;
border-radius: 50%;
border: 2px solid #b367ff;
object-fit: cover;
background: #1a0e25;
box-shadow: 0 0 10px rgba(157, 78, 221, 0.6);
}

/* ===================== Бейдж ===================== */
.nf-card-badge {
position: absolute;
top: 10px;
left: 10px;
background: linear-gradient(135deg, #b367ff, #7a3dbd);
padding: 5px 9px;
border-radius: 8px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.6px;
box-shadow: 0 0 12px rgba(157, 78, 221, 0.6);
}

/* ===================== Стрелки ===================== */
.nf-carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 52px;
height: 52px;
background: radial-gradient(circle at center, rgba(157, 78, 221, 0.85), rgba(80, 30, 140, 0.8));
border-radius: 50%;
color: #fff;
font-size: 30px;
text-align: center;
line-height: 52px;
cursor: pointer;
user-select: none;
z-index: 20;
box-shadow: 0 0 20px rgba(157, 78, 221, 0.8), 0 0 35px rgba(157, 78, 221, 0.6);
transition: all 0.3s ease;
}

.nf-carousel-arrow:hover {
background: radial-gradient(circle at center, rgba(180, 100, 255, 0.9), rgba(100, 40, 160, 0.85));
transform: translateY(-50%) scale(1.1);
}

.nf-carousel-arrow:active {
background: rgba(157, 78, 221, 0.7);
}

.nf-carousel-arrow-left { left: -25px; }
.nf-carousel-arrow-right { right: -25px; }

/* ===================== Нижняя панель ===================== */
.nf-tv-bottom {
position: relative;
width: 100%;
height: 40px;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Arial', sans-serif;
font-weight: bold;
font-size: 18px;
color: #fff;
letter-spacing: 2px;
text-shadow: 0 0 6px rgba(255,255,255,0.5);
border-radius: 0 0 12px 12px;
}

/* ===================== Реальный Apple-style стилус ===================== */
.tablet-stylus {
position: absolute;
left: -48px;
top: 50%;
transform: translateY(-60%) rotate(-1deg);
width: 16px;
height: 520px;

/* Матовый корпус как у Apple Pencil */
background: linear-gradient(180deg, #fefefe 0%, #e9e9e9 100%);
border-radius: 12px;

box-shadow:
0 4px 12px rgba(0,0,0,0.45),
inset 0 0 6px rgba(255,255,255,0.25);

z-index: 20;
transition: transform .2s ease, box-shadow .2s ease;
}

/* Корпус с плоскими гранями как в твоём исходнике */
.tablet-stylus {
clip-path: polygon(
50% 0%,
95% 8%,
95% 92%,
50% 100%,
5% 92%,
5% 8%
);
}

/* Наконечник */
.tablet-stylus::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 22px;
background: radial-gradient(circle at 30% 30%, #ffffff 0%, #cfcfcf 55%, #8a8a8a 100%);
border-radius: 50%;

box-shadow:
0 1px 5px rgba(0,0,0,0.4),
inset 0 1px 3px rgba(255,255,255,0.6);
}

/* Верхняя заглушка */
.tablet-stylus::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-100%);
width: 10px;
height: 10px;
background: #dcdcdc;
border-radius: 50%;

box-shadow:
0 1px 4px rgba(0,0,0,0.35),
inset 0 1px 2px rgba(255,255,255,0.5);
}

/* Надпись как в Apple Pencil */
.tablet-stylus .stylus-label {
position: absolute;
inset: 0;
writing-mode: vertical-rl;
transform: rotate(180deg);

font-family: "Segoe UI", sans-serif;
font-size: 11px;
font-weight: 600;
color: #6a6a6a;

display: flex;
align-items: center;
justify-content: center;

text-shadow: 0 0 1px rgba(0,0,0,0.25);
pointer-events: none;
}

/* Лёгкое движение как у твоего */
.tablet-stylus:hover {
transform: translateY(-10%) rotate(-4deg);

box-shadow:
0 6px 16px rgba(0,0,0,0.55),
inset 0 0 8px rgba(255,255,255,0.35);
}

/* ===================== Адаптация под планшет ===================== */
@media (max-width: 1024px) {
.nf-card { flex-direction: column; min-height: 300px; aspect-ratio: auto; }
.nf-card-img { width: 100%; height: 180px; }
.nf-card-content { width: 100%; padding: 18px 20px; }
.nf-card-title { font-size: 20px; }
.nf-card-desc { font-size: 15px; line-height: 1.55; max-height: calc(10 * 1.55em); }
.nf-card-meta { flex-direction: row; bottom: 12px; right: 12px; }
}
</style>

<div class="nf-dark-border">
<div class="nf-carousel-container">

<div class="nf-carousel-track" id="nf-forum-cards"></div>

<!-- Нижняя панель телевизора -->
<div class="nf-tv-bottom">
<span>SAMSUNG</span>
</div>

</div>

<div class="nf-carousel-arrow nf-carousel-arrow-left">❮</div>
<div class="nf-carousel-arrow nf-carousel-arrow-right">❯</div>

<!-- Белый стилус сбоку планшета -->

<div class="tablet-stylus">
<div class="stylus-label">STAINLESS STEEL</div>
</div>

</div>

<script>
(async function NF_CAROUSEL_V5(){

if(window.__NF_LOCK__) return;
window.__NF_LOCK__=true;

/* ================= CONFIG ================= */
const BASE_FORUM_URL='/forum/0-0-1-34';
const CACHE_KEY='nf_hot_cache_v5';
const TOPIC_KEY='nf_hot_first_topic';
const MAX_CARDS=12;
const CARD_WIDTH=785;
const VISIBLE=1;

/* ================= DOM ================= */
const track=document.querySelector('.nf-carousel-track');
if(!track) return;
const arrowL=document.querySelector('.nf-carousel-arrow-left');
const arrowR=document.querySelector('.nf-carousel-arrow-right');

/* ================= UTILS ================= */
const toAbs=u=>!u?'':u.startsWith('http')?u:u.startsWith('//')?location.protocol+u:location.origin+(u.startsWith('/')?u:'/'+u);
const esc=s=>String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));

function safeGet(k){try{return JSON.parse(localStorage.getItem(k)||'null')}catch{return null}}
function safeSet(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch{}}
function clearCache(){localStorage.removeItem(CACHE_KEY)}

async function fetchHTML(url){
if(!navigator.onLine) throw 'offline';
const r=await fetch(url,{credentials:'same-origin',cache:'no-cache'});
if(!r.ok) throw 'fetch fail';
return await r.text();
}

/* ================= AVATAR FROM postTdInfo ================= */
function extractAvatarFromPost(doc){
let img=doc.querySelector('.postTdInfo img.ipsUserPhoto');
if(!img) return '';
return toAbs(img.getAttribute('src')||'');
}

/* ================= FIND HOT ================= */
function findHot(doc){
const rows=[...doc.querySelectorAll('tr')];
const out=[];
for(const r of rows){
const link=r.querySelector('a.threadLink');
if(!link) continue;
const badge=r.querySelector('.ipsBadge');
const pages=r.querySelector('.postpSwithces,.postPSwithcesLink');
if(!((badge&&/горяч/i.test(badge.textContent||''))||pages)) continue;

const href=toAbs(link.href);
const title=link.textContent.trim();
const icon=toAbs(r.querySelector('img')?.src||'/forumimages/Newsletter-3.png');
const author=r.querySelector('.threadAuthTd')?.textContent.trim()||'Аноним';
const views=parseInt((r.querySelector('.threadViewTd,.views')?.textContent||'').replace(/\D/g,''))||0;

out.push({href,title,icon,author,views});
if(out.length>=MAX_CARDS) break;
}
return out;
}

/* ================= CLEAN TEXT ================= */
function cleanText(el, max=252){
if(!el) return '';
// Клонируем узел, чтобы не трогать оригинал
const clone = el.cloneNode(true);

// Убираем все теги, оставляем только текст
clone.querySelectorAll('*').forEach(node => {
// если есть текст внутри, оставляем, иначе удаляем
if(!node.textContent.trim()) node.remove();
});

let text = clone.textContent || '';
text = text.replace(/\s+/g, ' ').trim();
return text.slice(0, max);
}

/* ================= ENRICH ================= */
async function enrich(t){
try{
const html=await fetchHTML(t.href);
const doc=new DOMParser().parseFromString(html,'text/html');

const post=doc.querySelector('.post_content,.post_body,.post,.message');
const text=cleanText(post);

let img='';
post?.querySelectorAll('img').forEach(im=>{
if(img) return;
const s = im.src || '';
if(s) img = toAbs(s); // берём любую картинку
});
if(!img) img=t.icon;

let avatar=extractAvatarFromPost(doc);
if(!avatar){
avatar=toAbs(doc.querySelector('.avatar img')?.src||'/forumimages/default-avatar.png');
}

const author=doc.querySelector('.postUser,.author')?.textContent.trim()||t.author;

return {...t,text,img,avatar,author};
}catch{
return {...t,text:'',img:t.icon,avatar:'/forumimages/default-avatar.png'};
}
}

/* ================= RENDER ================= */
let index=0;

function render(cards){
if(!cards?.length) return;
track.innerHTML='';

cards.forEach(c=>{
const a=document.createElement('a');
a.className='nf-card';
a.href=c.href;
a.style.flex=`0 0 ${CARD_WIDTH}px`;

a.innerHTML=`
<div class="nf-card-img">
<img src="${c.img}">
<div class="nf-card-badge">🔥 Горячая тема</div>
</div>
<div class="nf-card-content">
<div class="nf-card-title">${esc(c.title)}</div>
<div class="nf-card-desc">${esc(c.text)}</div>
<div class="nf-card-meta">
<img src="${c.avatar}" style="width:46px;height:46px;border-radius:50%">
${esc(c.author)} • 👁 ${c.views}
</div>
</div>`;
track.appendChild(a);
});

track.style.display='flex';
updatePos();
}

function updatePos(){
const max=Math.max(0,track.children.length-VISIBLE);
index=Math.max(0,Math.min(index,max));
track.style.transform=`translateX(${-index*CARD_WIDTH}px)`;
}

arrowL?.addEventListener('click',()=>{index--;updatePos()});
arrowR?.addEventListener('click',()=>{index++;updatePos()});

/* ================= LOAD CACHE FIRST ================= */
const cached=safeGet(CACHE_KEY);
if(cached?.cards) render(cached.cards);

/* ================= CHECK FIRST HOT TOPIC ================= */
async function checkUpdates(){
if(!navigator.onLine) return;
let html,doc;
try{
html=await fetchHTML(BASE_FORUM_URL);
doc=new DOMParser().parseFromString(html,'text/html');
}catch{return}

const hot=findHot(doc);
if(!hot.length) return;

const firstTitle=hot[0].title.trim();
const savedTitle=localStorage.getItem(TOPIC_KEY);
if(savedTitle && savedTitle===firstTitle) return;

localStorage.setItem(TOPIC_KEY,firstTitle);
clearCache();

const cards=await Promise.all(hot.map(enrich));
safeSet(CACHE_KEY,{cards});
render(cards);
}

checkUpdates();

/* ================= WATCHDOG ================= */
setInterval(()=>{
if(!track.children.length){
const c=safeGet(CACHE_KEY);
if(c?.cards) render(c.cards);
}
},8000);

})();
</script>

</div>

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: