...

воскресенье, 20 июля 2014 г.

Про 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 = "http://ift.tt/1u5ggRS" + 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.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий