Сегодня я хотел бы анонсировать js-фреймворк, позволяющий создавать диалоговые микро-приложения в несколько строк js-кода, разделяемого между клиентом и сервером.
Идея простого, удобного и специализированного под диалоговые решения фреймоворка родилась после нескольких лет работы нашей команды с интерактивными приложениями. Нам хотелось создать простой конструктор интерактивных виджетов, не перегруженный сложным функциналом и максимально заточенный под конкреную задачу. В итоге мы пришли к решению перекроить наши наработки и немного изменить концепцию продукта.
В отличие от большинства фреймворков, WidLib не претендует на универсальность: он предназначен для быстрого создания многостраничных диалоговых приложений.
Спектр задач
- Виджеты заказа доставки
- Wi-Fi приложения (путеводитель по торговому центру или интерактивное меню при подключении к местному Wi-Fi)
- Калькуляторы (кредиты, пластиковые окна и двери)
- Формы подписки на сайте
- Викторины (игровые, а так же для маркетинговых акций и розыгрышей)
- Тесты (проверка компетенций, обучение, работа с кадрами)
- Онлайн-помощники (например, подбор туристического маршрута или ассистент колл-центра)
- Встраивание виджетов в мобильные приложения (здесь он похож на PhoneGap)
- Диалоговые приложения для соцсетей (опять-таки викторины, нелинейные опросы)
- Бронирование (билеты на самолет, номер в гостинице или время у стоматолога)
- и т.д.
Чтобы создать DSL, мы спросили себя – каким мы хотим видеть предельно простой, но при этом гибкий язык описания (и в меньшей степени – программирования) этих диалоговых приложений. Мы использовали подход convention over configuration, знакомый по Ruby on Rails. Простые приложения пишутся в три строчки, сложные – чуть больше.
Давайте рассмотрим на примере виджета по заказу пиццы на сайте:
(DSL еще находится в доработке, поэтому предложения приветствуются.)
Coffeescript
widlib=require("widlib-server")
server=widlib.init
# Можно использовать любой шаблонизатор (index_template=Handlebars.compile("...")),
# в функцию передается объект страницы, шаблон сохранится в @app.template
template: index_template
pages:
type:
# шаблоны можно также задавать индивидуально для каждой страницы
template: type_template # @app.pages["type"].template
body: "Выберите пиццу"
# при клике на ссылку происходит событие submit, автоматический переход на следующую страницу (если не указано явно)
# также у каждой страницы есть события onLeave, onEnter
inputs: [
{ value: "Маргарита", type: "link", name: "type", price: 350 },
{ value: "Пепперони", type: "link", name: "type", price: 360 },
{ value: "Филадельфия", type: "link", name: "type", price: 370 },
{ value: "Четыре сыра", type: "link", name: "type", price: 380 },
]
size:
body: "Выберите размер"
# если в параметре используем функцию - значение вычисляется лениво
inputs: ->
price = @session.input("type").price # session - хранит данные текущей сессии. @session.input("type") присвоился автоматически после страницы type.
[
{ value: "30", type: "link", name: "size", price: price },
{ value: "40", type: "link", name: "size", price: price*1.2 },
{ value: "50", type: "link", name: "size", price: price*1.5 },
]
# можно в явном виде указать следующую страницу или использовать функцию
onSubmit: "address"
address:
body: "Введите адрес"
inputs: [ { name: "address", type: "text", placeholder: "улица, дом, подъезд, квартира" }, { type: "submit", value: "Далее" } ]
phone:
body: "Введите телефон"
inputs: [ { name: "phone", type: "text", placeholder: "+7 xxx xx xx" }, { type: "submit", value: "Далее" }]
onSubmit: -> # также возможно использовать события onLoad, on
@data("orders").push @session.values() # данные записываем в постоянное хранилище
@data("email").push email_template(@session.values())
"success" # возвращаем имя следующей страницы
success:
body: "Ваша пицца уже едет к вам"
image: -> "/images/#{@session.value("type")}.jpg" # можно использовать дополнительные параметры, в данном случае image для view
data:
orders:
type: "spreadsheet" # используя модули для различных API можно хранить или синхронизировать данные с внешними сервисами
url: "https://docs.google.com/spreadsheet/ccc?key=0Au4e-jj1-69ZdEloMW03UExKLXIZFSUE"
email:
type: "email"
to: "1@interactiff.net"
server.listen "3000"
widlib=require("widlib-server")
server=widlib.init
template: index_template
pages:
type:
template: type_template
body: "Выберите пиццу"
inputs: [
{ value: "Маргарита", type: "link", name: "type", price: 350 },
{ value: "Пепперони", type: "link", name: "type", price: 360 },
{ value: "Филадельфия", type: "link", name: "type", price: 370 },
{ value: "Четыре сыра", type: "link", name: "type", price: 380 },
]
size:
body: "Выберите размер"
inputs: ->
price = @session.value("type").price
[
{ value: "30", type: "link", name: "size", price: price },
{ value: "40", type: "link", name: "size", price: price*1.2 },
{ value: "50", type: "link", name: "size", price: price*1.5 },
]
onSubmit: "address"
address:
body: "Введите адрес"
inputs: [ { name: "address", type: "text", placeholder: "улица, дом, подъезд, квартира" }, { type: "submit", value: "Далее" } ]
phone:
body: "Введите телефон"
inputs: [ { name: "phone", type: "text", placeholder: "+7 xxx xx xx" }, { type: "submit", value: "Далее" }]
onSubmit: -> # также возможно использовать события onLoad, on
@data("orders").push @session.values()
@data("email").push email_template(@session.values())
"success"
success:
body: "Ваша пицца уже едет к вам"
image: -> "/images/#{@session.value("type")}.jpg"
data:
orders:
type: "spreadsheet"
url: "https://docs.google.com/spreadsheet/ccc?key=0Au4e-jj1-69ZdEloMW03UExKLXIZFSUE"
email:
type: "email"
to: "1@interactiff.net"
server.listen "3000"
var client, server, widlib;
widlib = require("widlib-server");
server = widlib.init({
template: index_template,
pages: {
type: {
template: type_template,
body: "Выберите пиццу",
inputs: [
{ value: "Маргарита", type: "link", name: "type", price: 350 },
{ value: "Пепперони", type: "link", name: "type", price: 360 },
{ value: "Филадельфия", type: "link", name: "type", price: 370 },
{ value: "Четыре сыра", type: "link", name: "type", price: 380 },
]
},
size: {
body: "Выберите размер",
inputs: function() {
var price;
price = this.session.input("type").price;
return [
{ value: "30", type: "link", name: "size", price: price },
{ value: "40", type: "link", name: "size", price: price*1.2 },
{ value: "50", type: "link", name: "size", price: price*1.5 },
];
},
onSubmit: "address"
},
address: {
body: "Введите адрес",
inputs: [ { name: "address", type: "text", placeholder: "улица, дом, подъезд, квартира" }, { type: "submit", value: "Далее" } ]
},
phone: {
body: "Введите телефон",
inputs: [ { name: "phone", type: "text", placeholder: "+7 xxx xx xx" }, { type: "submit", value: "Далее" }],
onSubmit: function() {
this.data("orders").push(this.session.values());
this.data("email").push(email_template(this.session.values()));
return "success";
}
},
success: {
body: "Ваша пицца уже едет к вам",
image: function() {
return "/images/" + (this.session.value("type")) + ".jpg";
}
}
},
data: {
orders: {
type: "spreadsheet",
url: "https://docs.google.com/spreadsheet/ccc?key=0Au4e-jj1-69ZdEloMW03UExKLXI3cGRlbkJteGZFSUE#gid=0"
},
email: {
type: "email",
to: "1@interactiff.net"
}
}
});
server.listen("3000");
Первой строкой мы создаем объект widget и загружаем в него сценарий.
Объект сценария состоит из страниц и данных. Каждая страница – отдельный экран, который увидит пользователь в этом виджете.
Каждый объект с данными соответствует адаптеру для их хранения или передачи. В простейшем случае – это просто массив, в сложных – REST интерфейс, MongoDB, Google Spreadsheet и прочие.
Мы можем использовать один и тот же сценарий, как на клиенте (в т.ч. standalone), так и на сервере node.js. Использование на сервере позволяет скрыть сценарий или его часть от пользователя, что может пригодиться для калькулятора кредита или викторины с розыгрышем призов, а также даёт доступ к динамическим данным и API, агрегированию и обработке пользовательской информации.
Клиентская часть виджета доставки пиццы, в данном случае пустая:
client=new Widlib.Client
# здесь можно использовать тот же самый код, что и в серверной части.
# pages: ...
# data: ...
server: "/"
container: "#container"
Разработчику даже не нужно заботиться, каким образом передавать информацию, и в каком случае обмениваться данными с сервером. Библиотека автоматически проверяет наличие страниц или данных в локальном сценарии, после чего незаметно для автора запрашивает их у сервера (или отправляет), используя RPC. Серверная версия обладает более полным функционалом, а клиентская работает быстрее и независимо, что позволяет ее использовать в мобильных приложениях, а также не беспокоить сервер по пустякам.
Фичи
- Декларативный стиль написания
- Для простых случаев практически не требует навыков программирования
- Возможно использовать готовые шаблоны (планируется также создать библиотеку шаблонов и визарды для создания виджетов обывателями)
- Возможно использование декларативного html-шаблонизатора и биндинга rivets.js (подобен таковому в AngularJS). Скрипт связывания будет доступен в репозитории фреймворка
- Независимость от библиотек для работы с DOM
- Разделяемый код между клиентом и сервером
- Незаметный fallback клиентского к серверному скрипту.
Воспользоваться открытой библиотекой WidLib можно будет уже в декабре.
Призываю вас в комментариях описать, где бы вы хотели использовать подобный фреймворк.
P.S. для проекта будет доступна исчерпывающая документация, планируется библиотека примеров и визардов, а также сверхшустрый
Автор: Litiy