Про QML и новое REST API Яндекс.Диска

от автора

Доброго времени суток, друзья!
В последнее время на хабре совсем перестали появляться статьи на тему QtQuick\QML Про Ubuntu SDK (основанном на QtQuick) и вовсе тишина, а ведь в настоящий момент это основной инструментарий, предлагаемый для разработки приложений под Ubuntu (ни много ни мало самый популярный Linux-дистрибутив). Захотелось в меру своих возможностей исправить эту ситуацию с помощью написания данной статьи! Объять необъятное пытаться не стоит, поэтому начну, пожалуй, с повествования о том, как мне удалось заменить большой объем кода на C++ кодом на QML (в приложении под Ubuntu SDK). Если вам стало интересно, а может быть еще и непонятно, причем тут Яндекс.Диск, то прошу под кат!
image

Вступление

Начну издалека, но постараюсь кратко — несколько лет назад мне захотелось создать клиент какого-нибудь облачного хранилища под MeeGo (!). Так сложилось, что именно в тот момент Яндекс.Диск открыл свой API. Я достаточно быстро реализовал WebDAV API сервиса c помощью С++\Qt, а GUI с помощью QML. Получилось довольно неплохо — простая и надежная программа, большинство отзывов положительные (ну кроме тех, кто не сообразил, как залогиниться =\ ).
Спустя некоторое время я решил поучаствовать в OpenSource разработке базовых приложений для Ubuntu Phone — так я познакомился с Ubuntu SDK, работая над RSS Reader’ом «Shorts». А тем временем приближался Ubuntu App Showdown. Я решил поучаствовать со своим клиентом в категории «Портированные приложения» (можно портировать с любой ОС), благо переносить код с MeeGo на Ubuntu Phone фактически тривиально. Я бы забрал призовой девайс, если бы в тот момент мне уже не выслали один Nexus 4 как Core App Developer’у, второй за конкурс показался им перебором, меня сняли с участия, победила дурацкая змейка из example’ов Qt. Тем не менее, в результате получился отличный клиент Яндекс.Диска под Ubuntu Phone. Однако у него был и недостаток — C++ часть собиралась под ARM только, в итоге на уровне пакета терялась кроссплатформенность.
И совсем недавно мне на почту пришло уведомление от Яндекса о выходе в продакшн нового REST API Диска. Я сразу же задумался о реализации этого API на чистом JavaScript. Для тех, кто не знает — QML (не особо строго говоря) включает в себя JavaScript, то есть позволяет использовать все фичи этого языка, в совокупности с возможностями библиотеки Qt (свойства, сигналы и т.д., в результате получается довольно мощная и гибкая комбинация). В результате получилась бы полностью кроссплатформенная реализация клиента Яндекс.Диска (для всех платформ, где есть Qt, конечно же).

Исходные данные и цели

Итак, имеется готовое приложение, позволяющее выполнять различные операции над содержимым Яндекс.Диска (копирование, перемещение, удаление, получение публичных ссылок и т.д.). Сетевая часть выполнена с помощью C++\Qt, так же как и хранение модели отображаемых данных. Задача — перейти на новое API сервиса, реализовав его уже на JavaScript и не делая правок в коде UI.
image

Реализация REST API

Я выработал для себя простую технику реализации API веб-сервиса. Она заключается в использовании экстремально легковесного типа QtObject с кастомным набором свойств и методов. Схематично это выглядит следующим образом:

