English Version
Vuecket — веб-фреймворк, интегрирующий VueJS с клиентской стороны и Apache Wicket с серверной. Он берет все самое лучшее от обоих и позволяет разрабатывать full-stack приложения еще быстрее и проще. Конечно, это всё громкие слова, ведь Vuecket'у на данный момент (Август 2020) меньше месяца, и он не прошел ещё крещение «огнем и кровью» продакшн серверов. Но он уже включил в себя всё самое лучшее из наработанного нами при разработке нашего ключевого Open Source продукта Orienteer(платформа для быстрой разработки бизнес приложений). И именно из-за молодого возраста Vuecket'у нужна ваша помощь: поделитесь, пожалуйста, тем, что вам понравилось, что не очень, где нужна доработка и т.д.

Основные принципы, которыми мы руководствуемся при построении Vuecket'а:

  1. Быть декларативным — не императивным. Vuecket не диктует какие-то специальные требования к коду. Он может быть приложен достаточно быстро и легко к уже существующим Vue.JS или Apache Wicket проектам.
  2. Следовать принципу Парето. Vuecket должен обеспечивать 80% нужного функционала из коробки, но для оставшихся 20% должны быть хорошие и удобные точки расширения.

Легко заметить, что эти принципы также применимы и к Vue.JS, и к Apache Wicket.

Итак, как именно мы будем знакомиться с Vuecket? Предлагаю сделать чат/гостевую доску с поддержкой Markdown. Не буду сильно томить: законченное приложение здесь, а код здесь.

Создаем проект


Сгенерируем наш проект через `mvn archetype:generate`. Для этого можете использовать, например, следующую команду:

mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=8.9.0 -DgroupId=com.mycompany -DartifactId=mychat -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

Vuecket пока не обзавелся собственным темплейтом проекта Maven. Возможно, в будущем, мы добавим и это. Теперь подключим сам Vuecket. Добавьте следующую зависимость в `pom.xml` проекта:

<dependency>
	<groupId>org.orienteer.vuecket</groupId>
	<artifactId>vuecket</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

Вывод текста в Markdown


Проект Wicket по умолчанию уже содержит в себе страницу-приветствие Wicket. Давайте в нее добавим немного кода, чтобы убедиться, что Vuecket уже работает. Например, выведем Hello World, но в Markdown, и так, чтобы сам текст задавался на стороне сервера в Apache Wicket компоненте. Для отрисовки Markdown будем использовать библиотеку vue-markdown.

В HomePage.html вместо приветствия Wicket добавляем:

<div wicket:id="app">
	<vue-markdown wicket:id="markdown">This will be replaced</vue-markdown>
</div>

А в HomePage.java следующий код:

public HomePage(final PageParameters parameters) {
	super(parameters);
	add(new VueComponent<String>("app")
			.add(new VueMarkdown("markdown", "# Hello World from Vuecket")));
}

Но где класс VueMarkdown? А его мы определим следующим образом:

@VueNpm(packageName = "vue-markdown", path = "dist/vue-markdown.js", enablement = "Vue.use(VueMarkdown)")
public class VueMarkdown extends Label {
	public VueMarkdown(String id) {
		super(id);
	}
	public VueMarkdown(String id, Serializable label) {
		super(id, label);
	}
}

Обратите внимание на аннотацию @VueNpm. Она необходима, чтобы включить Vuecket на данном Wicket компоненте, который подгрузит все необходимое из NPM, чтобы помочь браузеру отобразить уже Vue компонент для Markdown правильно.

Если вы все сделали правильно, то после запуска проекта через `mvn jetty:run` вы должны увидеть что-то типа этого на http://localhost:8080


Итак, что здесь произошло, и почему это работает?

  • Мы разметили страницу, добавив в неё 2 Vue компонента: для приложения и для вывода Markdown'а
  • Мы связали Vue компоненты с Wicket компонентами на стороне сервера (в HomePage.java)
  • Мы указали Vuecket'у, какая именно Vue.JS библиотека нужна для отрисовки 'vue-markdown'
  • А дальше всё просто: Wicket при отрисовке страницы браузеру использовал строчку "# Hello World from Vuecket", которую мы задали при добавлении Wicket компонента, а Vuecket помог браузеру загрузить нужные VueJS библиотеки, запустить VueJS приложение и отрисовать приветствие уже как отрендеренный Markdown

Github commit в помощь

Ввод сообщения и его предпросмотр


На этом шаге мы усложним наше приложение: сделаем ввод сообщения и его предпросмотр.
Добавим textarea в HomePage.html для ввода сообщения, а так же привяжем это поле и vue-markdown к VueJS переменной «text».

<div wicket:id="app">
	<textarea v-model="text" style="width:100%" rows="5"></textarea>
	<vue-markdown wicket:id="markdown" :source="text">Will be replaced</vue-markdown>
</div>

Мы уже используем переменную «text», но теперь надо её добавить в data Vue компонент. Здесь несколько путей сделать это в Vuecket, но давайте пойдем самым долгим:

  • Создадим свой VueComponent для Vue приложения
  • Проассоциируем его со своим *.vue файлом
  • Пропишем в *.vue файле логику: на данный момент, просто поле «text»

Вот примерно какие изменения мы сделаем:

//HomePage.java:
public HomePage(final PageParameters parameters) {
	super(parameters);
	add(new ChatApp("app")
			.add(new VueMarkdown("markdown")));
}
//ChatApp.java:
@VueFile("ChatApp.vue")
public class ChatApp extends VueComponent<Void> {
	public ChatApp(String id) {
		super(id);
	}
}

Ну и сам ChatApp.vue:

<script>
module.exports = {
    data: function() {
        return {
            text : ""
        }
    }
}
</script>

В итоге при запуске `mvn jetty:run` и вводе какого-нибудь текста можно увидеть следующее


В этой главе мы научились: создавать всем привычные *.vue файлы и ассоциировать их с компонентами Apache Wicket

GitHub commit в помощь

Отображение списка сообщений и добавление нового


В этой главе не будет ничего Vuecket или Wicket специфичного: чистое сияние VueJS.
Если декомпозировать задачу, то нам надо будет сделать следующее:

  • Добавить в наше Vue приложение поле-список для сохранения сообщений
  • Добавить метод для добавления нового сообщения в список
  • Отобразить список сообщений и не забыть про markdown

Для начала изменим наш ChatApp.vue и добавим нужную логику: новое поле `messages` со списком сообщений и метод `addMessage` для добавления нового сообщения. И не забудем, что при добавлении сообщения в список хорошо бы очистить исходное поле ввода. Для сообщений будем хранить не только текст, но и дату добавления/отсылки. В будущем можно будет рассширить дополнительными полями, например, кто послал данное сообщение, приоритет, нужная подсветка и т.п.

<script>
module.exports = {
    data: function() {
        return {
            text : "",
            messages: []
        }
    },
    methods: {
    	addMessage : function() {
    		this.messages.push({
    			message: this.text,
    			date: new Date()
    		});
    		this.text = "";
    	}
    }
}
</script>

Так же поменяем HomePage.html, добавим отображение списка сообщений и добавим вызов нашего метода addMessage при нажатии на Ctrl-Enter.

<div wicket:id="app">
	<div v-for="(m, index) in messages">
		<h5>{{ index }}: {{ m.date }}</h5>
		<vue-markdown :source="m.message"></vue-markdown>
	</div>
	<textarea v-model="text" 
			  style="width:100%" 
			  rows="5" 
			  @keyup.ctrl.enter="addMessage"
			  placeholder="Enter message and Ctrl-Enter to add the message">
	 </textarea>
	<vue-markdown wicket:id="markdown" :source="text">Will be replaced</vue-markdown>
</div>

При запуске `mvn jetty:run` и вводе нескольких сообщений можно увидеть что-нибудь вроде этого


В этой главе мы лишь средствами VueJS научили приложение добавлять сообщение в список и отображать последний.

GitHub commit в помощь

Включаем коллаборацию


Если до этого содержание нашей гостевой книги было для каждого посетителя страницы уникальным, то в этой главе мы включим связь с сервером и позволим синхронизацию со всеми посетителями. Для этого нам понадобятся Vuecket Data Fibers — решение, которое позволяет синхронизировать объекты на стороне браузера с объектами на стороне сервера. И что самое интересное, нам ничего не понадобится делать для этого на стороне клиентского кода! Звучит круто? Поехали кодить! Хотя… Тут всего две новые строчки в нашем компоненте ChatApp.java:

private static final IModel<List<JsonNode>> MESSAGES = Model.ofList(new ArrayList<JsonNode>());

public ChatApp(String id) {
	super(id);
	addDataFiber("messages", MESSAGES, true, true, true);
}

Что тут произошло:

  • Мы создали модель MESSAGES, которая доступна всем, так как создана как static final.
  • Добавили data-fiber, который связывает объект messages на сторое клиента и объект внутри модели MESSAGES на стороне сервера.
  • Указали, что data-fiber нам нужен для 3 вещей: load, observe, refresh. Load — для первоначальной подгрузка данных, Observe — для синхронизации с сервером изменений на стороне клиента, Refresh — для периодической подгрузки данных со стороны сервера.

При запуске можно даже початиться с каким-нибудь еще посетителем вашей гостевой страницы


GitHub commit в помощь

Серверные методы


В предыдущей главе я чуть схитрил, предоставив доступ на чтение и запись к коллекции сообщений сразу всем посетителям сайта. Такого делать крайне не рекомендуется, ведь тогда через data-fiber любой поситель может перезаписать все сообщения на сервере чем-нибудь своим или вообще их стереть. Data-fibers стоит использовать только для связи пользовательских объектов на стороне браузера только с теми объектами данных на стороне сервера, которые принадлежат тому же пользователю. Это значит, никаких static моделей и данных!

Как нам исправить ситуацию? Для этого нам придётся:

  • Отказаться от data-fiber, который работает по всем направлениям, а использовать его лишь для первоначальной загрузки списка сообщений.
  • Использовать метод на стороне сервера добавления нового сообщения в список.

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

Начнем с класса Message для хранения сообщений пользователей. Это может быть, кстати, какой-то JPA класс, который позволяет сохранять данные в базу данных.

public class Message implements IClusterable {
	@JsonProperty("message")
	private String text;
	private Date date;
	
	public String getText() {
		return text;
	}
	public void setText(String text) {
		this.text = text;
	}
	public Date getDate() {
		return date;
	}
	public void setDate(Date date) {
		this.date = date;
	}	
}

Обратите внимание на @JsonProperty. Таким образом мы перенаправили JSON поле «message» на наше Java поле «text».

Далее поменяем ChatApp.java чтобы сделать то, что описано выше: добавить vuecket метод для сохранения сообщения. Так же в коде можете заметить подрезание списка сообщений лишь до 20 (пользователи Хабра весьма старательные), но при удалении сообщения, оно всё равно сохраняется навечно в логах сервера.

@VueFile("ChatApp.vue")
public class ChatApp extends VueComponent<Void> {
	
	private static final Logger LOG = LoggerFactory.getLogger(ChatApp.class);
	private static final int MAX_SIZE = 20;
	private static final IModel<List<Message>> MESSAGES = Model.ofList(new ArrayList<Message>());

	public ChatApp(String id) {
		super(id);
		addDataFiber("messages", MESSAGES, true, false, false);
	}
	
	@VueMethod
	public synchronized void addMessage(Context ctx, Message message) {
		List<Message> list = MESSAGES.getObject();
		list.add(message);
		trimToSize(list, MAX_SIZE);
		IVuecketMethod.pushDataPatch(ctx, "messages", list);
	}
	
	private void trimToSize(List<Message> list, int size) {
		//It's OK to delete one by one because in most of cases we will delete just one record
		while(list.size()>size) LOG.info("Bay-bay message: {}", list.remove(0));
	}
}

Видите метод с аннотацией @VueMethod? В нем мы получаем новое сообщение, сохраняем в списке, подрезаем и отправляем клиенту уже обновленный список. Так же обратите внимание, что data-fiber был переконфигурирован, чтобы запрашивать данные только при начальной загрузке Vue приложения.

Так же нам надо поменять логику в ChatApp.vue, чтобы вместо локального поля «messages» отправлять новое сообщение на сервер в ассинхронном режиме (vcInvoke)

module.exports = {
    data: function() {
        return {
            text : "",
            messages: []
        }
    },
    methods: {
    	addMessage : function() {
    		this.vcInvoke("addMessage", {
    			message: this.text,
    			date: new Date()
    		});
    		this.text = "";
    	}
    }
}

Что узнали из главы:

  • Как создавать методы на стороне сервера для Vuecket'а
  • Как вызывать методы на сервере из бразера
  • Как отсылать клиенту нужные изменения

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


GitHub commit в помощь

Заключение


На этом сегодня позвольте закруглиться. Есть еще много улучшений, которые можно придвнести в наш код, но я это оставлю вам, дорогие читатели. Если нужна будет помощь, то пишите, поможем!

Понравился фреймворк? Пожалуйста, поделитесь вашим мнением. Ведь именно вашим мнением живет и развивается Open Source.

Спойлер ближайших улучшений в Vuecket
  • Поддержка WebSocket'ов для более реактивной связи клиента и сервера.
  • Возможность пересылать между браузером и сервером именно дельту изменений, а не весь объект целиком.
  • Больше настроек для data-fiber'ов для тонкой их настройки.
  • Набор Vuecket/Wicket компонент, которые уже предварительно прединтегрированны с наиболее интересными VueJS библиотеками, например, уже упомянутый компонент для отображения текста в Markdown