Рендер страницы в JavaScript

Начинаем наше погружение в работу фронтенд-библиотек и фреймворков. И даже если вы занимались только бэкендом и мир JavaScript вам казался страшным, то самое время расширить кругозор и заглянуть на его сотрону. Как хороший дизайнер должен представлять, можно ли нарисованную им страницу сверстать, так и хорошему бэкендеру было бы неплохо понимать, что от вас хотят фронтендеры. Да и помимо кругозора там можно подсмотреть многие паттерны, которые для вас могут оказаться полезными.

Так что начнём наше исследование на примере написания с нуля браузерного приложения. И сегодня с классического серверного рендеринга HTML-страниц перейдём на построение DOM-дерева через JavaScript. Заодно рассмотрим пользу отделения данных от представления. Выделим побочные эффекты и сделаем удобные и тестируемые чистые функции:

  • 00:00:36 Вопрос работы с фронтендом
  • 00:01:57 Статическая страница
  • 00:04:16 Корневой элемент
  • 00:04:53 Создание логотипа в JavaScript
  • 00:07:31 Генерация шапки сайта
  • 00:08:10 Вывод текущего времени
  • 00:09:26 Группировка состояния страницы
  • 00:10:56 Вывод лотов
  • 00:14:03 Проблема неструктурированного кода
  • 00:14:36 Декомпозиция на компоненты
  • 00:16:13 Всплытие определений в JavaScript
  • 00:17:12 Концепция чистой функции
  • 00:21:15 Группировка аргументов функции
  • 00:23:37 Раздельный вывод лотов
  • 00:25:30 Компонент приложения
  • 00:25:58 Процедура рендерига

А в следующем эпизоде в нашей свёрстанной странице добавим динамику. Чтобы данные подгружались по API и в реальном времени обновлялись цены через WebSocket.

Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (24)
А

Простой урок, но при этом полезная тема про распаковку структур. Очень ждал что-нибудь про React или vue

Ответить
Руслан

Спасибо!

Ответить
Yevhenii Lykholai

Спасибо.

Ответить
Григорий

Отличный материал, давно задавался вопросом от чего отталкиваться при работе с фронтендом, и очень помогла проведённая аналогия бекендом. Спасибо!

Ответить
Кирилл

За временнЫе метки - респект!

Ответить
Paul

Спасибо, очень интересно!

Возник вопрос, почему в современном js так любят использовать const вместо переменных? Разве это не противоречит самой сути констант?

Ответить
Дмитрий Елисеев

Вместо var теперь рекомендуется использовать let для переопределяемых значений и const для привязанных навсегда.

Константу const a=5 или const b={amount:3} нельзя позже переопределить на новое значение a=6 или b={amount:7}. В константе будет постоянно храниться указатель на первоначальный объект. Но на сам объект при этом никакое ограничение не накладывается, поэтому можно поменять любое его поле напрямую через b.amount=7.

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

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

Ответить
Paul

Дмитрий, спасибо за ответ!

Ответить
Александр Кулик

Супер. Очень доходчиво

Ответить
Aёct'ann

Материал на высшем уровне!

Ответить
Tema Ovchinnikov

Спасибо, очень интересно! Если использовать history api то получается если взять к примеру вк. Левая часть (меню) статическая, а правая динамическая. Это получается на сервере возвращается json и на основе этого рендерится новостная лента список друзей мета информация страницы и тд?

Таким образом при воспроизведении музыки спокойно можно кликать на разные вкладки меню без перезагрузки страницы? Если вместо json фрагмент html отправить site.local/api/news это будет не быстрее чем на клиенте рендерить?

Ответить
Дмитрий Елисеев

Левая часть (меню) статическая, а правая динамическая.

Да, все фронтенд-приложения Single Page Application (SPA) используют History API вместо реальной смены адреса с переоткрытием страницы. То есть навешиваются на onCLick всех ссылок и просто меняют адрес. И в зависимости от текущего пути уже выводят разные компоненты внутри шаблона. Например, у нас мы можем внутри 'App' под вызовом Header() вызывать некую функцию вроде:

function Content ({ path }) {
  if (path === '/') {
      return Home()
  } else if (path === '/lots') {
      return Lots()
  }
}

в которую передавать window.location.pathname. Она будет работать как маршрутизатор и в итоге на странице будет заменяться только контент.

И каждый компонент-страница вроде Lots уже будет запрашивать нужный ему JSON по API.

Ответить
Дмитрий Елисеев

Если вместо json фрагмент html отправить /api/news это будет не быстрее чем на клиенте рендерить?

Браузеру почти нет разницы, откуда он всё берёт. Хоть с HTML, хоть из JS. Всё равно он сначала парсит тэги из HTML в дерево элементов и только после этого их рендерит.

Как раз в следующем эпизоде мы добавим динамическую загрузку и подгрузку данных. И там увидим, что основное неудобство будет не в первой загрузке данных, а в последующем обновлении. И там уже на JavaScript будет удобнее манипулировать элементами.

Ответить
Сергей

Спасибо. Хорошо излагаете.

Ответить
Тимур

Отличный материал. Но несколько "НО":

  • нужно было сказать, что есть 2 метода добавления: append и appendChild. В первом можно добавить как Node узел, так и текст. В случае с appendChild можно добавить только Node. Так как мы с помощью append добавляем только Node, лучше использовать appendChild. Есть подозрения, что в этом случае интерпритатор не проверяет тип, что экономит время
  • getElementByID устарел (хоть и работает). Для таких целей лучше использовать document.querySelector(), так будет понятнее для людей, кто пришел в профессию из верстки
  • JavaScript инициализирует все функции перед выполнением кода только тогда, когда используется декларативное описание. При использовании Function Expression такой подход вызовет ошибку, ибо функция создается только в момент объявления

