Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте»

в 11:39, , рубрики: basis.js, javascript, single page application, Вконтакте

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 1
Всем доброго времени суток.
Продолжаю интереснейший цикл статей про создание продвинутых Single Page Application на basis.js.
В прошлый раз мы научились работать с коллекциями и реализовали полноценный интерактивный список.
В этот раз мы начнем создавать полноценный клиент для ВКонтакте.
А именно: реализуем авторизацию, загрузку новостей, друзей и музыки.

Меню и навигация

Для начала, реализуем простую страничку с заголовком и меню.

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 2

При клике по пунктам меню (вкладкам), происходит навигация на разные url и выделение активного пункта меню.
Давайте посмотрим на код главного файла нашего будущего приложения:

let Node = require('basis.ui').Node;
let Header = require('app.ui.header.component');
let Menu = require('app.ui.menu.component');

require('basis.app').create({
  title: 'VK Client by Basis.JS',
  element: new Node({
    template: resource('./template.tmpl'),
    binding: {
      header: 'satellite:',
      menu: 'satellite:'
    },
    satellite: {
      header: Header,
      menu: Menu
    }
  })
});

Подключаем необходимые модули: Node, а так же компоненты заголовка и меню (которые рассмотрим ниже).
Далее создается приложение, при помощи метода basis.app.create(). Можно, конечно, обойтись без него и делать так, как мы делали раньше — создавать новый Node и помещать его в какой-нибудь элемент на странице.
Но в basis.js имеется хелпер basis.app, который инкапсулирует некоторую логику, связанную с заголовком страницы и размещением корневого компонента приложения на странице.
Так же, посмотрим на шаблон нашего приложения:

<div class="container">
  <!--{header}-->
  <!--{menu}-->
  <hr/>
</div>

Заголовок и меню являются сателлитами корневого компонента нашего приложения.
На данный момент, компонент заголовка очень простой:

let Node = require('basis.ui').Node;

module.exports = Node.subclass({
  template: resource('./template.tmpl') // <h1>Добро пожаловать!</h1>
});

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

let Node = require('basis.ui').Node;
let Value = require('basis.data').Value;
let router = require('basis.router');
let currentPage = Value.from(router.route(':page').param('page'));

module.exports = Node.subclass({
  template: resource('./template.tmpl'), // <div class="btn-group btn-group-lg"/>
  childClass: {
    template: resource('./item.tmpl'),
    selected: currentPage.compute((node, page) => node.url == page),
    binding: {
      title: 'title'
    },
    action: {
      click() {
        router.navigate(this.url);
      }
    }
  },
  childNodes: [
    {title: 'Новости', url: 'news'},
    {title: 'Друзья', url: 'friends'},
    {title: 'Музыка', url: 'audio'}
  ]
});

Давайте сразу обратим внимание на содержимое переменной currentPage.
Здесь всегда будет храниться актуальный маршрут, изменения которого мы можем отслеживать.
Это значение мы используем в свойстве selected пунктов меню.
То есть активность конкретного пункта меню зависит от текущего маршрута.
Если url текущего пункта меню совпадает с текущим маршрутом, то у этого пункта меню свойство selected = true.
Таким образом, в один момент времени будет выбран только один пункт меню.
При клике на конкретный пункт, происходит навигацию к указанному url.
Более подробно о роутере, встроенном в basis.js можно почитать в соответствующем разделе документации.

Теперь посмотрим на шаблон пункта меню:

<b:define name="active" from="selected" type="bool"/>

<button type="button" class="btn btn-default {active}" event-click="click">
  {title}
</button>

Каждый пункт меню — это кнопка. Если selected пункта равен true, то добавляем к кнопке класс active, в ином случае — убираем.

Вот и всё. Меню с навигацией — готово.
Теперь, при нажатии на пункты меню, будет происходить переход по соответствующему url.
Осталась небольшая мелочь — маршрут по умолчанию.
Если просто открыть наше приложение, без указания маршрута, то ни один пункт меню выбран не будет.
Давайте исправим это таким образом, чтобы маршрутом по умолчанию были Новости.
Модифицируем основной файл нашего приложения:

// ...
let router = require('basis.router');
let defaultRoute = 'news';