QtObject {     id: yadApi      signal responseReceived(var resObj, string code, int requestId)      property string clientId: "2ad4de036f5e422c8b8d02a8df538a27"     property string clientPass: ""     property string accessToken: ""     property int expiresIn: 0      // Public methods...     // Private methods... } 

Сигнал «responseReceived» высылается объектом API каждый раз, когда приходит асинхронный ответ от XMLHttpRequest (см. далее). Свойства «accessToken» и «expiresIn» выставляются после прохождения авторизации через OAuth извне (на странице входа для этой задачи используется WebView — он запрашивает у yadApi URL для получения токена, переходит по нему, предлагает пользователю ввести свои данные, в случае успеха получает токен и его время жизни).
А вот один из публичных методов API — удаление файла:

function remove(path, permanently) {         if (!path)             return         var baseUrl = "https://cloud-api.yandex.net/v1/disk/resources?path=" + encodeURIComponent(path)         if (permanently)             baseUrl += "&permanently=true"         return __makeRequst(baseUrl, "remove", "DELETE")     } 

Он очень простой — из переданных параметров формируется URL запроса, а затем передается во внутренний метод __makeReuqest. Он выглядит так:

function __makeRequst(request, code, method) {         method = method || "GET"          var doc = new XMLHttpRequest()         var task = {"code" : code, "doc" : doc, "id" : __requestIdCounter++}          doc.onreadystatechange = function() {             if (doc.readyState === XMLHttpRequest.DONE) {                 var resObj = {}                 if (doc.status == 200) {                     resObj.request = task                     resObj.response = JSON.parse(__preProcessData(code, doc.responseText))                 } else { // Error                     resObj.request = task                     resObj.isError = true                     resObj.responseDetails = doc.statusText                     resObj.responseStatus = doc.status                 }                 __emitSignal(resObj, code, doc.requestId)             }         }          doc.open(method, request, true)         doc.setRequestHeader("Authorization", "OAuth " + accessToken)         doc.send()          return task     }  

В вышеуказанном куске кода можно увидеть обещанный XMLHttpRequest, а так же отправку сигнала по получению результата. Помимо этого формируется объект запроса — это код операции, идентификатор и сам XMLHttpRequest. В дальнейшем он может использоваться для отмены, обработки результата и т.д. Если вдруг кому станет интересно насчет "__emitSignal" — он реализован тривиально:

function __emitSignal(resObj, operationCode, requestId) {         responseReceived(resObj, operationCode, requestId)     } 

Такой код может использоваться для логгирования и перехвата отправки сигналов. Что касается внутренней функции "__preProcessData" — она ничего (!) не делает, это закладка на будущее. Дело в том, что я в этом плане научен горьким опытом — при работе со Steam API в JSON’e ответов иногда приходят 64-х битные числа, притом они не заключены в кавычки. В результате JavaScript воспринимает их как double, теряется точность и да здравствует грусть печаль! Решением стал препроцессинг входящих данных, заключение чисел в кавычки, а так же последующая работа с ними уже как со строками.
И по большому счету это все — один за другим были реализованы все необходимые мне методы API, а именно создание папки, копирование, перемещение, удаление, загрузка, изменение статуса публичности. В сумме получилось 140 (!) строк кода на QML\JS, которые в функциональном плане полностью заменили собой тысячу другую строк кода на C++\Qt реализации протокола WebDAV.

Реализация прослойки

Реализация протокола WebDAV на C++ у меня получилась достаточно простой и прозрачной, однако ее неудобно было использовать напрямую из QML. В старой версии качестве посредника был создан специальный класс Bridge (название а-ля КО), позволяющий упростить работу с сервисом. Я решил не отказываться от этого подхода в новой версии и аккуратно подменить свой старый Bridge новым одноименным QML типом с идентичным набором методов и свойств. Поддержать свой же API, так сказать, UI бы продолжал вызывать те же самые функции, но абсолютно другой сущности. Опять же схематично это выглядит следующим образом:

QtObject {     id: bridgeObject      property string currentFolder: "/"     property bool isBusy: taskCount > 0      property int taskCount: 0     property var tasks: []      function slotMoveToFolder(folder) {         if (isBusy)             return          // .... code     }      function slotDelete(entry) {         __addTask(yadApi.remove(entry))     }      property QtObject yadApi: YadApi {         id: yadApi          onResponseReceived: {             __removeTask(resObj.request)              switch (resObj.request.code)             {             case "metadata":                 // console.log(JSON.stringify(resObj))                 if (!resObj.isError) {                     var r = resObj.response                     currentFolder = __checkPath(r.path)                      // Filling model                 } // !isError                 break;             case "move":             case "copy":             case "create":             case "delete":             case "publish":             case "unpublish":                 __addTask(yadApi.getMetaData(currentFolder))                 break;     } // API      property ListModel folderModel: ListModel {         id: dirModel     } } 

Итак, для подмены своего же класса мне были нужны свойства «currentFolder» и «isBusy». Первое свойство используется для хранения пути текущего каталога при навигации. Оно поддерживается актуальным в методе «slotMoveToFolder». Так же добавились несколько свойств и методов для учета выполняемых запросов (__addTask, __removeTask, массив tasks и его длина taskCount. Только не надо сейчас быть КО и говорить, что у массива есть длина и так — свойство позволяет делать binding’и в QML, в данном случае используется только в isBusy, в перспективе еще где-то). Именование функций оставил как раньше — начиная с приставки «slot» (в C++ версии класса можно было добиться видимости методов из QML двумя способами: сделать их слотами либо использовать Q_INVOKABLE). Для краткости опять же оставил только метод удаления и перехода в указанную директорию, все остальные так же присутствуют в полной версии исходного кода. Методы типа Bridge вызываются напрямую из UI.
Одним из свойств нового Bridge является описанная выше реализация API — YadApi. Так же по месту создания выполняется прослушивание сигналов о завершении операции с выполнением соответствующих действий. Так, переименование или удаление, например, вызывают перезагрузку содержимого каталога.
Отдельного внимания заслуживает модель данных — dirModel. В предыдущей реализации у меня был класс FolderModel, который наследовался от QAbstractItemModel по классическому сценарию — введение собственных ролей (кто знаком с Qt хоть немного поймут о чем речь) и так далее. Сейчас же от этого всего удалось с легкостью отказаться в пользу стандартной ListModel, умеющей хранить объекты JS. Заполняется эта модель следующим образом:

dirModel.clear() var items = r._embedded.items for(var i = 0; i < items.length; i++) {     var itm = items[i]     var o = {         /* All entries attributes */         "href" : __checkPath(itm.path),         "isFolder" : itm.type == "dir",         "displayName" : itm.name,         "lastModif" : itm.modified,         "creationDate" : itm.created,         /* Custom attributes */         "contentLen" : itm.size ? itm.size : 0,         "contentType" : itm.mime_type ? itm.mime_type : "",         "publicUrl" : itm.public_url ? itm.public_url : null,         "publicKey" : itm.public_key ? itm.public_key : null,         "isPublished" : itm.public_key ? true : false,         "isSelected" : false,         "preview" : itm.preview     }      dirModel.append(o) } 

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

Заключение

В конечном итоге я полностью отказался от C++ в своем клиенте Яндекс.Диска. Я ни в коем случае не клоню к тому, что в плюсах есть что-то плохое или в таком духе. Нет! Целью моей статьи было показать возможности чистого QML — с его помощью можно сделать действительно много, хотя его первостепенная задача есть разработка UI (в данной статье фактически не затронутая). И выглядит код просто и понятно, совсем не так как реализация калькулятора на CSS!
Спасибо за внимание! Код можно найти на launchpad’e.

P.S.Вопросы приветствуются, по желанию могу раскрыть любую часть статьи более детально!
P.S.S. В следующей статье планирую затронуть ключевые аспекты и инструменты Ubuntu SDK.

ссылка на оригинал статьи http://habrahabr.ru/post/230435/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *