Про 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.

© Habrahabr.ru