require('basis.app').create({
  title: 'VK Client by Basis.JS',
  element: new Node({
    // ...
  })
}).ready(() => {
  router.route('*page').param('page').as(page => page || router.navigate(defaultRoute, true));
});

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

Авторизация

Теперь задействуем ВКонтакте API и реализуем с его помощью авторизацию.
Посмотрите на обертку над VK API (далее просто API). Мы не будем рассматривать ее полностью, а посмотрим только на ключевые моменты.
Обратите внимание, что сам API является наследником от basis.data.Value.
Это значит, что у нее, как и у любого источника данных, есть состояния:

  • UNDEFINED когда пользователь не авторизован
  • PROCESSING во время авторизации
  • READY после успешной авторизации
  • ERROR в случае ошибки

Посмотрим на то, как реализована смена состояний модели. Для этого обратимся к методам login() и logout():

login() {
  this.setState(STATE.PROCESSING);
  this.isLoggedIn().then(
    () => this.setState(STATE.READY),
    () => {
      global.VK.Auth.login(response => {
        this.setState(response.session ? STATE.READY : STATE.UNDEFINED);
      }, config.perms);
    }
  );
},
logout() {
  global.VK.Auth.logout();
  this.setState(STATE.UNDEFINED);
}

Вызывая login(), API будет переведен в состояние PROCESSING.
Далее идет проверка — если пользователь уже авторизован, то сразу переводим API в состояние READY. Если нет, то авторизуемся при помощи метода VK.Auth.login() из VK API. Процесс авторизации через VK API сводится к тому, что вам показывается окно с предложением ввести логин и пароль.

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 3

Когда окно будет закрыто (авторизация прошла успешно или была отменена), будет вызван переданный callback, в котором будет установлено конечное состояние нашей модели: READY, в случае успешной авторизации и UNDEFINED, в случае отмены авторизации.
Вызывая logout(), уничтожаем сессию методом VK.Auth.logout() и переводим API в состояние UNDEFINED.
Теперь посмотрим на другой важный метод — callApi():

callApi(method, params = {}) {
  return this.isLoggedIn()
    .catch(e => Promise.reject(new Error('Ошибка авторизации!')))
    .then(
      () => {
        return new Promise((resolve, reject) => {
          basis.object.complete(params, {v: config.version});

          global.VK.api(method, params, response => {
            if (response.error) {
              reject(new Error(response.error.error_msg));
            } else {
              resolve(response.response);
            }
          });
        });
      },
      e => {
        this.setState(STATE.ERROR, e.message);
        throw e;
      }
    );
}

Суть данного метода — отправить запрос через VK API. Перед выполнением каждого запрос проверяем наличие авторизации. Если авторизации нет (например мы открыли наше приложение в двух вкладках браузера и в одной из них нажали выйти), то выбрасываем ошибку и переводим API в состояние ERROR. Если с авторизацией всё хорошо, то выполняем запрос. Если сервер, в ответ на запрос, сообщает нам об ошибке — выбрасываем ошибку и переводим API в состояние ERROR. В ином случае — возвращаем результат.

За счет этого, мы можем абстрагироваться от нюансов работы с VK API и оперировать лишь состояниями модели:

let STATE = require('basis.data').STATE;
let Value = require('basis.data').Value;
let Node = require('basis.ui').Node;
let router = require('basis.router');
let Header = require('app.ui.header.component');
let Menu = require('app.ui.menu.component');

let vkApi = require('app.vkApi');
let apiState = Value.state(vkApi);
let defaultRoute = 'news';

require('basis.app').create({
  title: 'VK Client by Basis.JS',
  element: new Node({
    // компонент приложения активен, пока пользователь авторизован
    active: apiState.as(state => state == STATE.READY),
    // компонент приложения заблокирован в процессе авторизации
    disabled: apiState.as(state => state == STATE.PROCESSING),
    template: resource('./template.tmpl'),
    binding: {
      header: 'satellite:',
      menu: 'satellite:',
      // будет содержать текст ошибки, в случае ее возникновения
      error: apiState.as(state => state == STATE.ERROR && state.data)
    },
    satellite: {
      header: Header,
      menu: Menu
    },
    action: {
      // обработка кнопки "авторизоваться"
      login() {
        vkApi.login();
      }
    }
  })
}).ready(() => {
  router.route('*page').param('page').as(page => page || router.navigate(defaultRoute, true));
  // пытаемся авторизоваться сразу после инициализации приложения
  vkApi.login();
});

Теперь применим эти свойства в шаблоне:

<div class="container">
  <div b:show="{active}">
    <!--{header}-->
    <!--{menu}-->
    <hr/>
  </div>
  <div class="jumbotron text-center" b:hide="{active}">
    <h1>
      VK Client
      <small> powered by basis.js</small>
    </h1>
    <div class="alert alert-danger" b:show="{error}">
      {error}
    </div>
    <button class="btn btn-primary btn-lg" event-click="login" disabled="{disabled}">Авторизация</button>
  </div>
</div>

Показываем экран приветствия пока пользователь не авторизован, в ином случае — показываем меню и заголовок.
Кнопка Авторизация будет заблокирована в процессе авторизации.
Так же, добавим кнопку выйти в главное меню:

<div>
  <div{childNodesElement} class="btn-group btn-group-lg"/>
  <button class="btn btn-primary btn-lg pull-right" event-click="logout">выйти</button>
</div>

И, в компоненте меню, обработаем клик по этой кнопке:

let vkApi = require('app.vkApi');
// ...

module.exports = Node.subclass({
  // ...
  action: {
    logout() {
      vkApi.logout();
    }
  }
  // ...
});

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 4

Отлично! Теперь у нас есть приложение, которое может авторизоваться в ВКонтакте, а так же удобный механизм отслеживания состояний модели. Двигаемся дальше.

Старницы

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

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 5

Для начала, создадим общую страницу, от которой будем наследовать все остальные:

let Value = require('basis.data').Value;
let Expression = require('basis.data.value').Expression;
let STATE = require('basis.data').STATE;
let Node = require('basis.ui').Node;

module.exports = Node.subclass({
  active: basis.PROXY,
  binding: {
    loading: Value.query('childNodesState').as(state => state == STATE.PROCESSING),
    error: Value.query('childNodesState').as(state => state == STATE.ERROR && state.data),
    empty: node => new Expression(
      Value.query(node, 'childNodesState'),
      Value.query(node, 'childNodes.length'),
      (state, itemCount) => !itemCount && state == STATE.READY
    )
  },
  handler: {
    activeChanged() {
      if (this.active) {
        this.dataSource.deprecate();
      }
    }
  }
});
<div>
  <div class="alert alert-info" b:show="{loading}">загружается...</div>
  <div class="alert alert-warning" b:show="{empty}">список пуст</div>
  <div class="alert alert-danger" b:show="{error}">{error}</div>
  <div{page} b:hide="{loading}"/>
</div>

Так уж получилось, что все три страницы имеют общую логику работы:

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

Нечто похожее мы уже делали в прошлый раз.
Чтобы не дублировать код в компонентах всех трех страниц, выносим его в отдельный файл.
Давайте подробнее рассмотрим что там происходит:
Наша абстрактная страница — это всего лишь Node с определенными биндингами и еще парой деталей.
Я не буду сейчас останавливаться на этих биндингах, так как они подробно разбирались в прошлый раз.
Сейчас нас больше инеересует другое.
Что такое active: basis.PROXY?
В прошлый раз мы выяснили, что набор данных начинает синхронизацию только когда его состояние UNDEFINED или DEPRECATED и у него есть активный потребитель. Выполнение этих двух условий обязательно для того, чтобы запустить процесс синхронизации набора. Сейчас нас больше интересует часть про "когда есть активный потребитель".
Потребитель — это сущность (наследник basis.data.AbstractData), которой нужны данные (и при том актуальные), представленные в другом объекте данных.
Активный потребитель — это потребитель со свойством active = true.

По умолчанию, когда для Node назначается dataSource, Node автоматически становится потребителем данного набора.
Отлично, потребитель есть. Но активный ли он?
Опять же, по умолчанию, Node не является активным потребитетем (свойство active = false).
"А давайте просто добавим active: true в описание Node и проблема будет решена" — можете предложить вы.
Не всё так однозначно. Мы ведь делаем умное приложение? А значит набор должен не просто один раз синхронизироваться при запуске приложения, а еще и обновлять свои данные по мере необходимости.
У нас есть три страницы и три набора под каждую из них (новости, друзья и аудиозаписи). Будем запускать синхронизацию набора только тогда, когда переходим на вкладку, которая нуждается в этом наборе. Таким образом, мы не только реализуем механизм актуализации данных, но еще и добавим "ленивую" синхронизацию данных. То есть синхронизацию только при необходимости.
Исходя из этого, при переходе на какую-либо вкладку, мы должны переводить состояние соответствующего набора в DEPRECATED.
Но как узнать, что мы переключились на какую-либо вкладку?
Скорее всего вы уже начали думать, что мы всё дальше отдаляемся от первоначального вопроса.
Но это не так. Еще чуть-чуть и вы увидите, как все сюжетные линии сольются воедино, прояснив общую картину.
Итак, как узнать, что мы переключились на какую-либо вкладку?
Как и в случае с dataSource, Node автоматически становится потребителем всех своих сателлитов, а так же их владельцем. Мы сделаем так, что при переключении между вкладками, соответствующая страница будет становиться сателлитом корневого Node нашего приложения.
Значит можно заставить страницу среагировать в тот момент, когда она станет сателлитом и в этот момент перевести состояние своего набора в DEPRECATED:

// ...

module.exports = Node.subclass({
  // ...
  handler: {
    ownerChanged() {
      if (this.owner) {
        this.dataSource.deprecate();
      }
    }
  }
});

Отлично! В тот момент, когда страница станет сателлитом корневого компонента нашего приложения, набор данных перейдет в состояние DEPRECATED.

Но давайте еще раз проговорим: "Набор данных начинает синхронизацию только когда его состояние UNDEFINED или DEPRECATED и у него есть активный потребитель."
С переключением состояния набора и наличием потребителя мы разобрались. Но как быть с активностью? Если просто добавить к странице active: true, то она будет всегда активна и ее источник данных попытается синхронизировать данные сразу при создании, вне зависимости от того, нужны нам сейчас эти данные или нет.
Нам это не совсем подходит, ведь есть случаи, когда синхронизация просто невозможна. Например, когда мы еще не прошли процедуру авторизации или отключен интернет.
Чтобы не заоморачиваться обработкой этих кейсов в самой странице, добавим ей свойство active: basis.PROXY, которое переведет Node в особый режим, в котором Node будет активен только тогда, когда у него самого есть активный потребитель.
Зная это, нет необходимости отслеживать ownerChanged, а лучше подписаться на activeChanged. Таким образом будем застравлять набор синхронизировать данные только в момент появляения активного потребителя.
Взгляните еще раз на итоговый код компонента страницы:

let Value = require('basis.data').Value;
let Expression = require('basis.data.value').Expression;
let STATE = require('basis.data').STATE;
let Node = require('basis.ui').Node;

module.exports = Node.subclass({
  active: basis.PROXY,
  binding: {
    loading: Value.query('childNodesState').as(state => state == STATE.PROCESSING),
    error: Value.query('childNodesState').as(state => state == STATE.ERROR && state.data),
    empty: node => new Expression(
      Value.query(node, 'childNodesState'),
      Value.query(node, 'childNodes.length'),
      (state, itemCount) => !itemCount && state == STATE.READY
    )
  },
  handler: {
    activeChanged() {
      if (this.active) {
        this.dataSource.deprecate();
      }
    }
  }
});

Таким образом, страница будет активна только когда у нее есть активный потребитель.
Как было сказано выше — потреителем страницы будет являться компонент нашего приложения, а он, если помните, активен только когда пользователь авторизован.
В момент активации страницы, состояние ее источника данных переходит в DEPRECATED.
Так мы делегировали обязанности по обработке различных кейсов основному приложению.

Теперь картина должна проясниться:

  • есть три страницы (новости, друзья, аудиозаписи)
  • у каждой старницы свой собственный набор данных, который умеет получать данные при помощи VK API
  • при переключении между вкладками, сателлитом основного компонента приложения будет становиться соответствующая вкладке страница
  • страница активна только когда у нее есть активный потребитель

Теперь перейдем к созданию наследников рассмотренного компонента страницы.
Начнем с новостей:

let Page = require('../Page');

