JavaScript: Более безопасное чтение и запись URL

Источник: «Safer URL reading and writing in modern JavaScript»
Создание и манипулирование URL-адресами в виде строк может казаться безопасным, пока не сложатся общие ошибки. Но у современного JavaScript есть решение для этого, Конструктор URL, делает чтение, запись и изменение URL-адресов более безопасным и чистым.

Возможно вы неосознанно пишите URL-адреса небезопасным способом

Можете найти баг в этом коде?

const url = `https://builder.io/api/v2/content
?model=
${model}&locale=${locale}?query.text=${text}`


const res = await fetch(url)

Их как минимум три…

Мы разберём их ниже:

Распространённая проблема 1 — неправильные символы-разделители

Это, безусловно ошибка новичка, но её так легко пропустить, что я поймал ей в собственном коде даже после 10 лет JS разработки.

url = url + '?foo=bar'

Но подождите, в исходном URL-адресе мог быть параметр запроса. Ok, должно быть так:

url = url + '&foo=bar'

Но подождите, если в исходном URL-адресе не было параметров запроса, то теперь это неправильно…

Распространённая проблема 2 — забыли закодировать

model и locale, скорее всего не нужно кодировать, так как они безопасны для URL-адресов, но я не перестаю думать, что text может быть любым текстом, включая пробелы и специальные символы, которые вызовут проблемы.

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

const url = `https://builder.io/api/v2/content
?model=
${
encodeURIComponent(model)
}
&locale=${
encodeURIComponent(locale)
}
&query.text=${
encodeURIComponent(text)
}
`

Но всё кажется немного… уродливее.

Распространённая проблема 3 — случайные пробелы

Чтобы разбить этот длинный URL-адрес на несколько строк, мы случайно включили в URL-адрес символ новой строки и лишние пробелы, из-за чего запрос по этому адресу не будет работать должным образом.

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

const url = `https://builder.io/api/v2/content`
+ `?model=${
encodeURIComponent(model)
}
&locale=${
encodeURIComponent(locale)
}
&query.text=${
encodeURIComponent(text)
}
`

Это сильно сложно для правильного построения одного URL-адреса. И будем ли мы помнить об этом в следующий раз, тем более, что приближается дедлайн, и нам нужно скорее выпустить или исправить эту новую функцию как можно быстрее?

Должен быть лучший способ…

URL конструктор

Более чистым и безопасным решением вышеуказанных проблем — использование URL конструктора.

const url = new URL('https://builder.io/api/v2/content')

url.searchParams.set('model', model)
url.searchParams.set('locale', locale)
url.searchParams.set('text', text)

const res = await fetch(url.toString())

Он решает несколько проблем:

Изменение URL-адресов

Это также полезно в случаях, когда мы изменяем URL-адрес, текущее состояние которого не известно.

Например, вместо этой проблемы:

url += (url.includes('?') ? '&' : '?') + 'foo=bar'

Мы можем сделать так:

// Полагая, что `url` это URL-адрес
url.searchParams.set('foo', 'bar')

// Или если URL это строка
const structuredUrl = new URL(url)
structuredUrl.searchParams.set('foo', 'bar')
url = structuredUrl.toString()

Точно также можно переписать другие части URL:

const url = new URL('https://builder.io')

url.pathname = '/blog' // Обновляем путь
url.hash = '#featured' // Обновляем хэш
url.host = 'www.builder.io' // Обновляем хост

url.toString() // https://www.builder.io/blog#featured

Чтение параметров URL

Теперь старая проблема Я просто хочу прочитать параметр запроса из текущего URL без библиотеки решена.

const pageParam = new URL(location.href).searchParams.get('page')

Или обновите текущий URL-адрес с помощью:

const url = new URL(location.href)
const currentPage = Number(url.searchParams.get('page'))
url.searchParams.set('page', String(currentPage + 1))
location.href = url.toString()

Но это не ограничивается только браузером. Его также можно использовать в Node.js.

const http = require('node:http');

const server = http.createServer((req, res) => {
const url = new URL(req.url, `https://${req.headers.host}`)
// Чтение пути, запроса и т.д. ...
});

А также Deno:

import { serve } from "https://deno.land/std/http/mod.ts";
async function reqHandler(req: Request) {
const url = new URL(req.url)
// Чтение пути, запроса и т.д. ...
return new Response();
}
serve(reqHandler, { port: 8000 });

Свойства URL, которые нужно знать

Экземпляры URL поддерживают все привычные свойства, из которых вы можете читать или писать.

const url = new URL('https://builder.io/blog?page=1');

url.protocol // https:
url.host // builder.io
url.pathname // /blog
url.search // ?page=1
url.href // https://builder.io/blog?page=1
url.origin // https://builder.io
url.searchParams.get('page') // 1

Методы URLSearchParams, которые нужно знать

Объект URLSearchParam, доступный в экземпляре URL как url.searchParams, поддерживает ряд удобных методов:

searchParams.has(name)

Проверяет содержит ли параметр поиска заданное имя

url.searchParams.has('page') // true

searchParams.get(name)

Получить значение заданного параметра

url.searchParams.get('page') // '1'

searchParams.getAll(name)

Получить все значения заданного параметра. Это удобно, если разрешено использование нескольких значений с одним и тем же именем, например &page=1&page=2.

url.searchParams.getAll('page') // ['1']

searchParams.set(name, value)

Задать значение параметра

url.searchParams.set('page', '1')

searchParams.append(name, value)

Добавить параметр. Полезно, если вы поддерживаете использование нескольких значений одним параметром, например &page=1&page=2.

url.searchParams.append('page', '2')

searchParams.delete(name)

Полностью удалить параметр из URL

url.searchParams.delete('page')

Поддержка браузерами и средами исполнения

new URL() поддерживается всеми современными браузерами, а так же Node.js и Deno!

Дополнительные материалы

Предыдущая Статья

Laravel 10: Стоит ли обновлять?

Следующая Статья

JavaScript: Как использовать Коллекции — Map и Set