2 недели назад История
README.md


EN / RU

Реактивные Веб-компоненты


reacton

GitHub | GitFlic | GitVerse | NpmJS | Скачать⤵️


Reacton (сокр. Rtn) – это фреймворк JavaScript для быстрого создания реактивных Веб-компонентов. Он поддерживает все методы и свойства, которые предоставляются стандартными Веб-компонентами. Кроме этого, фреймворк содержит ряд дополнительных методов и реализует рендеринг Веб-компонентов на стороне сервера.


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

class WHello {
  // инициализация свойств объекта состояния
  message = 'Reacton'
  color = 'orangered'

  static mode = 'open' // добавить Теневой DOM

  // вернуть HTML-разметку компонента
  static template = `
    <h1>Привет, {{ message }}!</h1>
    
    <style>
      h1 {
        color: {{ color }};
      }
    </style>
  `
}


  1. Быстрый старт
  2. Состояние компонента
  3. Циклы
  4. Примеси
  5. Виды
  6. Реактивные свойства
  7. Статические свойства
  8. Специальные методы
  9. Эмиттер событий
  10. Маршрутизатор
  11. Серверный рендеринг




Быстрый старт


Для создания компонентов применяются классы. Классы могут быть как встроенными в основной скрипт, так и импортированы из внешнего модуля. Создайте новый рабочий каталог, например, с названием app, и скачайте в этот каталог файл rtn.global.js.

Добавьте в каталог файл index.html со следующим содержимым:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // инициализация свойств объекта состояния
      message = 'Reacton'
      color = 'orangered'

      static mode = 'open' // добавить Теневой DOM

      // вернуть HTML-разметку компонента
      static template = `
        <h1>Привет, {{ message }}!</h1>
        
        <style>
          h1 {
            color: {{ color }};
          }
        </style>
      `
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>
</html>

Чтобы гарантировать отсутствие конфликтов имён между стандартными и пользовательскими HTML-элементами, имя компонента должно содержать дефис «-», например, “my-element” и “super-button” – это правильные имена, а “myelement” – нет.

В большинстве примеров этого руководства, префикс будет состоять из одной буквы «w-». т.е. компонент Hello будет называться “w-hello”.

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

Открыв файл index.html в браузере, на экране отобразится созданное в компоненте Hello сообщение:

Привет, Reacton!


Компоненты можно выносить в отдельные модули. В этом случае, файл компонента Hello выглядел бы как показано ниже:

export default class WHello {
  // инициализация свойств объекта состояния
  message = 'Reacton'
  color = 'orangered'

  static mode = 'open' // добавить Теневой DOM

  // вернуть HTML-разметку компонента
  static template = `
    <h1>Привет, {{ message }}!</h1>
    
    <style>
      h1 {
        color: {{ color }};
      }
    </style>
  `
}

Для работы с внешними компонентами, вам потребуется любой разработочный сервер, такой, например, как lite-server.

Установить данный сервер можно с помощью команды в терминале:

npm install --global lite-server

Запуск сервера из каталога, в котором находится приложение, осуществляется с помощью команды в терминале:

lite-server


Кроме этого, фреймворк поддерживает однофайловые компоненты, которые могут быть использованы наравне с модульными, при создании проекта в системе сборки webpack.

Ниже показан пример простого однофайлового компонента:

<h1>Привет, {{ message }}!</h1>
      
<style>
  h1 {
    color: {{ color }};
  }
</style>

<script>
  exports = class WHello {
    // инициализация свойств объекта состояния
    message = 'Reacton'
    color = 'orangered'

    static mode = 'open' // добавить Теневой DOM
  }
</script>

Однофайловый компонент должен присваивать свой класс переменной exports. Эта переменная будет автоматически объявлена во время создания структуры компонента в системе сборки проекта.

В однофайловых компонентах можно использовать инструкцию import, например:

<script>
  // импортировать из модуля объект по умолчанию
  import obj from './module.js'

  exports = class WHello {
    // инициализация свойств объекта состояния
    message = obj.message
    color = obj.color

    static mode = 'open' // добавить Теневой DOM
  }
</script>


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

Чтобы иметь возможность работать в браузере с компонентами, в которых логика отделена от HTML-содержимого, существуют встроенные компоненты.

Ниже показан пример простого встроенного компонента:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <!-- определить шаблон компонента Hello -->
  <template id="tempHello">
    <h1>Привет, {{ message }}!</h1>
        
    <style>
      h1 {
        color: {{ color }};
      }
    </style>

    <script>
      return class WHello {
        // инициализация свойств объекта состояния
        message = 'Reacton'
        color = 'orangered'

        static mode = 'open' // добавить Теневой DOM
      }
    </script>
  </template>

  <script src="rtn.global.js"></script>

  <script>
    // передать шаблон компонента Hello функции Rtn
    Rtn(tempHello)
  </script>
</body>
</html>

Встроенный компонент должен возвращать свой класс, а содержимое его тега <script> можно рассматривать как функцию. Однако, встроенные компоненты не подходят для рендеринга на стороне сервера и, кроме этого, в них нельзя использовать инструкцию import, но допускается использование выражения import(), например:

<script>
  // импортировать модуль и сохранить его объект в переменной
  let obj = await import('./module.js')

  return class WHello {
    // инициализация свойств объекта состояния
    message = obj.message
    color = obj.color

    static mode = 'open' // добавить Теневой DOM
  }
</script>


Для быстрого доступа к компоненту, достаточно добавить идентификатор к элементу, который подключает компонент к документу, как показано ниже:

<!-- подключить компонент Hello к документу -->
<w-hello id="hello"></w-hello>

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

hello.$state.message = 'Веб-компоненты'
hello.$state.color = 'blue'

Цвет и содержимое заголовка изменятся:

Привет, Веб-компоненты!



Состояние компонента


Каждый компонент может содержать изменяющиеся данные, которые называются состоянием. Состояние можно определить в конструкторе класса компонента:

class WHello {
  constructor() {
    // инициализация свойств объекта состояния
    this.message = 'Reacton'
    this.color = 'orangered'
  }
  ...
}

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

class WHello {
  // инициализация свойств объекта состояния
  message = 'Reacton'
  color = 'orangered'
  ...
}


Методы компонента не являются состоянием. Они предназначены для выполнения действий с состоянием компонента и хранятся в прототипе объекта состояния:

class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  // определить метод объекта состояния
  printStr(str) {
    return this.message
  }

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ printStr() }}!</h1>`
}


Для доступа к объекту состояния, применяется специальное свойство $state. С помощью этого свойства, можно получить или присвоить новое значение состоянию, как показано ниже:

hello.$state.message = 'Веб-компоненты'

Обновление содержимого компонента на основе нового состояния происходит автоматически.


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

В примере ниже, обработчик элемента <h1> будет работать и после обновления состояния компонента. Поскольку обновление изменит только старое значение его атрибута и текстового содержимого:

class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  /* этот метод выполняется после подключения компонента к документу
    когда для компонента уже создан DOM, из которого можно выбирать элементы */
  static connected() {
    this.$('h1').addEventListener('click', e => console.log(e.target))
  }

  // вернуть HTML-разметку компонента
  static template = `<h1 :title="message">Привет, {{ message }}!</h1>`
}



Циклы


Reacton поддерживает три вида циклов «for», которые реализованы в JavaScript. Все они определяются с помощью специального атрибута $for и выводят содержимое своих HTML-элементов столько раз, сколько предусмотрено условием цикла.

В скомпилированном компоненте, данный атрибут отображаться не будет.

В примере ниже, цикл «for» выводит 10 параграфов с числами от 0 до 9:

class WHello {
  // вернуть HTML-разметку компонента
  static template = `
    <div $for="i = 0; i < 10; i++">
      <p>Число: {{ i }}</p>
    </div>
  `
}

В специальном атрибуте $for нельзя использовать операторы определения переменных: var, let и const соответственно. Это приведёт к ошибке:

static template = `
  <div $for="let i = 0; i < 10; i++">
    <p>Число: {{ i }}</p>
  </div>
`


Цикл «for-in» используется для вывода содержимого объектов, как показано ниже:

class WHello {
  // инициализация свойства объекта состояния
  user = {
    name: 'Иван',
    age: 32
  }

  // вернуть HTML-разметку компонента
  static template = `
    <ul $for="prop in user">
      <li>
        <b>{{ prop }}</b>: {{ user[prop] }}
      </li>
    </ul>
  `
}


Цикл «for-of» предназначен для работы с массивами:

class WHello {
  // инициализация свойства объекта состояния
  colors = ['красный', 'зелёный', 'синий']

  // вернуть HTML-разметку компонента
  static template = `
    <ul $for="col of colors">
      <li>{{ col }}</li>
    </ul>
  `
}


При использовании событий в циклах с помощью специального атрибута @event, они будут использовать актуальное значение переменной цикла для своей фазы итерации:

static template = `
  <ul $for="col of colors">
    <li @click="console.log(col)">{{ col }}</li>
  </ul>
`

Подробнее об этих событиях и других специальных атрибутах, будет рассказано далее в руководстве.


Можно применять циклы с любой глубиной вложенности:

class WHello {
  // инициализация свойства объекта состояния
  users = [
    {
      name: 'Иван',
      age: 32,
      skills: {
        frontend: ['HTML', 'CSS'],
        backend: ['Ruby', 'PHP', 'MySQL']
      }
    },
    {
      name: 'Ольга',
      age: 25,
      skills: {
        frontend: ['HTML', 'JavaScript'],
        backend: ['PHP']
      }
    },
    {
      name: 'Максим',
      age: 30,
      skills: {
        frontend: ['HTML', 'CSS', 'JavaScript', 'jQuery'],
        backend: ['Ruby', 'MySQL']
      }
    }
  ]

  // вернуть HTML-разметку компонента
  static template = `
    <div $for="user of users">
      <div>
        <p>
          <b>Имя</b>: {{ user.name }}
        </p>
        <p>
          <b>Возраст</b>: {{ user.age }}
        </p>
        <div $for="category in user.skills">
          <b>{{ category[0].toUpperCase() + category.slice(1) }}</b>:
          <ol $for="item of user.skills[category]">
            <li>{{ item }}</li>
          </ol>
        </div>
      </div>
      <hr>
    </div>
  `
}



Примеси


Примесь – общий термин в объектно-ориентированном программировании: класс, который содержит в себе методы для других классов. Эти методы могут использовать разные компоненты, что позволяет не создавать методы с одинаковым функционалом для каждого компонента отдельно.

В примере ниже, метод printName() из примеси используют компоненты Hello и Goodbye:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <!-- подключить компонент Goodbye к документу -->
  <w-goodbye></w-goodbye>

  <script src="rtn.global.js"></script>

  <script>
    // определить класс Mixin для общих методов
    class Mixin {
      printName() {
        return this.userName
      }
    }

    // расширить класс компонента Hello от класса Mixin
    class WHello extends Mixin {
      // инициализация свойства объекта состояния
      userName = 'Анна'

      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, {{ printName() }}!</h1>`
    }

    // расширить класс компонента Goodbye от класса Mixin
    class WGoodbye extends Mixin {
      // инициализация свойства объекта состояния
      userName = 'Иван'

      // вернуть HTML-разметку компонента
      static template = `<p>До свидания, {{ printName() }}...</p>`
    }
    
    // передать классы компонентов Hello и Goodbye функции Rtn
    Rtn(WHello, WGoodbye)
  </script>
</body>
</html>



Виды


Для отображения различных компонентов, используется специальный атрибут $view. Этот атрибут может быть назначен любому элементу, но обычно используется элемент DIV. Содержащий атрибут элемент заменяется на компонент, название которого содержится в значении этого атрибута, например:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- подключить компонент WContent к документу -->
  <w-content></w-content>

  <script src="rtn.global.js"></script>

  <script>
    class WContent {
      // инициализация свойства объекта состояния
      compName = 'w-hello'

      // определить метод объекта состояния
      changeView() {
        this.compName = this.compName === 'w-hello' ? 'w-goodbye' : 'w-hello'
      }
      
      // вернуть HTML-разметку компонента
      static template = `
        <div $view="compName"></div>
        <button @click="changeView">Переключить</button>
      `
    }

    class WHello {
      // инициализация свойства объекта состояния
      userName = 'Анна'

      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, {{ userName }}!</h1>`
    }

    class WGoodbye {
      // инициализация свойства объекта состояния
      userName = 'Иван'

      // вернуть HTML-разметку компонента
      static template = `<p>До свидания, {{ userName }}...</p>`
    }
    
    // передать классы компонентов функции Rtn
    Rtn(WContent, WHello, WGoodbye)
  </script>
</body>
</html>

Атрибут $view нельзя использовать вместе с циклами. Пример ниже приведёт к ошибке:

static template = `
  <div $view="compName" $for="i = 0; i < 10; i++">
    <p>Число: {{ i }}</p>
  </div>
`



Реактивные свойства


Все используемые свойства объекта состояния в компоненте являются реактивными. Это означает, что при изменении их значения, изменяются и значения во всех местах HTML-разметки компонента, где эти свойства используются.

Для вставки реактивных свойств в текстовые узлы, применяются двойные фигурные скобки:

static template = `
  <h1>Привет, {{ message }}!</h1>
  
  <style>
    h1 {
      color: {{ color }};
    }
  </style>
`

Чтобы вставить реактивное свойство в атрибут, перед его именем необходимо поставить символ двоеточия:

static template = `<h1 :title="message">Привет, Reacton!</h1>`

В примере ниже, реактивное свойство добавляется логическому атрибуту:

class WHello {
  // инициализация свойства объекта состояния
  hide = true

  // вернуть HTML-разметку компонента
  static template = `<h1 :hidden="hide">Привет, Reacton!</h1>`
}

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


Для атрибутов событий, перед названием атрибута указывается символ ***@***, за которым следует название события без префика on, как показано ниже:

class WHello {
  // инициализация свойства объекта состояния
  hide = true

  // вернуть HTML-разметку компонента
  static template = `
    <h1 :hidden="hide">Привет, Reacton!</h1>
    <button @click="hide = !hide">Скрыть/Показать</button>
  `
}

Вместо непосредственного изменения реактивного свойства в атрибуте события, в него можно передать название метода, который изменяет реактивное свойство, например:

class WHello {
  // инициализация свойства объекта состояния
  hide = true

  // определить метод объекта состояния
  changeHide() {
    this.hide = !this.hide
  }

  // вернуть HTML-разметку компонента
  static template = `
    <h1 :hidden="hide">Привет, Reacton!</h1>
    <button @click="changeHide">Скрыть/Показать</button>
  `
}


В атрибутах событий доступен объект event, с помощью свойства target которого, можно получить ссылку на элемент, на котором произошло событие:

static template = `<button @click="console.log(event.target)">Показать в консоли</button>`

Атрибуты событий могут иметь такие же параметры, что передаются в третьем аргументе методу addEventListener. Эти параметры указываются через точку, после названия события:

@click.once.capture.passive

В примере ниже, вызывающий событие элемент будет показан в консоли только один раз:

static template = `<button @click.once="console.log(event.target)">Показать в консоли</button>`



Статические свойства


alias – это статическое свойство позволяет добавить псевдоним для ключевого слова this, т.е. для контекста объекта состояния:

class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  static alias = 'o' // добавить псевдоним для "this"

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ o.message }}!</h1>`
}

По умолчанию, нет необходимости добавлять ключевое слово this перед названиями свойств объекта состояния. Однако, если псевдоним добавлен, то перед именами свойств и методов, необходимо использовать либо псевдоним, либо это ключевое слово.


time – это статическое свойство позволяет добавить таймер обновления компонента, если задать ему значение “true”, как показано ниже:

class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  static time = true // добавить таймер обновления

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
}

Время обновления компонента в миллисекундах отображается в консоли, после каждого изменения любого свойства объекта состояния.


name – это статическое свойство используется, например, когда функции Rtn передаётся анонимный класс, как показано ниже:

// передать анонимный класс компонента Hello функции Rtn
Rtn(class {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  static name = 'w-hello' // название компонента

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
})


mode – это статическое свойство отвечает за добавление компоненту Теневого DOM. Оно может содержать два значения: “open” или “closed”. В последнем случае, когда компонент является закрытым, то невозможно получить доступ из консоли к свойствам его объекта состояния, методам выборки элементов и обновления содержимого.

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

class WHello {
  static mode = 'closed' // добавить закрытый Теневой DOM

  // выполняется в конце подключения компонента к документу
  static connected() {
    // получить элемент с помощью метода выборки
    const elem = this.$('h1')

    // добавить элементу обработчик события
    elem.addEventListener('click', e => console.log(e.target))
  }

  // инициализация свойства объекта состояния
  message = 'Reacton'

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
}

Только компоненты имеющие Теневой DOM, могут содержать локальные стили.


extends – это статическое свойство отвечает за создание модифицированных компонентов, т.е. таких, которые встраиваются в стандартные HTML-элементы, например:

<body>
  <!-- встроить компонент Hello в элемент header -->
  <header is="w-hello"></header>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // инициализация свойства объекта состояния
      message = 'Reacton'

      static extends = 'header' // название встраиваемого элемента

      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, {{ message }}!</h1>`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>

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


serializable – это статическое свойство отвечает за сериализацию Теневого DOM компонента с помощью метода getHTML(). По умолчанию имеет значение “false”.


template() – это статическое свойство возвращает будущее HTML-содержимое компонента в виде строки:

// вернуть HTML-разметку компонента
static template = `
  <h1>Привет, {{ message }}!</h1>
  
  <style>
    h1 {
      color: {{ color }};
    }
  </style>
`


startConnect() – этот статический метод выполняется в самом начале подключения компонента к документу, до формирования HTML-содержимого компонента и вызова статического метода connected(), но после создания объекта состояния компонента.

В нём можно инициализировать свойства объекта состояния имеющимися значениями:

class WHello {
  // выполняется в начале подключения компонента к документу
  static startConnect() {
    // инициализация свойства объекта состояния
    this.message = 'Reacton'
  }

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
}

или получить данные с сервера для их инициализации. Но в этом случае, метод должен быть асинхронным:

class WHello {
  // выполняется в начале подключения компонента к документу
  static async startConnect() {
    // инициализация свойства объекта состояния данными с условного сервера
    this.message = await new Promise(ok => setTimeout(() => ok('Reacton'), 1000))
  }

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
}

Это единственный статический метод, который можеть быть асинхронным.


connected() – этот статический метод выполняется в самом конце подключения компонента к документу, после формирования HTML-содержимого компонента и вызова статического метода startConnect().

В нём можно добавлять обработчики событий внутренним элементам компонента:

class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  // выполняется в конце подключения компонента к документу
  static connected() {
    // получить элемент с помощью метода выборки
    const elem = this.$('h1')

    // добавить элементу обработчик события
    elem.addEventListener('click', e => console.log(e.target))
  }

  // вернуть HTML-разметку компонента
  static template = `<h1>Привет, {{ message }}!</h1>`
}

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


disconnected() – этот статический метод выполняется при удалении компонента из документа.

adopted() – этот статический метод выполняется при перемещении компонента в новый документ.

changed() – этот статический метод выполняется при изменении одного из отслеживаемых атрибутов.

attributes – этот статический массив содержит имена отслеживаемых атрибутов, например:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello data-message="Reacton"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // выполняется в начале подключения компонента к документу
      static startConnect() {
        // инициализация свойства объекта состояния
        this.message = this.$data.message
      }
      
      // выполняется в конце подключения компонента к документу
      static connected() {
        // получить элемент с помощью метода выборки
        const elem = this.$('h1')

        // добавить элементу обработчик события
        elem.addEventListener('click', e => this.$data.message = 'Веб-компоненты')
      }

      // выполняется при изменении одного из отслеживаемых атрибутов
      static changed(name, oldValue, newValue) {
        // если новое значение атрибута не равно старому значению 
        if (newValue !== oldValue) {
          // изменить значение свойства объекта состояния
          this.message = newValue
        }
      }

      // содержит имена отслеживаемых атрибутов
      static attributes = ['data-message']

      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, {{ message }}!</h1>`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>


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

В примере ниже, свойства id не существует в объекте состояния компонента. Поэтому оно запрашивается из самого компонента:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, компонент с ID {{ id }}!</h1>`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>



Специальные методы


Все специальные методы и свойства начинаются с символа доллара «$», за которым следует название метода или свойства.

$() – этот специальный метод выбирает элемент из содержимого компонента по указаному селектору, например, для добавления элементу обработчика события:

// выполняется в конце подключения компонента к документу
static connected() {
  // получить элемент с помощью метода выборки
  const elem = this.$('h1')

  // добавить элементу обработчик события
  elem.addEventListener('click', e => console.log(e.target))
}

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


$$() – этот специальный метод выбирает все элементы из содержимого компонента по указаному селектору, например, для добавления элементам обработчиков событий при переборе их в цикле:

// выполняется в конце подключения компонента к документу
static connected() {
  // получить все элементы с помощью метода выборки
  const elems = this.$$('h1')

  // перебрать коллекцию элементов в цикле
  for (const elem of elems) {
    // добавить элементу обработчик события
    elem.addEventListener('click', e => console.log(e.target))
  }
}

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


$entities() – этот специальный метод обезвреживает строку, содержащую HTML-содержимое полученное из недостоверных источников. По умолчанию, экранируется символ амперсанда «&», символы меньше «<» и больше «>», двойные «"» и одинарные кавычки «'», например:

class WHello {
  // выполняется в начале подключения компонента к документу
  static async startConnect() {
    // получение HTML-содержимого с условного сервера
    const html = await new Promise(ok => setTimeout(() => ok('<script>опасный код<\/script>'), 1000))

    // инициализация свойства объекта состояния обезвреженным HTML-содержимым
    this.message = this.$entities(html)
  }

  // вернуть HTML-разметку компонента
  static template = `{{ message }}`
}

Кроме вышеперечисленных символов, можно экранировать любые символы, передав во втором и последующих аргументах массив вида: [регулярное выражение, строка замены], например:

this.$entities(html, [/\(/g, '&lpar;'], [/\)/g, '&rpar;'])

Этот метод доступен в качестве свойства функции Rtn, как показано ниже:

Rtn.entities(html)

или именованного импорта, когда используется модульная версия фреймворка:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <script type="module">
    import Rtn, { Entities } from "./rtn.esm.js"

    class WHello {
      // вернуть HTML-разметку компонента
      static template = `${ Entities('<script>опасный код<\/script>') }`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>


Специальные методы: $event(), $router() и $render(), будут рассмотрены в следующих разделах. Как и для метода $entities(), для них так же имеются свои именованные импорты:

import Rtn, { Tag, Event, Router, Render } from "./rtn.esm.js"

Функция Rtn всегда импортируется по умолчанию.


$state – это специальное свойство ссылается на прокси объекта состояния компонента. Это означает, что если необходимое свойство не обнаруживается в объекте состояния, то поиск происходит в самом компоненте.

В примере ниже, свойства id не существует в объекте состояния компонента. Поэтому оно запрашивается из самого компонента:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, компонент с ID {{ id }}!</h1>`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>


$host – это специальное свойство ссылается на элемент, который подключает компонент к документу, т.е. на элемент компонента. Это может пригодиться, если свойства с одинаковыми именами имеются и объекте состояния и в компоненте.

Прокси объекта состояния изначально ищет свойство в самом объекте состояния, это значит, что для получения одноимённого свойства из элемента компонента, необходимо использовать специальное свойство $host, как показано ниже:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello id="hello"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // инициализация свойства объекта состояния
      id = 'Reacton'

      // вернуть HTML-разметку компонента
      static template = `
        <h1>Привет, свойство ID со значением {{ id }}!</h1>
        <h2>Привет, компонент с ID {{ $host.id }}!</h2>
      `
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>


$shadow – это специальное свойство ссылается на Теневой DOM компонента:

hello.$shadow

Для закрытых компонентов и компонентов без Теневого DOM, это свойство возвращает значение “null”.


$data – это специальное свойство ссылается на объект dataset компонента, который используется для доступа к пользовательским атрибутам, например:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello data-message="Reacton"></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // вернуть HTML-разметку компонента
      static template = `<h1>Привет, {{ $data.message }}!</h1>`
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)
  </script>
</body>


$props – это специальное свойство ссылается на объект состояния родительского компонента, когда в дочерний компонент передаётся специальный атрибут $props без значения:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <script src="rtn.global.js"></script>

  <script>
    // родительский компонент Hello
    class WHello {
      // инициализация свойств объекта состояния
      message = 'Reacton'
      color = 'orangered'

      // вернуть HTML-разметку компонента
      static template = `<w-inner $props></w-inner>`
    }

    // дочерний компонент Inner
    class WInner {
      static mode = 'open' // добавить Теневой DOM

      // вернуть HTML-разметку компонента
      static template = `
        <h1>Привет, {{ $props.message }}!</h1>
        
        <style>
          h1 {
            color: {{ $props.color }};
          }
        </style>
      `
    }

    // передать классы компонентов Hello и Inner функции Rtn
    Rtn(WHello, WInner)
  </script>
</body>

Специальный атрибут $props здесь указывается без какого-либо значения:

// вернуть HTML-разметку компонента
static template = `<w-inner $props></w-inner>`

Для доступа к свойствам объекта состояния родительского компонента в HTML-разметке дочернего, используется специальное свойство $props, как показано ниже:

// вернуть HTML-разметку компонента
static template = `
  <h1>Привет, {{ $props.message }}!</h1>
  
  <style>
    h1 {
      color: {{ $props.color }};
    }
  </style>
`

Если необходимо передать из родительского компонента только некоторые свойства, а не весь объект состояния целиком, то специальный атрибут $props должен содержать значение, в котором через запятую указываются названия передаваемых свойств:

// вернуть HTML-разметку компонента
static template = `<w-inner $props="message, color"></w-inner>`

Можно изменять состояние внешних компонентов через специальное свойство $props внутренних, но не наоборот. Поскольку здесь используется односторонняя связь между компонентами:

// родительский компонент Hello
class WHello {
  // инициализация свойства объекта состояния
  message = 'Reacton'

  // вернуть HTML-разметку компонента
  static template = `
    <h1>Привет, {{ message }}!</h1>
    <w-inner $props="message"></w-inner>
  `
}

// дочерний компонент Inner
class WInner {
  // вернуть HTML-разметку компонента
  static template = `<button @click="$props.message='Веб-компоненты'">Изменить</button>`
}

Чтобы из одних компонентов, изменять данные в любых других компонентах, используются пользовательские события, которые будут рассмотрены далее.



Эмиттер событий


Чтобы компоненты могли взаимодействовать между собой и обмениваться друг с другом данными, применяются пользовательские события. Для создания пользовательских событий, используется специальный метод $event(), который доступен в качестве свойства функции Rtn.

Если метод вызывается как конструктор, то он возвращает новый объект эмиттера, который будет генерировать и отслеживать пользовательские события, например:

const emit = new Rtn.event()

В роли эмиттера выступает обычный фрагмент документа. Можно создавать сколько угодно новых эмиттеров, и каждый эмиттер может генерировать и отслеживать сколько угодно новых пользовательских событий.

Когда метод $event() вызывается как обычная функция, то в первом аргументе он получает эмиттер, во втором передаётся название пользовательского события, а в третьем аргументе можно передавать любые данные:

this.$event(emit, 'new-array', ['Оранжевый', 'Фиолетовый'])

Эти данные затем будут доступны в обработчике пользовательского события, в качестве свойства detail объекта события Event, как показано ниже:

emit.addEventListener('new-array', event => {
  this.rgb = event.detail
})

В системе сборки webpack, эмиттер можно экспортировать из отдельного модуля, например, из файла Events.js:

import { Event } from 'reacton-js'
export const Emit = new Event()

для последующего импорта эмиттера в файлы компонентов, которые будут его использовать:

import { Emit } from './Events'


В примере ниже, каждой кнопке из компонента Hello добавляется обработчик события “click”, внутри которого, происходит запуск соответствующего пользовательского события объекта эмиттера.

Для отслеживания пользовательских событий, эмиттеру назначаются соответствующие обработчики в компоненте Colors. В последнем обработчике, через свойство detail объекта события Event, свойству состояния присваивается новый массив:

<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello></w-hello>

  <!-- подключить компонент Colors к документу -->
  <w-colors></w-colors>

  <script src="rtn.global.js"></script>

  <script>
    // создать новый объект эмиттера
    const emit = new Rtn.event()

    class WHello {
      // вернуть HTML-разметку компонента
      static template = `
        <button id="reverse">Обратить массив</button>
        <button id="new-array">Новый массив</button>
      `

      // выполняется в конце подключения компонента к документу
      static connected() {
        // добавить обработчик события кнопке "Обратить массив"
        this.$('#reverse').addEventListener('click', () => {
          // инициировать событие "reverse"
          this.$event(emit, 'reverse')
        })

        // добавить обработчик события кнопке "Новый массив"
        this.$('#new-array').addEventListener('click', () => {
          // инициировать событие "new-array"
          this.$event(emit, 'new-array', ['Оранжевый', 'Фиолетовый'])
        })
      }
    }

    class WColors {
      // инициализация свойства объекта состояния
      rgb = ['Красный', 'Зелёный', 'Синий']

      // вернуть HTML-разметку компонента
      static template = `
        <ul $for="col of rgb">
          <li>{{ col }}</li>
        </ul>
      `

      // выполняется в конце подключения компонента к документу
      static connected() {
        // добавить эмиттеру обработчик события "reverse"
        emit.addEventListener('reverse', () => {
          this.rgb.reverse() // обратить массив
        })

        // добавить эмиттеру обработчик события "new-array"
        emit.addEventListener('new-array', event => {
          this.rgb = event.detail // присвоить свойству новый массив
        })
      }
    }

    // передать классы компонентов Hello и Colors функции Rtn
    Rtn(WHello, WColors)
  </script>
</body>



Маршрутизатор


В основе маршрутизатора лежат пользовательские события. Для создания маршрутных событий, используется специальный метод $router(), который доступен в качестве свойства функции Rtn.

Если метод вызывается как конструктор, то он возвращает новый объект эмиттера с переопределённым методом addEventListener(), который будет генерировать и отслеживать маршрутные события, например:

const emitRouter = new Rtn.router()

Когда метод $router() вызывается как обычная функция, то в первом аргументе он получает эмиттер, во втором передаётся название маршрутного события, а в третьем аргументе можно передавать любые данные:

this.$router(emitRouter, '/about', ['Оранжевый', 'Фиолетовый'])

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

this.$router(emitRouter, event.target.href, ['Оранжевый', 'Фиолетовый'])


Передаваемые в последнем аргументе метода $router() пользовательские данные, будут доступны в обработчике маршрутного события, в качестве свойства detail объекта события Event, как показано ниже:

emitRouter.addEventListener('/about', event => {
  const arr = event.detail
  ...
})

Начальный слэш «/» в названии маршрутного события не является обязательным:

emitRouter.addEventListener('about', event => {
  const arr = event.detail
  ...
})

Вся остальная часть названия маршрутного события, кроме начального слэша, должна до конца совпадать со значением атрибута href ссылки, после нажатия на которую, сработает соответствующий этому значению обработчик:

<a href="/about">О нас</a>


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

emitRouter.addEventListener('/abou\\w', event => {
  ...
})

Чтобы не приходилось дважды использовать символ обратного слэша в обычной строке для экранирования специальных символов регулярных выражений, можно воспользоваться теговой функцией raw() встроенного объекта String, заключив название маршрутного события в шаблонную строку, например:

emitRouter.addEventListener(String.raw`/abou\w`, event => {
  ...
})

или так:

const raw = String.raw
emitRouter.addEventListener(raw`/abou\w`, event => {
  ...
})


Кроме свойства detail, объект события Event имеет дополнительное свойство params, для получения параметров маршрутов, как показано ниже:

emitRouter.addEventListener('/categories/:catId/products/:prodId', event => {
  const catId = event.params["catId"]
  const prodId = event.params["prodId"]
  ...
})

Этот обработчик будет выполняться для всех ссылок вида:

<a href="/categories/5/products/7">Продукт</a>

тогда catId будет иметь значение 5, а prodId значение 7.

Для поддержки параметров запросов, объект события Event имеет дополнительное свойство search, которое является короткой ссылкой на свойство searchParams встроенного класса URL, например:

const raw = String.raw
emitRouter.addEventListener(raw`/categories\?catId=\d&prodId=\d`, event => {
  const catId = event.search.get("catId")
  const prodId = event.search.get("prodId")
  ...
})

Этот обработчик будет выполняться для всех ссылок вида:

<a href="/categories?catId=5&prodId=7">Продукт</a>

тогда catId будет иметь значение 5, а prodId значение 7.

Последнее дополнительное свойство объекта события Event называется url, оно является объектом встроенного класса URL и помогает разобрать запрос на составляющие:

emitRouter.addEventListener('/about', event => {
  const hostname = event.url.hostname
  const origin = event.url.origin
  ...
})


Ниже показан пример создания простого маршрутизатора для трёх компонентов страниц:

<body>
  <!-- подключить компонент Menu к документу -->
  <w-menu></w-menu>

  <!-- подключить компонент Content к документу -->
  <w-content></w-content>

  <script src="rtn.global.js"></script>

  <script>
    class WHome {
      // вернуть HTML-разметку компонента
      static template = `<h1>Главная</h1>`
    }
    class WAbout {
      // вернуть HTML-разметку компонента
      static template = `<h1>О нас</h1>`
    }
    class WContacts {
      // вернуть HTML-разметку компонента
      static template = `<h1>Контакты</h1>`
    }

    // создать новый объект эмиттера для маршрутизатора
    const emitRouter = new Rtn.router()

    class WMenu {
      // выполняется в конце подключения компонента к документу
      static connected() {
        // добавить обработчик события "click" для элемента NAV
        this.$('nav').addEventListener('click', event => {
          event.preventDefault() // отменить действие по умолчанию
          // инициировать событие для значения "href" текущей ссылки
          this.$router(emitRouter, event.target.href)
        })
      }
    
      // вернуть HTML-разметку компонента
      static template = `
        <nav>
          <a href="/">Главная</a>
          <a href="/about">О нас</a>
          <a href="/contacts">Контакты</a>
        </nav>
      `
    }

    class WContent {
      // выполняется в начале подключения компонента к документу
      static startConnect() {
        // добавить эмиттеру обработчик события с необязательным параметром маршрута
        emitRouter.addEventListener(`(:page)?`, event => {
          // присвоить свойству название компонента страницы
          this.page = `w-${event.params.page || 'home'}` 
        })
        
        // инициировать событие для значения "href" текущей страницы
        this.$router(emitRouter, location.href)
      }

      // вернуть HTML-разметку компонента
      static template = `<div $view="page"></<div>`
    }

    // передать классы компонентов функции Rtn
    Rtn(WHome, WAbout, WContacts, WMenu, WContent)
  </script>
</body>

Для обработки маршрутов этих страниц, эмиттеру маршрутизатора назначается обработчик с необязательным параметром маршрута в компоненте Content:

// добавить эмиттеру обработчик события с необязательным параметром маршрута
emitRouter.addEventListener(`(:page)?`, event => {
  // присвоить свойству название компонента страницы
  this.page = `w-${event.params.page || 'home'}` 
})

Для того, чтобы этот обработчик срабатывал сразу при открытии приложения и подключал соответствующий маршруту компонент страницы, в конце статического метода connected(), инициируется событие для адреса текущего маршрута из свойства href объекта location:

// инициировать событие для значения "href" текущей страницы
this.$router(emitRouter, location.href)

Остальные компоненты страниц загружаются при клике по соответствующей им ссылке в компоненте Menu:

// добавить обработчик события "click" для элемента NAV
this.$('nav').addEventListener('click', event => {
  event.preventDefault() // отменить действие по умолчанию
  // инициировать событие для значения "href" текущей ссылки
  this.$router(emitRouter, event.target.href)
})



Серверный рендеринг


SSR (Server Side Rendering) – это методика разработки, при которой содержимое веб-страницы отрисовывается на сервере, а не в браузере клиента. Для рендеринга содержимого веб-страниц используется метод render(), который доступен в качестве свойства функции Rtn. Этот метод работает как на стороне сервера, так и в браузере клиента.

В примере ниже, данный метод выводит содержимое всей страницы на консоль браузера:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- подключить компонент Hello к документу -->
  <w-hello>
    <!-- передаваемое в слот HTML-содержимое -->
    <span>Реактивные Веб-компоненты</span>
  </w-hello>

  <script src="rtn.global.js"></script>

  <script>
    class WHello {
      // инициализация свойств объекта состояния
      message = 'Reacton'
      color = 'orangered'

      static mode = 'open' // добавить Теневой DOM

      // вернуть HTML-разметку компонента
      static template = `
        <h1>{{ message }} – это <slot></slot></h1>
        
        <style>
          h1 {
            color: {{ color }};
          }
        </style>
      `
    }

    // передать класс компонента Hello функции Rtn
    Rtn(WHello)

    // вывести HTML-содержимое страницы на консоль браузера
    Rtn.render().then(html => console.log(html))
  </script>
</body>
</html>

Также этот метод доступен в качестве именованного импорта, при использовании модульной версии фреймворка:

import { Render } from "./rtn.esm.js"

Метод возвращает промис, который разрешается после того, когда HTML-содержимое всех используемых компонентов для текущего маршрута приложения будет доступно:

Rtn.render().then(html => console.log(html))

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


Чтобы вывести в консоль браузера содержимое не всего документа, а только начиная с определённого элемента, необходимо передать в метод объект с параметром parent, значением которого будет элемент, с которого начинается вывод.

В примере ниже, содержимое документа выводится начиная с элемента BODY:

Rtn.render({ parent: document.body }).then(html => console.log(html))

По умолчанию, метод выводит очищенное HTML-содержимое документа, т.е. такое, в которых удалены теги: STYLE, SCRIPT и TEMPLATE. Чтобы метод выводил полное HTML-содержимое, необходимо передать ему объект с параметром clean и значением “false”, как показано ниже:

Rtn.render({ clean: false }).then(html => console.log(html))

Во всех примерах выше, передаваемое в слот содержимое выводилось без самих тегов SLOT. Чтобы передаваемое содержимое выводилось внутри этих тегов, т.е. в полном соответствии со структурой расположения данного содержимого в компоненте, методу необходимо передать объект с параметром slots и значением “true”, например:

Rtn.render({ slots: true }).then(html => console.log(html))

Все три параметра можно передавать одновременно:

Rtn.render({
  parent: document.body,
  clean: false,
  slots: true
}).then(html => console.log(html))


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

npm i

Для запуска приложения в режиме разработки, используется команда:

npm start

а для финальной сборки, со всеми минимизациями приложения в режиме продакшен, команда:

npm run build


Это обычный проект с использованием менеджера задач Gulp и сборщика модулей Webpack. Код сервера находится в файле app.js, а сам сервер написан с помощью фреймворка Express.

Файл сервера представляет собой типичное приложение на фреймворке Express:

const express = require('express')
const jsdom = require('jsdom')
const { JSDOM } = require('jsdom')
const fs = require('fs')
const port = process.env.PORT || 3000

// connect database file
let DB = JSON.parse(fs.readFileSync(__dirname + '/db.json').toString())

// create an Express application object
const app = express()

// create a parser for application/x-www-form-urlencoded data
const urlencodedParser = express.urlencoded({ extended: false })

// define directory for static files and ignore index.html file
app.use(express.static(__dirname + '/public', { index: false }))

// define an array of bot names that will receive the rendered content
const listBots = [
  'Yandex', 'YaDirectFetcher', 'Google', 'Yahoo', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'Lighthouse',
  'ia_archiver', 'Ask Jeeves', 'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'Ezooms', 'Tourlentabot', 'MJ12bot',
  'AhrefsBot', 'SearchBot', 'SiteStatus', 'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks',
  'proximic', 'OpenindexSpider', 'statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 'oBot', 'C-T bot', 'Updownerbot',
  'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot', 'StackRambler', 'msnbot'
]

// loads only scripts and ignores all other resources
class CustomResourceLoader extends jsdom.ResourceLoader {
  fetch(url, options) {
    return regJSFile.test(url) ? super.fetch(url, options) : null
  }
}

// define the bot agent string to test
const testAgent = process.argv[2] === 'test' ? 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' : ''

// define a regular expression to search for bot names in a string
const regBots = new RegExp(`(${listBots.join(')|(')})`, 'i')

// search for script file extensions
const regJSFile = /\.m?js$/

// favicon request
app.get('/favicon.ico', (req, res) => res.sendStatus(204))

// database request
app.get('/db', (req, res) => res.send(DB[req.query.page]))

// all other requests
app.use(async (req, res) => {
  // if the request comes from a bot
  if (regBots.test(testAgent || req.get('User-Agent'))) {
    // define a new JSDOM object with parameters
    const dom = await JSDOM.fromFile('index.html', {
      url: req.protocol + '://' + req.get('host') + req.originalUrl, // determine the full URL
      resources: new CustomResourceLoader(), // loading only scripts
      runScripts: 'dangerously' // allow page scripts to execute
    })

    // return the rendered HTML content of the page
    dom.window.onload = async () => res.send(await dom.window._$RtnRender_())
  }
  // otherwise, if the request comes from a user
  else {
    // return the main page file of the application
    res.sendFile(__dirname + '/index.html')
  }
})

// start the server
app.listen(port, () => console.log(`The server is running at http://localhost:${port}/`))


Чтобы метод render() мог работать на сервере, используется jsdom – это реализация Веб-стандартов на JavaScript.

Обычным пользователям не нужно отдавать отрендеренное содержимое страницы. Оно необходимо только поисковым ботам и другим автоматическим системам учёта анализа HTML-содержимого. Список этих систем находится в массиве, который можно пополнить дополнительными названиями:

// define an array of bot names that will receive the rendered content
const listBots = [
  'Yandex', 'YaDirectFetcher', 'Google', 'Yahoo', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'Lighthouse',
  'ia_archiver', 'Ask Jeeves', 'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'Ezooms', 'Tourlentabot', 'MJ12bot',
  'AhrefsBot', 'SearchBot', 'SiteStatus', 'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks',
  'proximic', 'OpenindexSpider', 'statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 'oBot', 'C-T bot', 'Updownerbot',
  'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot', 'StackRambler', 'msnbot'
]

Если в заголовке запроса будет присутствовать любое из этих названий, то сервер будет отдавать отрендеренное HTML-содержимое:

// if the request comes from a bot
if (regBots.test(testAgent || req.get('User-Agent'))) {
  // define a new JSDOM object with parameters
  const dom = await JSDOM.fromFile('index.html', {
    url: req.protocol + '://' + req.get('host') + req.originalUrl, // determine the full URL
    resources: new CustomResourceLoader(), // loading only scripts
    runScripts: 'dangerously' // allow page scripts to execute
  })

  // return the rendered HTML content of the page
  dom.window.onload = async () => res.send(await dom.window._$RtnRender_())
}

Для всех остальных запросов, сервер будет возвращать файл index.html, который является единственным html-файлом в этом одностраничном приложении:

// otherwise, if the request comes from a user
else {
  // return the main page file of the application
  res.sendFile(__dirname + '/index.html')
}


Рендеринг осуществляется с помощью функции _$RtnRender_() глобального объекта window, как показано ниже:

// return the rendered HTML content of the page
dom.window.onload = async () => res.send(await dom.window._$RtnRender_())

Эта функция назначается объекту в файле index.js, который является главным файлом всего приложения:

// add the Render method as a property of the window object
window._$RtnRender_ = Render


Рендеринг не поддерживает динамические импорты, вместо них необходимо использовать обычные инструкции импорта и экспорта модулей. Кроме этого, рендеринг не поддерживает глобальный метод fetch(). Вместо него необходимо использовать встроенный объект XMLHttpRequest.

Объект XMLHttpRequest можно обернуть в функцию и затем, вызывать эту функцию вместо того, чтобы каждый раз писать код запроса этого объекта вручную, как показано в файле helpers.js, например:

export const httpRequest = (url, method = 'GET', type = 'json') => {
  const xhr = new XMLHttpRequest()
  xhr.open(method, url)
  xhr.responseType = type
  xhr.send()
  return new Promise(ok => xhr.onload = () => ok(xhr.response))
}


После установки всех зависимостей приложения из файла package.json, для запуска сервера в обычном режиме, необходимо открыть консоль из каталога /server, и ввести в ней следующую команду:

node app

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

node app test



Описание

Реактивные Веб-компоненты

Конвейеры
0 успешных
0 с ошибкой