Доброго времени суток, друзья!
В последнее время на хабре совсем перестали появляться статьи на тему QtQuickQML Про Ubuntu SDK (основанном на QtQuick) и вовсе тишина, а ведь в настоящий момент это основной инструментарий, предлагаемый для разработки приложений под Ubuntu (ни много ни мало самый популярный Linux-дистрибутив). Захотелось в меру своих возможностей исправить эту ситуацию с помощью написания данной статьи! Объять необъятное пытаться не стоит, поэтому начну, пожалуй, с повествования о том, как мне удалось заменить большой объем кода на C++ кодом на QML (в приложении под Ubuntu SDK). Если вам стало интересно, а может быть еще и непонятно, причем тут Яндекс.Диск, то прошу под кат!
Вступление
Начну издалека, но постараюсь кратко — несколько лет назад мне захотелось создать клиент какого-нибудь облачного хранилища под 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.
Реализация 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 (!) строк кода на QMLJS, которые в функциональном плане полностью заменили собой тысячу другую строк кода на 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.
Автор: QtRoS