Предзагрузка данных

В Page.js управление маршрутами строится вокруг функций-обработчиков, привязанных к определённым URL. Для современных SPA (Single Page Application) часто возникает необходимость загружать данные до того, как будет выполнен переход на новый маршрут. Это обеспечивает плавный пользовательский опыт и предотвращает отображение пустых состояний.

Предзагрузка данных — это процесс асинхронного получения информации до вызова рендеринга страницы. В Page.js она реализуется через middleware-функции и возможности маршрутов.


Использование middleware для асинхронной загрузки

Page.js поддерживает цепочку middleware, где каждый обработчик принимает аргументы (ctx, next):

page('/user/:id', loadUserData, renderUserPage);

function loadUserData(ctx, next) {
    const userId = ctx.params.id;
    fetch(`/api/users/${userId}`)
        .then(res => res.json())
        .then(data => {
            ctx.user = data;
            next();
        })
        .catch(err => {
            console.error(err);
            ctx.user = null;
            next();
        });
}

function renderUserPage(ctx) {
    if (!ctx.user) {
        document.body.innerHTML = '<p>Ошибка загрузки данных</p>';
        return;
    }
    document.body.innerHTML = `<h1>${ctx.user.name}</h1><p>${ctx.user.email}</p>`;
}

Ключевые моменты:

  • ctx хранит контекст маршрута, включая параметры и любые данные, которые были предзагружены.
  • Middleware вызывается до финальной функции рендера.
  • Асинхронная загрузка выполняется через fetch или любые другие промисы.

Предзагрузка нескольких источников данных

В сложных приложениях может потребоваться загрузка сразу нескольких ресурсов перед рендерингом. Для этого используют Promise.all:

function loadDashboardData(ctx, next) {
    const userPromise = fetch('/api/currentUser').then(r => r.json());
    const statsPromise = fetch('/api/stats').then(r => r.json());

    Promise.all([userPromise, statsPromise])
        .then(([user, stats]) => {
            ctx.user = user;
            ctx.stats = stats;
            next();
        })
        .catch(err => {
            console.error(err);
            ctx.user = null;
            ctx.stats = null;
            next();
        });
}

page('/dashboard', loadDashboardData, renderDashboard);

function renderDashboard(ctx) {
    if (!ctx.user || !ctx.stats) {
        document.body.innerHTML = '<p>Не удалось загрузить данные</p>';
        return;
    }
    document.body.innerHTML = `
        <h1>Привет, ${ctx.user.name}</h1>
        <p>Статистика: ${ctx.stats.views} просмотров</p>
    `;
}

Особенности такого подхода:

  • Упрощает синхронизацию нескольких асинхронных операций.
  • Позволяет хранить данные в контексте маршрута и использовать их в последующих middleware или рендере.
  • Обеспечивает обработку ошибок централизованно перед рендерингом.

Асинхронные функции в middleware

С использованием async/await код становится более читаемым:

async function loadProduct(ctx, next) {
    try {
        const res = await fetch(`/api/products/${ctx.params.id}`);
        ctx.product = await res.json();
    } catch (err) {
        console.error(err);
        ctx.product = null;
    }
    next();
}

page('/product/:id', loadProduct, renderProduct);

function renderProduct(ctx) {
    if (!ctx.product) {
        document.body.innerHTML = '<p>Товар не найден</p>';
        return;
    }
    document.body.innerHTML = `
        <h2>${ctx.product.name}</h2>
        <p>Цена: ${ctx.product.price}</p>
    `;
}

Преимущества async/await:

  • Логика обработки ошибок становится проще через try/catch.
  • Код читается как синхронный, что облегчает поддержку.

Динамическая предзагрузка в зависимости от состояния

Иногда необходимо предзагружать данные только при определённых условиях, например, при изменении параметра маршрута:

function loadComments(ctx, next) {
    if (ctx.query.loadComments === 'true') {
        fetch(`/api/posts/${ctx.params.id}/comments`)
            .then(res => res.json())
            .then(comments => {
                ctx.comments = comments;
                next();
            });
    } else {
        ctx.comments = [];
        next();
    }
}

page('/post/:id', loadComments, renderPost);

function renderPost(ctx) {
    const commentsHtml = ctx.comments.map(c => `<li>${c.text}</li>`).join('');
    document.body.innerHTML = `
        <h1>Пост ${ctx.params.id}</h1>
        <ul>${commentsHtml}</ul>
    `;
}

Выводы из практики:

  • ctx.query позволяет работать с GET-параметрами для динамической загрузки.
  • Предзагрузка не обязательно должна выполняться для всех маршрутов одинаково.

Раннее кэширование данных

Для ускорения навигации можно сохранять результаты предзагрузки в локальном кэше:

const cache = {};

async function loadProfile(ctx, next) {
    const userId = ctx.params.id;
    if (cache[userId]) {
        ctx.user = cache[userId];
    } else {
        const res = await fetch(`/api/users/${userId}`);
        ctx.user = await res.json();
        cache[userId] = ctx.user;
    }
    next();
}

page('/profile/:id', loadProfile, renderProfile);

Преимущества кэширования:

  • Снижает количество сетевых запросов.
  • Ускоряет повторные переходы на уже посещённые страницы.
  • Позволяет реализовать мгновенный рендер при повторном посещении маршрута.

Интеграция с глобальным состоянием

Предзагрузка данных часто объединяется с глобальными хранилищами состояния, например, Redux или простыми объектами:

const store = {
    user: null,
    posts: []
};

async function preloadData(ctx, next) {
    if (!store.user) {
        store.user = await fetch('/api/currentUser').then(r => r.json());
    }
    if (!store.posts.length) {
        store.posts = await fetch('/api/posts').then(r => r.json());
    }
    ctx.store = store;
    next();
}

page('/home', preloadData, renderHome);

function renderHome(ctx) {
    const { user, posts } = ctx.store;
    document.body.innerHTML = `
        <h1>Добро пожаловать, ${user.name}</h1>
        <ul>${posts.map(p => `<li>${p.title}</li>`).join('')}</ul>
    `;
}

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


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