Как сделать редирект с JavaScript?
- понедельник, 5 мая 2025 г. в 00:00:03
Вопрос перенаправлений кажется простым или не особо серьезным для обдумывания, но в будущем может вызвать проблемы при слишком халатном подходе.
Я хочу сравнить самые популярные методы для перенаправления с добавлением записей в историю браузера:
window.location.href
window.location.assign
Базово: оба способа перенаправляют на другую страницу с новой записью в истории.
Так же оба уязвимы XSS-атакам, например оба примера отработают:
const input = 'javascript:alert("хак")';
window.location.href = input;
window.location.assign(input);
Главное — способы имеют разный синтаксис:
// через свойство
window.location.href = '<https://example.com>';
// через метод
window.location.assign('<https://example.com>');
История поддержки
href
исторически поддерживается всеми браузерами с самых ранних версий.
assign
попал в рекомендации W3C осенью 2014 года, можно считать, что с того момента он был везде.
Тестирование
Метод assign()
проще тестировать:
// Jest пример
const mockAssign = jest.fn();
window.location.assign = mockAssign;
// После вызова функции
expect(mockAssign)
.toHaveBeenCalledWith('<https://valid-url.com>');
Для тестирования href
потребуется более сложный мокинг глобальных объектов.
Сразу понятно намерение — выполнить редирект. С href
мы так же можем что-то сравнивать и это может сбивать с толку.
Проще тестировать (пример выше).
Объектно-ориентированный стиль — “выполнить метод объекта”. В целом выглядит чище.
Достаточно сделать дополнительные проверки URL перед вызовом. Напишем для этого функцию:
function safeRedirect(href: string) {
try {
// создаём URL-объект из href
const urlObject = new URL(
href,
// добавим второй аргумент, чтобы относительные URL не ломались
window.location.origin,
);
// перенаправляем
window.location.assign(urlObject);
} catch (error) {
console.error('Некорректный URL:', href, error);
return error;
}
}
Здесь мы проверяем действительно ли это URL (полный или относительный, так же подойдут просто search params или hash) и не даем сделать перенаправление, если что-то пойдет не так.
Мы можем улучшить эту функцию, чтобы в любом месте вызова дополнительно обработать ошибки, а так же иметь возможность не делать запись в истории браузера:
type SafeRedirectOptions = {
/**
* Если true, то перенаправление будет добавлено в историю браузера.
* @default true
*/
history?: boolean;
};
const defaultSafeRedirectOptions: SafeRedirectOptions = { history: true };
/**
* Безопасно перенаправляет браузер на указанный URL.
*
* @param url Строка URL или относительный путь (а так же query, hash).
* @throws {TypeError} Если URL некорректен.
* @example
* safeRedirect('<https://example.com>');
* safeRedirect('/path/to/page');
* safeRedirect('javascript:alert("Я никогда не выполнюсь..")')
* .catch((error) => console.error('Зато я выполнился!', error));
*/
export function safeRedirect(url: string, optionsArg: SafeRedirectOptions = defaultSafeRedirectOptions): Promise<void> {
const options = { ...defaultSafeRedirectOptions, ...optionsArg };
return new Promise((resolve, reject) => {
try {
window.location[options.history ? 'assign' : 'replace'](new URL(url, window.location.origin));
resolve();
} catch (error) {
console.error('[safeRedirect] Некорректный URL:', url, error);
reject(new TypeError(`Invalid URL provided: ${url}`));
}
});
}
При использовании href
или assign
мы не добьемся мгновенного перехода. Т.е. код, который написан ниже сработает, будто href
или assign
сработали асинхронно:
window.location.href = '<https://maxxborer.com>';
alert('Я выполнился 🤔');
window.location.assign('<https://maxxborer.com>');
alert('И я выполнился 🫠');
Но при этом тот же assign
не возвращает Promise и мы не сможем использовать даже await
, чтобы дождаться редиректа и заблокировать дальнейшее выполнение кода.
Почему это вообще важно? Например, пользователь заходит на вашу закрытую страницу, которая требует авторизации. Страница отправляет кучу запросов, которые требуют авторизованную сессию. Сессия у пользователя закончилась и вам, после получения 401 ошибки, нужно перенаправить его на страницу авторизации. Вы сделали редирект, но что толку? Все последующие запросы все равно будут выполнены.
Мы можем сделать хак для таких случаев, не дать промису зарезолвиться:
/**
* Замораживает выполнение кода на указанное количество миллисекунд.
* @param ms Количество миллисекунд.
* @returns Promise<void>
*/
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type SafeRedirectOptions = {
/**
* Если true, то перенаправление будет добавлено в историю браузера.
* @default true
*/
history?: boolean;
/**
* Если true, то дальнейший рендер страницы не будет выполнен (требует использования `await`).
* @default false
*/
freeze?: boolean;
};
const defaultSafeRedirectOptions: SafeRedirectOptions = { history: true, freeze: false };
// Предполагаем, что минимум за 5 секунд успеем сделать редирект
const SLEEP_TIME_FREEZE = 5000;
/**
* Безопасно перенаправляет браузер на указанный URL.
*
* @param url Строка URL или относительный путь (а так же query, hash).
* @throws {TypeError} Если URL некорректен.
* @example
* safeRedirect('<https://example.com>');
* safeRedirect('/path/to/page');
* safeRedirect('javascript:alert("Я никогда не выполнюсь..")')
* .catch((error) => console.error('Зато я выполнился!', error));
* await safeRedirect('<https://example.com>', { freeze: true }); // заморозит выполнение кода до перенаправления
*/
export function safeRedirect(url: string, optionsArg: SafeRedirectOptions = defaultSafeRedirectOptions): Promise<void> {
const options = { ...defaultSafeRedirectOptions, ...optionsArg };
return new Promise((resolve, reject) => {
try {
window.location[options.history ? 'assign' : 'replace'](new URL(url, window.location.origin));
if (options.freeze) {
// В целом мы могли бы просто не вызывать resolve здесь,
// но это было бы ошибкой, потому что привело бы
// к утечкам памяти
sleep(SLEEP_TIME_FREEZE).then(() => resolve());
} else {
resolve();
}
} catch (error) {
console.error('[safeRedirect] Некорректный URL:', url, error);
reject(new TypeError(`Invalid URL provided: ${url}`));
}
});
}
async function onError(status: number) {
if (status === 401) {
// промис не резолвится, код ниже не выполнится
await safeRedirect('/login', { freeze: true });
return;
}
// другая обработка ошибок
}
В большинстве случаев window.location.href
и window.location.assign()
взаимозаменяемы: оба делают обычный редирект с записью в историю.
Если важна лаконичность — достаточно href
. Если нужен чистый, явно тестируемый и расширяемый код — отдайте предпочтение location.assign()
.
Не забывайте про валидацию URL и блокирование рендера, когда нужно остановить дальнейшее выполнение после редиректа.