А так все по делу

Ответить
Алексей

Как всегда спасибо, и есть вопрос, я посмотрел пока только 6 уроков, но вижу что react даже с JSX сложен для восприятия и главное для редактирования простым верстальщиком который знает html+css, посмотрел на vue и он вроде проще для визуального восприятия и изменения верстки, вопрос почему вы выбрали React и чем он лучше Vue, спасибо!

Ответить
Дмитрий Елисеев

Они идут от разных сторон. React из JS, а Vue из HTML.

React использует нативный JS с React.createElement. И просто через примитивный JSX позволяет заменять вызов React.createElement на HTML-подобный синтаксис.

А Vue изначально имеет полноценный отдельный HTML-шаблонизатор с <template>, куда уже нужно вписывать нужные ему управляющие конструкции вроде v-if и v-for.

простым верстальщиком который знает html+css

Верно. React с JSX удобнее и привычнее JS-программисту, но меньше понятен HTML-верстальщику.

А Vue своим HTML-шаблонизатором <template> удобен верстальщику, но неудобен программисту.

Ответить
Алексей

Спасибо за ответ, все как всегда подробно и понятно!

Ответить
Михаил

Отлично материал подается! Сам я бэкендер, но чтобы не дергать фронтовиков по мелочам и понимать, что там происходит, стараюсь быть в теме клиентской стороны. Спасибо автору, все по делу и очень понятно.

Ответить
Александр

Спасибо за урок! Есть немного обратной связи:

  1. Названия функций должны быть глаголом. Дано понятие "Компонент", но не сказано, что это такое и почему функции с большой буквы. Зная твой подход, я то конечно подозреваю, что далее ты к этому подведешь и станет понятно. :)

  2. Зависимостей между функциями лучше же тоже избегать, даже если они чистые?

  3. Сокращать синтакис объявления объекта мне кажется лишним и ошибочным. Не знаю почему разработчики пошли по этому пути! :)

lots.forEach((lot) => {
    node.append(Lot({ lot: lot }));
});
lots.forEach((lot) => {
    node.append(Lot({ lot }));
});

Первый вариант мне кажется более читаемым.

Ответить
Дмитрий Елисеев
  1. Компонент в React можно сделать в виде функции или в виде класса. Поэтому удобно их всегда именовать по смыслу существительным Header вместо названия-глагола renderHeader для функции и Header для класса. Ещё такое особое именование компонентов-виджетов удобно, чтобы не путать их с другими обычными функциями-глаголами.

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

  3. Да, некоторым более читаемо классическое многословное написание:

function (name) {
  return { name: name }
}

вместо нового короткого аналога:

(name) => ({ name })

Синтаксический сахар практически всегда придумывается для укорачивания кода. Со временем он становится привычным.

Ответить
Олег

Спасибо за как всегда блестящую подачу материала, Дмитрий! Просмотрел весь цикл до конца и вернулся в начало, чтобы переосмыслить увиденное. И неожиданно для себя обнаружил несколько однотипных тавтологий и мест, которые можно было бы упростить. Я по поводу деструктуризации. Она теряет смысл, когда в объявлении функции и при её вызове используется один объект. Таких примеров здесь несколько, например

function App ({state}) {...}

и потом в вызовах

App({state})

Здесь достаточно простого синтаксиса

function App (state) {...}

а затем

App(state)

В вызовах других методов, например, в Clock внутри App() не нужна такая подстановка аргументов, как

node.append(Clock({time: time.state}))

Вместо этого можно было просто написать

node.append(Clock(state))

поскольку функция Clock объявлена с оператором деструктуризации как

function Clock({time})

И таких примеров тоже несколько.

Ответить
Дмитрий Елисеев

Я по поводу деструктуризации. Она теряет смысл, когда в объявлении функции и при её вызове используется один объект.

Сейчас многие функции-компоненты принимают только один аргумент. Пока мы вызываем функции вручную мы можем передавать его просто без деструктуризации.

Но вскоре мы перейдём на JSON-формат описания виртуального дерева и синтаксис JSX. Там мы со всеми компонентами будем работать уже универсально атоматически, а не вручную. И там будет удобнее передавать один объект props, чтобы не путаться в числе параметров и их порядке.

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

Ответить
Tema Ovchinnikov

Интересно получается

let state = {
  time: new Date(),
  lotsData: [
    { id: 1, title: 'Apple', description: 'Apple description', price: '16' },
    { id: 2, title: 'Orange', description: 'Orange description', price: '41' },
  ],
};

function Logo() {
  return '<img class="logo" src="img-logo.png" alt="" />';
}

function Header() {
  return '<header class="header">${Logo()}</header>';
}

function Clock({ time }) {
  return '<div class="clock">${time.toLocaleTimeString()}</div>';
}

function Lot(lotData) {
  return (
    `<article class="lot">
      <div>${lotData.price}</div>
      <h1>${lotData.title}</h1>
      <p>${lotData.description}</p>
    </article>'
  );
}

function Lots({ lotsData }) {
  return lotsData.map((lotData) => '<div class="lots">${Lot(lotData)}</div>');
}

function App({ state }) {
  return '<div class="app">${Header()} ${Clock({ time: state.time })} ${Lots({
    lotsData: state.lotsData,
  })}</div>';
}

function renderDom(realDom, virtualDom) {
  realDom.innerHTML = virtualDom;
}

function render({ state }) {
  renderDom(document.getElementById('root'), App({ state }));
}

render({ state });

setInterval(() => {
  state = {
    ...state,
    time: new Date(),
  };
  render({ state });
});
Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Yandex
MailRu
GitHub
Google