← Браузер/URL API#154 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

URL API

Представь: ты строишь страницу каталога с фильтрами — категория, ценовой диапазон, сортировка, страница. Состояние фильтров нужно сохранять в URL (?category=phones&price_min=5000&page=2), чтобы пользователь мог поделиться ссылкой. Вручную конкатенировать строки URL надёжно? Нет. URL API — правильный инструмент.

Что решает этот механизм

Ручная сборка URL через конкатенацию строк ломается на пробелах, кириллице и спецсимволах. URL API автоматически экранирует значения, корректно обрабатывает все части URL и позволяет изменять отдельные компоненты не затрагивая остальные.

На основе предыдущих уроков

  • строки — URL-параметры это строки, URLSearchParams инкапсулирует логику их разбора
  • fetch — URL-объект передаётся в fetch напрямую вместо строки
  • new URL(string)

    const url = new URL('https://shop.example.com/catalog?category=phones&page=2#reviews')
    
    console.log(url.href)      // 'https://shop.example.com/catalog?category=phones&page=2#reviews'
    console.log(url.origin)    // 'https://shop.example.com'
    console.log(url.protocol)  // 'https:'
    console.log(url.host)      // 'shop.example.com'
    console.log(url.hostname)  // 'shop.example.com' (без порта)
    console.log(url.port)      // '' (80/443 не показываются)
    console.log(url.pathname)  // '/catalog'
    console.log(url.search)    // '?category=phones&page=2'
    console.log(url.hash)      // '#reviews'

    Изменение частей URL

    Все свойства URL доступны для записи:

    const url = new URL('https://example.com/old-path')
    
    url.pathname = '/new-path'
    url.search   = '?version=2'
    url.hash     = '#section-1'
    
    console.log(url.href)
    // 'https://example.com/new-path?version=2#section-1'

    URLSearchParams — работа с параметрами запроса

    const url = new URL('https://api.example.com/products?sort=price&order=asc')
    const params = url.searchParams  // объект URLSearchParams
    
    // Чтение
    console.log(params.get('sort'))       // 'price'
    console.log(params.get('order'))      // 'asc'
    console.log(params.get('missing'))    // null
    console.log(params.has('sort'))       // true
    
    // Изменение
    params.set('sort', 'name')       // заменить значение
    params.append('tag', 'sale')     // добавить параметр (может дублироваться)
    params.delete('order')           // удалить параметр
    
    console.log(url.href)
    // 'https://api.example.com/products?sort=name&tag=sale'
    
    // Итерация по параметрам
    for (const [key, value] of params) {
      console.log(key, '=', value)
    }
    
    // Все значения для одного ключа (если append использовался несколько раз)
    const tags = params.getAll('tag')  // ['sale']

    Относительные URL

    URL умеет резолвить относительные пути:

    const base = new URL('https://example.com/api/v1/')
    
    const endpoint = new URL('./users', base)
    console.log(endpoint.href)  // 'https://example.com/api/v1/users'
    
    const absolute = new URL('/admin', base)
    console.log(absolute.href)  // 'https://example.com/admin'

    URL.createObjectURL(blob) — временные ссылки

    // Создать ссылку для скачивания файла
    function downloadFile(content, filename, type = 'text/plain') {
      const blob = new Blob([content], { type })
      const url  = URL.createObjectURL(blob)
    
      const a    = document.createElement('a')
      a.href     = url
      a.download = filename
      a.click()
    
      // Обязательно освобождаем память после использования
      URL.revokeObjectURL(url)
    }
    
    downloadFile('{ "data": 42 }', 'result.json', 'application/json')

    Реальный пример: построение URL для API

    function buildProductsURL(baseURL, filters) {
      const url = new URL(baseURL)
    
      if (filters.category) url.searchParams.set('category', filters.category)
      if (filters.minPrice) url.searchParams.set('price_gte', filters.minPrice)
      if (filters.maxPrice) url.searchParams.set('price_lte', filters.maxPrice)
      if (filters.page)     url.searchParams.set('page', filters.page)
      if (filters.sort)     url.searchParams.set('sort', filters.sort)
    
      return url.href
    }
    
    const apiURL = buildProductsURL('https://api.example.com/products', {
      category: 'smartphones',
      minPrice: 10000,
      maxPrice: 80000,
      page: 2,
      sort: 'price_asc',
    })
    
    console.log(apiURL)
    // 'https://api.example.com/products?category=smartphones&price_gte=10000&price_lte=80000&page=2&sort=price_asc'

    Типичные ошибки

    1. Строить URL конкатенацией строк — спецсимволы ломают запрос

    // ПЛОХО — пробелы и кириллица в параметрах ломают URL
    const city = 'Нижний Новгород'
    const url = '/api/weather?city=' + city  // /api/weather?city=Нижний Новгород — сломано!
    
    // ХОРОШО — URLSearchParams экранирует автоматически
    const params = new URLSearchParams({ city })
    const url2 = '/api/weather?' + params.toString()
    // /api/weather?city=%D0%9D%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9+%D0%9D%D0%BE%D0%B2%D0%B3%D0%BE%D1%80%D0%BE%D0%B4

    2. Передавать относительный URL без базы в new URL()

    // ПЛОХО — без базы относительный URL выбросит TypeError
    const url = new URL('/api/users')  // TypeError: Failed to construct 'URL': Invalid URL
    
    // ХОРОШО — передай базу вторым аргументом
    const url2 = new URL('/api/users', 'https://example.com')
    console.log(url2.href)  // 'https://example.com/api/users'
    
    // Или используй window.location.origin в браузере
    const url3 = new URL('/api/users', window.location.origin)

    3. Забыть revokeObjectURL — утечка памяти при работе с Blob

    // ПЛОХО — URL объект в памяти до перезагрузки страницы
    const blob = new Blob([data], { type: 'text/csv' })
    const url = URL.createObjectURL(blob)
    link.href = url
    link.click()
    // URL.revokeObjectURL(url) забыт — утечка памяти!
    
    // ХОРОШО — освобождаем сразу после использования
    link.click()
    URL.revokeObjectURL(url)  // освобождаем память

    В реальных проектах

  • SPA роутинг: React Router, Vue Router строят URL через new URL() для парсинга текущего адреса и навигации
  • API клиенты: axios, ky внутри используют URL и URLSearchParams для безопасной сборки запросов с параметрами
  • Экспорт данных: кнопки «скачать CSV/JSON» используют URL.createObjectURL(blob) для генерации ссылки без серверного запроса
  • Примеры

    URL API: парсинг URL, работа с URLSearchParams, построение API-запросов с фильтрами

    // URL API доступен в современных sandbox-окружениях
    // Используем реальный класс URL
    
    // --- Демо 1: Разбор URL на части ---
    console.log('=== Парсинг URL ===')
    
    const shopUrl = new URL('https://shop.example.com:8080/catalog/phones?brand=apple&sort=price&page=3#specs')
    
    console.log('href:     ', shopUrl.href)
    console.log('origin:   ', shopUrl.origin)    // 'https://shop.example.com:8080'
    console.log('protocol: ', shopUrl.protocol)  // 'https:'
    console.log('host:     ', shopUrl.host)      // 'shop.example.com:8080'
    console.log('hostname: ', shopUrl.hostname)  // 'shop.example.com'
    console.log('port:     ', shopUrl.port)      // '8080'
    console.log('pathname: ', shopUrl.pathname)  // '/catalog/phones'
    console.log('search:   ', shopUrl.search)    // '?brand=apple&sort=price&page=3'
    console.log('hash:     ', shopUrl.hash)      // '#specs'
    
    // --- Демо 2: URLSearchParams ---
    console.log('\n=== URLSearchParams ===')
    
    const params = shopUrl.searchParams
    
    console.log('brand:', params.get('brand'))  // 'apple'
    console.log('sort:', params.get('sort'))    // 'price'
    console.log('page:', params.get('page'))    // '3'
    console.log('has(brand):', params.has('brand'))     // true
    console.log('has(color):', params.has('color'))     // false
    console.log('get(missing):', params.get('missing')) // null
    
    // Изменение параметров
    params.set('page', '4')
    params.append('tag', 'sale')
    params.append('tag', 'new')
    params.delete('sort')
    
    console.log('\nПосле изменений:')
    console.log('page:', params.get('page'))        // '4'
    console.log('tag (все):', params.getAll('tag')) // ['sale', 'new']
    console.log('sort:', params.get('sort'))        // null — удалён
    console.log('URL после:', shopUrl.href)
    
    // Итерация
    console.log('\nВсе параметры:')
    for (const [key, value] of shopUrl.searchParams) {
      console.log(` ${key} = ${value}`)
    }
    
    // --- Демо 3: Построение API URL ---
    console.log('\n=== Построение URL для API ===')
    
    function buildApiURL(base, filters) {
      const url = new URL(base)
      const validFilters = {
        category:  v => url.searchParams.set('category', v),
        minPrice:  v => url.searchParams.set('price_gte', v),
        maxPrice:  v => url.searchParams.set('price_lte', v),
        inStock:   v => url.searchParams.set('in_stock', v ? '1' : '0'),
        page:      v => url.searchParams.set('page', v),
        limit:     v => url.searchParams.set('limit', v),
        sort:      v => url.searchParams.set('sort', v),
        tags:      v => v.forEach(t => url.searchParams.append('tags', t)),
      }
    
      for (const [key, value] of Object.entries(filters)) {
        if (value !== undefined && value !== null && validFilters[key]) {
          validFilters[key](value)
        }
      }
    
      return url
    }
    
    const apiUrl = buildApiURL('https://api.myshop.ru/v2/products', {
      category: 'laptops',
      minPrice: 50000,
      maxPrice: 150000,
      inStock: true,
      page: 1,
      limit: 20,
      sort: 'price_asc',
      tags: ['gaming', 'sale'],
    })
    
    console.log('API URL:')
    console.log(apiUrl.href)
    console.log('\nПараметры по отдельности:')
    for (const [k, v] of apiUrl.searchParams) {
      console.log(` ${k}: ${v}`)
    }
    
    // --- Демо 4: Относительные URL ---
    console.log('\n=== Относительные URL ===')
    
    const base = new URL('https://api.example.com/v2/users/123/')
    const relative1 = new URL('./profile', base)
    const relative2 = new URL('../posts', base)
    const relative3 = new URL('/admin/settings', base)
    
    console.log('base:      ', base.href)
    console.log('./profile: ', relative1.href)   // ...users/123/profile
    console.log('../posts:  ', relative2.href)   // ...v2/users/posts
    console.log('/admin:    ', relative3.href)   // ...example.com/admin/settings

    URL API

    Представь: ты строишь страницу каталога с фильтрами — категория, ценовой диапазон, сортировка, страница. Состояние фильтров нужно сохранять в URL (?category=phones&price_min=5000&page=2), чтобы пользователь мог поделиться ссылкой. Вручную конкатенировать строки URL надёжно? Нет. URL API — правильный инструмент.

    Что решает этот механизм

    Ручная сборка URL через конкатенацию строк ломается на пробелах, кириллице и спецсимволах. URL API автоматически экранирует значения, корректно обрабатывает все части URL и позволяет изменять отдельные компоненты не затрагивая остальные.

    На основе предыдущих уроков

  • строки — URL-параметры это строки, URLSearchParams инкапсулирует логику их разбора
  • fetch — URL-объект передаётся в fetch напрямую вместо строки
  • new URL(string)

    const url = new URL('https://shop.example.com/catalog?category=phones&page=2#reviews')
    
    console.log(url.href)      // 'https://shop.example.com/catalog?category=phones&page=2#reviews'
    console.log(url.origin)    // 'https://shop.example.com'
    console.log(url.protocol)  // 'https:'
    console.log(url.host)      // 'shop.example.com'
    console.log(url.hostname)  // 'shop.example.com' (без порта)
    console.log(url.port)      // '' (80/443 не показываются)
    console.log(url.pathname)  // '/catalog'
    console.log(url.search)    // '?category=phones&page=2'
    console.log(url.hash)      // '#reviews'

    Изменение частей URL

    Все свойства URL доступны для записи:

    const url = new URL('https://example.com/old-path')
    
    url.pathname = '/new-path'
    url.search   = '?version=2'
    url.hash     = '#section-1'
    
    console.log(url.href)
    // 'https://example.com/new-path?version=2#section-1'

    URLSearchParams — работа с параметрами запроса

    const url = new URL('https://api.example.com/products?sort=price&order=asc')
    const params = url.searchParams  // объект URLSearchParams
    
    // Чтение
    console.log(params.get('sort'))       // 'price'
    console.log(params.get('order'))      // 'asc'
    console.log(params.get('missing'))    // null
    console.log(params.has('sort'))       // true
    
    // Изменение
    params.set('sort', 'name')       // заменить значение
    params.append('tag', 'sale')     // добавить параметр (может дублироваться)
    params.delete('order')           // удалить параметр
    
    console.log(url.href)
    // 'https://api.example.com/products?sort=name&tag=sale'
    
    // Итерация по параметрам
    for (const [key, value] of params) {
      console.log(key, '=', value)
    }
    
    // Все значения для одного ключа (если append использовался несколько раз)
    const tags = params.getAll('tag')  // ['sale']

    Относительные URL

    URL умеет резолвить относительные пути:

    const base = new URL('https://example.com/api/v1/')
    
    const endpoint = new URL('./users', base)
    console.log(endpoint.href)  // 'https://example.com/api/v1/users'
    
    const absolute = new URL('/admin', base)
    console.log(absolute.href)  // 'https://example.com/admin'

    URL.createObjectURL(blob) — временные ссылки

    // Создать ссылку для скачивания файла
    function downloadFile(content, filename, type = 'text/plain') {
      const blob = new Blob([content], { type })
      const url  = URL.createObjectURL(blob)
    
      const a    = document.createElement('a')
      a.href     = url
      a.download = filename
      a.click()
    
      // Обязательно освобождаем память после использования
      URL.revokeObjectURL(url)
    }
    
    downloadFile('{ "data": 42 }', 'result.json', 'application/json')

    Реальный пример: построение URL для API

    function buildProductsURL(baseURL, filters) {
      const url = new URL(baseURL)
    
      if (filters.category) url.searchParams.set('category', filters.category)
      if (filters.minPrice) url.searchParams.set('price_gte', filters.minPrice)
      if (filters.maxPrice) url.searchParams.set('price_lte', filters.maxPrice)
      if (filters.page)     url.searchParams.set('page', filters.page)
      if (filters.sort)     url.searchParams.set('sort', filters.sort)
    
      return url.href
    }
    
    const apiURL = buildProductsURL('https://api.example.com/products', {
      category: 'smartphones',
      minPrice: 10000,
      maxPrice: 80000,
      page: 2,
      sort: 'price_asc',
    })
    
    console.log(apiURL)
    // 'https://api.example.com/products?category=smartphones&price_gte=10000&price_lte=80000&page=2&sort=price_asc'

    Типичные ошибки

    1. Строить URL конкатенацией строк — спецсимволы ломают запрос

    // ПЛОХО — пробелы и кириллица в параметрах ломают URL
    const city = 'Нижний Новгород'
    const url = '/api/weather?city=' + city  // /api/weather?city=Нижний Новгород — сломано!
    
    // ХОРОШО — URLSearchParams экранирует автоматически
    const params = new URLSearchParams({ city })
    const url2 = '/api/weather?' + params.toString()
    // /api/weather?city=%D0%9D%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9+%D0%9D%D0%BE%D0%B2%D0%B3%D0%BE%D1%80%D0%BE%D0%B4

    2. Передавать относительный URL без базы в new URL()

    // ПЛОХО — без базы относительный URL выбросит TypeError
    const url = new URL('/api/users')  // TypeError: Failed to construct 'URL': Invalid URL
    
    // ХОРОШО — передай базу вторым аргументом
    const url2 = new URL('/api/users', 'https://example.com')
    console.log(url2.href)  // 'https://example.com/api/users'
    
    // Или используй window.location.origin в браузере
    const url3 = new URL('/api/users', window.location.origin)

    3. Забыть revokeObjectURL — утечка памяти при работе с Blob

    // ПЛОХО — URL объект в памяти до перезагрузки страницы
    const blob = new Blob([data], { type: 'text/csv' })
    const url = URL.createObjectURL(blob)
    link.href = url
    link.click()
    // URL.revokeObjectURL(url) забыт — утечка памяти!
    
    // ХОРОШО — освобождаем сразу после использования
    link.click()
    URL.revokeObjectURL(url)  // освобождаем память

    В реальных проектах

  • SPA роутинг: React Router, Vue Router строят URL через new URL() для парсинга текущего адреса и навигации
  • API клиенты: axios, ky внутри используют URL и URLSearchParams для безопасной сборки запросов с параметрами
  • Экспорт данных: кнопки «скачать CSV/JSON» используют URL.createObjectURL(blob) для генерации ссылки без серверного запроса
  • Примеры

    URL API: парсинг URL, работа с URLSearchParams, построение API-запросов с фильтрами

    // URL API доступен в современных sandbox-окружениях
    // Используем реальный класс URL
    
    // --- Демо 1: Разбор URL на части ---
    console.log('=== Парсинг URL ===')
    
    const shopUrl = new URL('https://shop.example.com:8080/catalog/phones?brand=apple&sort=price&page=3#specs')
    
    console.log('href:     ', shopUrl.href)
    console.log('origin:   ', shopUrl.origin)    // 'https://shop.example.com:8080'
    console.log('protocol: ', shopUrl.protocol)  // 'https:'
    console.log('host:     ', shopUrl.host)      // 'shop.example.com:8080'
    console.log('hostname: ', shopUrl.hostname)  // 'shop.example.com'
    console.log('port:     ', shopUrl.port)      // '8080'
    console.log('pathname: ', shopUrl.pathname)  // '/catalog/phones'
    console.log('search:   ', shopUrl.search)    // '?brand=apple&sort=price&page=3'
    console.log('hash:     ', shopUrl.hash)      // '#specs'
    
    // --- Демо 2: URLSearchParams ---
    console.log('\n=== URLSearchParams ===')
    
    const params = shopUrl.searchParams
    
    console.log('brand:', params.get('brand'))  // 'apple'
    console.log('sort:', params.get('sort'))    // 'price'
    console.log('page:', params.get('page'))    // '3'
    console.log('has(brand):', params.has('brand'))     // true
    console.log('has(color):', params.has('color'))     // false
    console.log('get(missing):', params.get('missing')) // null
    
    // Изменение параметров
    params.set('page', '4')
    params.append('tag', 'sale')
    params.append('tag', 'new')
    params.delete('sort')
    
    console.log('\nПосле изменений:')
    console.log('page:', params.get('page'))        // '4'
    console.log('tag (все):', params.getAll('tag')) // ['sale', 'new']
    console.log('sort:', params.get('sort'))        // null — удалён
    console.log('URL после:', shopUrl.href)
    
    // Итерация
    console.log('\nВсе параметры:')
    for (const [key, value] of shopUrl.searchParams) {
      console.log(` ${key} = ${value}`)
    }
    
    // --- Демо 3: Построение API URL ---
    console.log('\n=== Построение URL для API ===')
    
    function buildApiURL(base, filters) {
      const url = new URL(base)
      const validFilters = {
        category:  v => url.searchParams.set('category', v),
        minPrice:  v => url.searchParams.set('price_gte', v),
        maxPrice:  v => url.searchParams.set('price_lte', v),
        inStock:   v => url.searchParams.set('in_stock', v ? '1' : '0'),
        page:      v => url.searchParams.set('page', v),
        limit:     v => url.searchParams.set('limit', v),
        sort:      v => url.searchParams.set('sort', v),
        tags:      v => v.forEach(t => url.searchParams.append('tags', t)),
      }
    
      for (const [key, value] of Object.entries(filters)) {
        if (value !== undefined && value !== null && validFilters[key]) {
          validFilters[key](value)
        }
      }
    
      return url
    }
    
    const apiUrl = buildApiURL('https://api.myshop.ru/v2/products', {
      category: 'laptops',
      minPrice: 50000,
      maxPrice: 150000,
      inStock: true,
      page: 1,
      limit: 20,
      sort: 'price_asc',
      tags: ['gaming', 'sale'],
    })
    
    console.log('API URL:')
    console.log(apiUrl.href)
    console.log('\nПараметры по отдельности:')
    for (const [k, v] of apiUrl.searchParams) {
      console.log(` ${k}: ${v}`)
    }
    
    // --- Демо 4: Относительные URL ---
    console.log('\n=== Относительные URL ===')
    
    const base = new URL('https://api.example.com/v2/users/123/')
    const relative1 = new URL('./profile', base)
    const relative2 = new URL('../posts', base)
    const relative3 = new URL('/admin/settings', base)
    
    console.log('base:      ', base.href)
    console.log('./profile: ', relative1.href)   // ...users/123/profile
    console.log('../posts:  ', relative2.href)   // ...v2/users/posts
    console.log('/admin:    ', relative3.href)   // ...example.com/admin/settings

    Задание

    Напиши функцию parseQueryString(urlString) которая принимает URL-строку и возвращает объект с параметрами запроса (ключ: значение). Если у ключа несколько значений (через append) — значение должно быть массивом. Напиши функцию buildURL(base, params) которая добавляет параметры из объекта к URL и возвращает строку.

    Подсказка

    parseQueryString: итерируй url.searchParams циклом for...of. buildURL: для массивов используй append, для одиночных значений — set.

    Загружаем среду выполнения...
    Загружаем AI-помощника...