module.exports = new Page({
  template: resource('./list.tmpl'),
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      text: 'data:',
      date: Value.query('data.date').as(format('%D.%M.%Y %H:%I:%S'))
    }
  }
});

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

// ...
let pageByName = {
  news: resource('./ui/pages/news/component.js'),
  friends: resource('./ui/pages/friends/component.js'),
  audio: resource('./ui/pages/audio/component.js')
};

require('basis.app').create({
  title: 'VK Client by Basis.JS',
  element: new Node({
    // ...
    binding: {
      // ...
      page: 'satellite:'
    },
    satellite: {
      // ...
      page: router.route(':page').param('page').as(page => pageByName[page])
    }
  })
})
// ...

Сателлитом с именем page будет являться компонент, который соответствует текущему маршруту на основании карты в переменной pageByName. Теперь нужно добавить использование этого сателлита в шаблон:

<div class="container">
  <div b:show="{active}">
    <!--{header}-->
    <!--{menu}-->
    <hr/>
    <!--{page}-->
  </div>
  ...
</div>

Теперь, если бы у страниц был источник данных, то наше приложение начало бы работать.

Источник данных

Выше была показана обертка над VK API. Помимо прочего, там есть методы для получения списка новостей, друзей и аудиозаписей. В качестве источника данных, будем использовать basis.entity — типизированные сущности.
Опишем тип для новостей:

let STATE = require('basis.data').STATE;
let entity = require('basis.entity');
let vkApi = require('app.vkApi');

let News = entity.createType('News', {
  text: String,
  date: Date
});

News.extendReader(data => data.date *= 1000);
News.all.setSyncAction(() => vkApi.news().then(News.all.set));

module.exports = News;

Каждая новость состоит из двух полей — текста и даты.
Заметьте, что мы расширяем reader. Данная возможность используется в тех случаях, когда необходимо модифицировать данные перед тем, как они станут экземпляром типа.
Так же, у каждого типа есть свойство all, которое является набором всех созданных объектов данного типа.
Где бы мы ни создали экземпляр типа News, он будет помещен в набор News.all.
Для данного набора мы определяем syncAction, то есть метод, который будет вызываться в случае необходимости синхронизации.
Всё что нам нужно сделать — получить данные из ВКонтакте и передать их методу News.all.set(), который заменит существующие экземпляры типа News на новые.
Заметьте, что нет необходимости явно указывать контекст метода таким образом: News.all.set.bind(News.all).
Данный метод уже имеет привязку к контексту News.all для удобства использования.
Так же заметьте, что, если метод, указанный в syncAction возвращает промис, то состояние набора данных будет определяться автоматически, в зависимости от состояния промиса.

Теперь News.all может быть передан в качестве источника данных для страницы новостей. Соответственно, в момент активации страницы, состояние News.all будет переведено в DEPRECATED и начнется процесс синхронизации, описанный в syncAction набора

News.all.
Подобным образом опишем оставшиеся два типа:

Friends entity

let entity = require('basis.entity');
let vkApi = require('app.vkApi');

let Friends = entity.createType('Friends', {
  photo: String,
  first_name: String,
  last_name: String
});

Friends.extendReader(data => data.photo = data.photo_100);
Friends.all.setSyncAction(() => vkApi.friends().then(Friends.all.set));

module.exports = Friends;

Audio entity

let entity = require('basis.entity');
let vkApi = require('app.vkApi');

let Audio = entity.createType('Audio', {
  artist: String,
  title: String,
  duration: Date
});

Audio.extendReader(data => data.duration *= 1000);
Audio.all.setSyncAction(() => vkApi.audio().then(Audio.all.set));

module.exports = Audio;

Теперь укажем News.all в качестве источника данных для страницы:

let Value = require('basis.data').Value;
let Page = require('../Page');
let News = require('app.type.news');
let format = require('basis.date').format;
let dataSource = News.all;

module.exports = new Page({
  template: resource('./list.tmpl'),
  dataSource: dataSource,
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      text: 'data:',
      date: Value.query('data.date').as(format('%D.%M.%Y %H:%I:%S'))
    }
  }
});

Аналогичным образом, укажем соответствующие наборы другим страницам.

Friends page

let Page = require('../Page');
let Friends = require('app.type.friends');
let dataSource = Friends.all;

