Сравнение с Vue/React

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

На этой странице мы попытаемся выделить часть этих различий. Как вы понимаете, было приложено много усилий, чтобы оставаться объективным. Однако, если вы не согласны с некоторыми из обсуждаемых пунктов, не стесняйтесь открывать issue/submit PR, чтобы исправить этот текст.

Содержание

Размер

OWL должен быть небольшим и работать на несколько более низком уровне абстракции, чем React и Vue. Кроме того, jQuery - не совсем аналогичный фреймворк, но его интересно сравнить.

ФреймворкРазмер (minified, gzipped)
OWL18kb
Vue + VueX30kb
Vue + VueX + Vue Router39kb
React + ReactDOM + Redux40kb
jQuery30kb

Обратите внимание, что эти сравнения не совсем корректны, так как мы не сравниваем один и тот же набор функций. Например, Vue и Vue Router поддерживают более продвинутые варианты использования.

Основан на использовании классов

И React, и Vue отошли от определения компонентов с помощью классов. Они предпочитают более функциональный подход, в частности, с новыми механизмами хуков.

Это имеет как свои преимущества так и недостатки. Но по факту и React, и Vue предлагают множество различных способов определения новых компонентов. В отличие от yb[], Owl имеет только один механизм: компоненты на основе классов. Мы считаем, что компоненты Owl достаточно быстры для всех наших сценариев использования, и сделать их максимально простыми для разработчиков было очень важно (для нас).

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

Скорее всего мир фреймворков пользовательского интерфейса движется в сторону композиции по многим очень веским причинам. Owl по-прежнему хорош в композиции (например, Owl поддерживает слоты, которые являются основным механизмом для создания универсальных компонентов многократного использования). Но он также может использовать наследование (и это очень важно, поскольку шаблоны также могут быть унаследованы с помощью преобразований xpaths).

Сборка и использования различных инструментов

OWL разработан таким образом, чтобы быть простым и спользоваться автономно. По разным причинам Odoo не хочет полагаться на стандартные веб-инструменты (такие как webpack), и OWL можно использовать, просто добавив тег script на страницу.

<script src="owl.min.js" />

Для сравнения, React поощряет использование JSX, что требует этапа сборки, а большинство приложений Vue используют однофайловые компоненты, что также требует этапа сборки.

С другой стороны, дополнительный инструментарий в некоторых случаях может усложнить его использование, хотя он также приносит много преимуществ. К тому же у React/Vue есть большая экосистема.

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

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

Использование шаблонов

OWL uses its own QWeb engine, which compiles templates on the frontend, as they are needed. This is extremely convenient for our use case, in particular because templates are described in XML files, and can be modified by XPaths. Since Odoo is at its heart a modular application, this is an important feature for us.

OWL использует свой собственный QWeb-движок, который компилирует шаблоны на фронтэнде по мере необходимости. Это чрезвычайно удобно для нашего сценария использования, в частности, потому, что шаблоны описаны в XML-файлах и могут быть изменены с помощью XPaths. Поскольку Odoo по своей сути является модульным приложением то для нас очень это важная.

<div>
  <button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
</div>

Vue на самом деле отчасти похож. Его язык шаблонов отчасти близок к QWeb, с заменой v на t. Тем не менее, он также является более функциональным. Например, шаблоны Vue имеют слоты или модификаторы событий. Большая разница заключается в том, что большинство приложений Vue необходимо будет создавать заранее, чтобы скомпилировать шаблоны в функции javascript. Обратите внимание, что Vue имеет отдельную сборку, которая включает в себя компилятор шаблонов.

В отличие от этого, большинство приложений React не используют язык шаблонов, а пишут код JSX, который предварительно компилируется в обычный JavaScript на этапе сборки. Этот пример выполнен с использованием (отчасти устаревшей) системы классов React:

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Преимущества такого подхода заключается в том, что он обладает всей мощью Javascript, но менее структурирован, чем язык шаблонов. Обратите внимание, что инструментарий довольно впечатляющий: здесь, на github, есть подсветка синтаксиса для jsx!

Для сравнения ниже приведен эквивалентный Owl компонент, написанный с помощью тега xml

class Clock extends Component {
  static template = xml`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {props.date.toLocaleTimeString()}.</h2>
      </div>
    `;
}

Асинхронный рендеринг

На самом деле есть разница между OWL и React/Vue: компоненты в OWL полностью асинхронны. В их жизненном цикле есть два асинхронных хука:

  • willStart (перед началом рендеринга компонента)
  • willUpdateProps (до установки новых пропсов)

Оба эти метода могут быть реализованы и возвращают объект promise. Затем рендеринг будет ждать завершения этих promise, прежде чем исправлять DOM. Это полезно в таких случаях использования: например, компонент может захотеть получить внешнюю библиотеку (компоненту календаря может потребоваться специализированная библиотека визуализации календаря)., в его willStart хуке.

class MyCalendarComponent extends owl.Component {
    ...

    willStart() {
        return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
    }
    ...
}

Такой подход содержит в себе ряд опасностей (останавливка рендеринга в ожидании ответа по сети очень плохая идея), но с другой стороны он обладает чрезвычайной мощностью, что и продемонстрировал веб-клиент Odoo.

Ленивая загрузка статических библиотек, вполне, может быть выполнена с помощью React/Vue, но это но будет раелизовано более запутанно. Например, в Vue вам нужно использовать ключевое слово dynamic import, которое необходимо транспилировать во время сборки, чтобы компонент загружался асинхронно (см. документацию).

Реактивность

У React есть простая модель: всякий раз, когда состояние изменяется, оно заменяется новым состоянием (с помощью метода setState). Затем DOM исправляется. Это просто, эффективно и немного неудобно писать.

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

Owl ближе к vue: он также волшебным образом отслеживает свойства состояния, но увеличивает внутренний счетчик только при каждом его изменении. Обратите внимание, что это сделано с Proxy, что означает, что он полностью прозрачен для разработчиков. Поддерживается добавление новых ключей. Как только какая-либо часть состояния была изменена, рендеринг ставится на исполнение в следующем тике микрозадачи (очередь объектов promise).

Управление состоянием

Управление состоянием приложения - сложная проблема. За последние несколько лет было предложено много решений. Все зависит от вида приложения, о котором мы говорим. Небольшому приложению может оказаться достаточно обычного объекта чтобы хранить его состояние.

Однако есть несколько общих решений для React и Vue: redux и vuex. Оба они являются централизованным хранилищем, которому принадлежит состояние, и они диктуют, как состояние может быть изменено.

Redux

В Redux состояние видоизменяется редьюсерами. Редьюсеры - это функции, которые изменяют состояние, возвращая другой объект:

  ...
  switch (action.type) {
    case ADD_TODO: {
      const { id, content } = action.payload;
      return {
        ...state,
        allIds: [...state.allIds, id],
        byIds: {
          ...state.byIds,
          [id]: {
            content,
            completed: false
          }
        }
      };
    }

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

VueX

VueX основан на другом принципе: состояние видоизменяется с помощью некоторых специальных функций (мутаций), которые изменяют состояние на месте:

    function ({state}, payload) {
        const { id, content } = payload;
        const message = {id, content, completed: false};
        state.messages.push(message)
    }

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

Owl

Owl store немного похож на смесь redux и vuex: у него есть действия (но не мутации), и, как и VueX, он отслеживает изменения состояния. Однако он не уведомляет компонент об изменении состояния. Вместо этого компоненты должны подключаться к хранилищу, как в redux, с помощью хука useStore (см. документацию store).

const actions = {
  increment({ state }, val) {
    state.counter.value += val;
  },
};

const state = {
  counter: { value: 0 },
};
const store = new owl.Store({ state, actions });

class Counter extends Component {
  static template = xml`
      <button t-name="Counter" t-on-click="dispatch('increment')">
        Click Me! [<t t-esc="counter.value"/>]
      </button>`;
  counter = useStore((state) => state.counter);
  dispatch = useDispatch();
}

Counter.env.store = store;
const counter = new Counter();

Хуки

Хуки недавно захватили мир React. Они решают множество, казалось бы, несвязанных проблем: присоединяют повторно используемое поведение к компоненту составным способом, извлекают логику с отслеживанием состояния из компонента или повторно используют логику с отслеживанием состояния между компонентами, не изменяя иерархию компонентов.

Вот пример хука React useState:

import React, { useState } from "react";

function Example() {
  // Объявляем новую переменную состояния, которую мы назовем "count".
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

Из-за того, как React разработал API хуков, они работают только для функциональных компонентов. Но в таком случае они действительно очень мощные. Каждая крупная библиотека React находится в процессе редизайна своего API с помощью хуков (например, Redux).

В Vue 2 нет хуков, но проект Vue работает над своей следующей версией, в которой будет представлен новый composition API. Эта работа основана на новых идеях, представленных React hooks.

Судя по тому, как React и Vue представляют свои хуки, может показаться, что хуки не совместимы с компонентами класса. Однако это не так, как показывают Owl хуки. Они вдохновлены как React, так и Vue. Например, хук useState назван в честь React, но его API ближе к reactive Vue-хук.

Вот как выглядит приведенный выше пример Counter в Owl:

import { Component, Owl } from "owl";
import { xml } from "owl/tags";

class Example extends Component {
  static template = xml`
      <div>
        <p>You clicked {count.value} times</p>
        <button t-on-click="increment">Click me</button>
      </div>`;

  count = useState({ value: 0 });

  increment() {
    this.state.value++;
  }
}

Поскольку фреймворк Owl имел хуки с самого начала, его основные API предназначены для взаимодействия с хуками с самого начала. Например, абстракции Context и Store.