7 лет без «фрейморка»

7 лет без «фрейморка»

Данные и API

Данные и API

* — backend

JSSDK

Раньше

JSSDK: Стало

JSSDK: Thread

			// Получение списка тредов
			Thread.find({folder: 8}).then(threads => {
				// ...
			});
		

Пример перехвата

			RPC.intercept({
				status: "invalid",
				process(req) {
					if (req.get("body.folder.error") === "not_open") {
							// Возвращаем Promise
						return openSecureFolderLayer({
							folder: req.get("body.folder.value")
						});
					}
				}
			});
		

~2K assertions

JSDoc

JSSDK: Model

Model: Методы получения модели

new Model(): Базовый интрейфейс модели

* name — поддерживает dot-нотацию

Персистентность

Backbone и его проблемы

			const listFoo = new BackboneThreadCollection({folder: 123});
			listFoo.fetch();

			const listBar = new BackboneThreadCollection({folder: 123});
			listBar.fetch();

			#!+ console.log(listFoo.get(456) === listBar.get(456));
			// false :[

			listFoo.on("chnage", () => { console.log("Cnanged"); });
			listBar.get(456).set("flag", true);
			#!- // ничего не произойдет, потому что инстансы разные
		

JSSDK: Персистентность

			// Где-то получаем тред
			Thread.findOne(123).then(thread => {
				thread.get("messages").on("change:flags", () => {
					// Реагируем на изменение флагов
				});
			});

			#!+ // Где-то письмо (которое входит в тред)
			Message.findOne(345).then(message => {
				#!+ // Инвертируем флаг (мы так не делаем :])
				#!- message.set("flags.unread", !message.is("flags.unread"));
			#!- });
		

Поддержка вложенных моделей

			const Thread = Message.extend({
				className: "mail.Thread", // для логирования
				findUrl: "threads", // Получение списка тредов
				findOneUrl: "threads/thread", // Запрос за тредом
				defaults: { // Свойства по умолчанию
					...
					messages: new Message.List.Sorted,
				}
			});

			// Отсортированный список моделей
			Message.List.Sorted = Message.List.extend({
				comparator: {date: true} // desc
			});
		

Неполные данные

Неполные данные

			const Thread = Message.extend({
				findOneUrl: "threads/thread"
				findOneUrl: (query) => query.withoutQuote
						? "threads/thread/short"
						: "threads/thread"
				},
				isDataFully(query) {
					#!+ return this.is("messages.length") && (
							query.withoutQuote || hasQuotes(this)
					#!- );
				}
			});
		

Логирование

			Client.find().then(list => ...);
		

Ошибки

			request.on("error", (evt, req) => {
				saveToLog(req);
			});
		
  • uuid:`string` — id запроса
  • url:`string` — что запрашивали
  • type:`string` — GET, POST и т.п.
  • data:`Object` — Параметры запроса
  • options:`Object` — Опции запроса
  • body:`Object` — Ответ (JSON)
  • error:`*` — Ошибка
  • responseText:`*` — raw-ответ
  • startTime:`number` — Время начала запроса
  • endTime:`number` — Время завершения
  • duration:`number` — Продолжительность запроса
  • aborted:`boolean` — Отменен
  • batched:`boolean` — В «пачке»
  • pending:`boolean` — В ожидании ответа
  • retries:`number` — Количество попыток

«Действие»

«Действие» / Lifecycle

			const MyAction = Action.extend(/** @lends MyAction# */{
				#!+ // Подготовка данных
				prepare(params, options)/* Promise|Object */ {
					return {computed: params.foo + params.bar};
				#!- },
				#!+ // Выполняемая операция
				#!- operation({computed}, params, options) /* Promise */ { /* … */ },
				#!+ // Обратная операция
				#!- undoOperation(data, params, options) /* Promise */ { /* … */  },
				#!+ // Откатываем изменения в случае ошибки при выполнении
				#!- rollbackOperation() { }
			});
		

Пример MoveTo

			MoveTo.execute({
				folderTo: 123, // id папки куда переносим
				models: [...] // Массив Писем или Тредов (модели)
			}).then(action => {
				// Ссылка на модель Папки куда переместили
				const folderTo = action.folderTo;

				// Список реально перемещенных моделей
				const affectedModels = action.models;

				// Показывает нотификацию для отмены действия
				showUndoNotify().onUndo(() => action.undo());
			});
		

Приложение

Установка

Общие сведения

Точка вход

			require(["app/app", "logger"], function (app, logger) {
				// Скрыть лоудер загрузки приложения
				app.ready(hideLoading);
				// Перехват ссылок и запуск приложения
				app.listenFrom(document, {
					autoStart: true,
					logger: logger
				});
			});
		

Что такое app?

Что такое app?

			const app = Pilot.create({
				#! model: { /* модели доступные всем маршрутам */ },
				#!+ "#letters": {
					url: "/:folder", // "/inbox/", "/trash/" или "/123/"
					#!+ model: { // модели конкретного маршрута
						threads: {
							fetch: ({params: {folder}}) => Thread.find({folder}),
						},
					#!- }
				#!- },
			});
		

Навигация по id

			// Перейти на нужный маршрут
			app.go("#letters"); // "/inbox/"
			app.go("#letters", {folder: 123}); // "/123/"

			// Сформировать url
			app.getUrl("#letters", {folder: 0}); // "/inbox/"
			app.getUrl("#letters", {folder: 123}); // "/123/"

			// Или для текущего маршрута
			app.route.getUrl({folder: 0}); // "/inbox/"
		

Продвинутая настройка маршрута

			"#letters": {
				url: {
					pattern: "/:folder", // "/inbox/", "/trash/" или "/123/"
					#!+ params: {
						folder: {
							#! default: Folder.INBOX,
							#! decode: (val) => (val in TYPE2ID) ? TYPE2ID[val] : toInt(val),
							#! encode: (id) => (id in ID2TYPE) ? ID2TYPE[id] : id,
						}
					#!- },
					#! toUrl: (params, builder) => builder({params, ...extra}),
				},
				model: { /*...*/ },
				// ...
			}
		

App Model

App Model

			console.dir(app.model);
			{
				request: Pilot.Request, // текущий «location»
				authUser: User, // авторизованный юзер
				folder: Folder, // активная папка
				folders: Folder.List, // список папок
				letters: Thread.List, // список тредов или писем
				letter: Thread, // активный тред (на чтении)
				status: Object, // статус ящика
				...
			}
		

App Model

			// Источники данных
			import request from "./source/request";
			import authUser from "./source/authUser";
			import status from "./source/status";
			import folders from "./source/folders";
			// ...etc
			const loader = new Pilot.Loader({
				request,
				authUser,
				status,
				folders,
				// ...etc
			}, {
				/* разные опции */
			});
		

Описания источника данных

			{
				default: null, // значение по умолчанию
				match: null, // или массив id-маршрутов
				fetch: (/** @Pilot.Request */req) => ...,
				reaction: (req, newStatus, event) => {
					// Реакция на события
					// (загрузка данных или «Действия» над моделями)
				}
			}
		
			// source/status.js — состояние ящика (папки, письма и т.п.)
			export default {
				fetch: (req) => loadStatusByRequest(req),
			};
			#!+ // source/folders.js — список папок
			export default {
				fetch: (req, **waitFor**) =>
					waitFor("status").then(status => status.folders),
			#!- };
		
			// source/folder.js — активная папка
			export default {
				fetch(req, waitFor) {
					if (req.is("#read-letter")) { // Если это «Чтение»
						#!+ return Promise
							  .all([waitFor("folders"), waitFor("letter")])
						#!-	   .then(([folders, letter]) => folders.get(letter.get("folder")));
					} else {
						#! return waitFor("folders").then(folders => folders.get(req.params.folder));
					}
				}
			};
		
			// source/letter.js — активный тред или письмо
			export default {
				match: ["#read-letter", "#search-letter"],
				fetch: ({params:{letter}}) => Thread.findOne(letter),
			};
		

Pilot.Request

  • params — параметры маршрута
  • query — GET-параметры
  • route — текущий маршрут, route.is("#letters #search")
  • router — ссылка на роутер
  • href
  • protocol
  • host
  • hostname
  • port
  • pathname
  • search
  • hash
  • referrer

UI + Данные

			const app = Pilot.create(sitemap);
			#!+ // Создаём AppView
			app.view = new UIApplication(dataSource.getData(), {
				router: app,
				dataSource,
			#!- });
			#!+ // Подписываем View на изменение данных
			dataSource.onChange(data => {
				app.view.set(data);
			#!- });
		

UIApplication

— Все блоки «глупые» (ну почти)

— Attrs Down / Events Up  

Attrs Down, Events Up

			<b:letter-status
				name="unread"
				state="{attrs.unread}"
				remit:click="invert:unread"
			/>
			#!+ <b:dataset-letters>         // <--- слушает "invert:unread"
				<b:dataset>
					<b:dataset-letters-item>
			#!-				<b:letter-status/>  // <--- излучает "invert:unread"
		

Структура блока

/Button/

Button/Button.spec.js

			export default {
				keywords: "btn кнопка",
				cases: {
					"base": {
						attrs: [{text: "Написать"}]
					},
					"primary": {
						attrs: [
							{type: "submit", text: "Написать"},
							{ico: "compose", type: "submit", text: "Написать"},
							{ico: "compose", short: true, type: "submit", text: "Написать"},
							{pressed: true, type: "submit", text: "Написать"}
						]
					},
				}
			};
		

Блоки💖️JSSDK

Блоки💖️JSSDK

Где логика?

Медиаторы

Например

			<b:portal-menu
				#! use:mediator="letters-actions letters-selection"
			/>
		
			import DeleteAction from "jssdk/mail/actions/Delete";
			import UIPortalMenu from "ui-block/PortalMenu/PortalMenu";
			import lettersSelection from "mediator/letters-selection";

			#!+ export default feast.Mediator.create("letters-actions", {
				components: {
					#!+ "portal": {  // имя для обращения внутри медиатора
						#! "class": UIPortalMenu, // <b:portal-menu/>
						#!+ // события, на которые подписывается медиатор
						#!- "events": ["delete", "move" ...],
					#!- },
					#!+ "router": {
						"class": Pilot,
					#!- },
					// ...
				},
				handleEvent(evt) { /* ... */ },
				// ...
			#!- });
		
			export default feast.Mediator.create("layout-manager", {
				components: { /* ... */ },
				handleEvent(evt) {
					// Тип события
					const type = evt.type;
					// Получаем список моделей от медиатора отвечающего за выделение писем
					const selectedModels = lettersSelection.getModels();
					#!+
					if (type === "delete") {
						#!+ DeleteAction.execute({models: selectedModels})
							.then(this._done)
						#!-	.catch(this._fail);
					} else if (type === "move") {
						// ...
					}
					#!-
					#!+ swipeManager.reset(); // Закрываем свайпы
					lettersSelection.reset(); // Сбрасываем выделение

					#!- // По завершению `handleEvent`, всем связанные блоки с медиатором будут обновлены
				}
			});
		

Сервисы

The End

JSSDK
Feast (UI)

github.com/RubaXa