module.exports = new Page({
  template: resource('./list.tmpl'),
  dataSource: dataSource,
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      photo: 'data:',
      first_name: 'data:',
      last_name: 'data:'
    }
  }
});

Audio page

let Value = require('basis.data').Value;
let Page = require('../Page');
let Audio = require('app.type.audio');
let format = require('basis.date').format;
let dataSource = Audio.all;

module.exports = new Page({
  template: resource('./list.tmpl'),
  dataSource: dataSource,
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      artist: 'data:',
      title: 'data:',
      duration: Value.query('data.duration').as(format('%I:%S'))
    }
  }
});

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

Всё готово, но давайте добавим еще немного улучшений.
Добавим страницу 404. Для этого, модифицируем наш главный файл:

// ...
let pageByName = {
  news: resource('./ui/pages/news/component.js'),
  friends: resource('./ui/pages/friends/component.js'),
  audio: resource('./ui/pages/audio/component.js'),
  notFound: resource('./ui/pages/404/component.js')
};

require('basis.app').create({
  title: 'VK Client by Basis.JS',
  element: new Node({
    satellite: {
      header: Header,
      menu: Menu,
      page: router.route(':page').param('page').as(page => pageByName[page] || pageByName.notFound)
    }
    // ...
  })
})
// ...

Всё что мы сделали — добавили новый маршрут в карту маршрутов и модифицировали отслеживание изменения маршрута. Если требуемый маршрут не найден в карте маршрутов, то использовать маршрут notFound.

Делаем крутые Single Page Application на basis.js — часть 3. Клиент для «ВКонтакте» - 6

Кстати, вы заметили, что компоненты подключаются через resource а не через require?
resource позволяет реализовать ленивую инициализацию компонентов.
То есть компонент будет проинициализирован не сразу, а только в тот момент, когда он понадобится в первый раз.
Подробнее о ресурсах можно почитать в соответствующем разделе документации.

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

let Page = require('../Page');
let Value = require('basis.data').Value;
let News = require('app.type.news');
let format = require('basis.date').format;
let Filter = require('basis.data.dataset').Filter;
let textOnlyNews = new Filter({
  source: News.all,
  state: Value.query('source.state'),
  rule: 'data.text',
  deprecate() {
    this.source.deprecate();
  }
});

module.exports = new Page({
  template: resource('./list.tmpl'),
  dataSource: textOnlyNews,
  childClass: {
    template: resource('./item.tmpl'),
    binding: {
      text: 'data:',
      date: Value.query('data.date').as(format('%D.%M.%Y %H:%I:%S'))
    }
  }
});

Всё что мы сделали — подменили источник данных страницы новостей фильтром, который отбросит все новости без текста.

И последнее… оживим компонент заголовка:

let Node = require('basis.ui').Node;
let STATE = require('basis.data').STATE;
let DataObject = require('basis.data').Object;
let vkApi = require('app.vkApi');

let dataSource = new DataObject({
  data: {
    firstName: '',
    lastName: ''
  },
  syncAction() {
    return vkApi.me().then(me => {
      this.update({ firstName: me.first_name, lastName: me.last_name });
    });
  }
});

module.exports = Node.subclass({
  active: basis.PROXY,
  delegate: dataSource,
  template: '<h1>Добро пожаловать {firstName} {lastName}!</h1>',
  binding: {
    firstName: 'data:',
    lastName: 'data:'
  }
});

Таким образом, компонент заголовка научился получать и отображать имя и фамилию владельца аккаунта.

Заключение, но не конец

Итак, сегодня мы создали полноценный клиент для ВКонтакте на basis.js. Приложение умеет авторизоваться и синхронизировать данные с сервером.
Обратите внимание на то, что, как и в прошлый раз, мы идем по пути FRP и основной упор делаем на работу с данными. Другими словами: мы выстраиваем поток данных таким образом, чтобы приложение выполняло свои задачи. При этом, специфика basis.js такова, что клиентский код получается довольно линейным из-за отсутствия циклов и большого количества ветвлений. По крайней мере обозначенные задачи можно решить без них.
В следующих статьях мы будем улучшать наш клиент и наращивать его функционал.
Спасибо за интерес к basis.js!

Огромная благодарность lahmatiy за бесценные советы ;)

Несколько полезных ссылок:

Автор: smelukov

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js