Доброго времени суток, друзья!
В последнее время на хабре совсем перестали появляться статьи на тему QtQuick\QML Про 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 фактически тривиально. Победить не удалось по техническим причинам. Тем не менее, в результате получился отличный клиент Яндекс.Диска под Ubuntu Phone. Однако у него был и недостаток — C++ часть собиралась под ARM только, в итоге на уровне пакета терялась кроссплатформенность.
И совсем недавно мне на почту пришло уведомление от Яндекса о выходе в продакшн нового REST API Диска. Я сразу же задумался о реализации этого API на чистом JavaScript. Для тех, кто не знает — QML (не особо строго говоря) включает в себя JavaScript, то есть позволяет использовать все фичи этого языка, в совокупности с возможностями библиотеки Qt (свойства, сигналы и т.д., в результате получается довольно мощная и гибкая комбинация). В результате получилась бы полностью кроссплатформенная реализация клиента Яндекс.Диска (для всех платформ, где есть Qt, конечно же).
Итак, имеется готовое приложение, позволяющее выполнять различные операции над содержимым Яндекс.Диска (копирование, перемещение, удаление, получение публичных ссылок и т.д.). Сетевая часть выполнена с помощью C++\Qt, так же как и хранение модели отображаемых данных. Задача — перейти на новое API сервиса, реализовав его уже на JavaScript и не делая правок в коде UI.
Я выработал для себя простую технику реализации API веб-сервиса. Она заключается в использовании экстремально легковесного типа QtObject с кастомным набором свойств и методов. Схематично это выглядит следующим образом:
Сигнал «responseReceived» высылается объектом API каждый раз, когда приходит асинхронный ответ от XMLHttpRequest (см. далее). Свойства «accessToken» и «expiresIn» выставляются после прохождения авторизации через OAuth извне (на странице входа для этой задачи используется WebView — он запрашивает у yadApi URL для получения токена, переходит по нему, предлагает пользователю ввести свои данные, в случае успеха получает токен и его время жизни).
А вот один из публичных методов API — удаление файла:
Он очень простой — из переданных параметров формируется URL запроса, а затем передается во внутренний метод __makeReuqest. Он выглядит так:
В вышеуказанном куске кода можно увидеть обещанный XMLHttpRequest, а так же отправку сигнала по получению результата. Помимо этого формируется объект запроса — это код операции, идентификатор и сам XMLHttpRequest. В дальнейшем он может использоваться для отмены, обработки результата и т.д. Если вдруг кому станет интересно насчет "__emitSignal" — он реализован тривиально:
Такой код может использоваться для логгирования и перехвата отправки сигналов. Что касается внутренней функции "__preProcessData" — она ничего (!) не делает, это закладка на будущее. Дело в том, что я в этом плане научен горьким опытом — при работе со Steam API в JSON'e ответов иногда приходят 64-х битные числа, притом они не заключены в кавычки. В результате JavaScript воспринимает их как double, теряется точность и да здравствует грусть печаль! Решением стал препроцессинг входящих данных, заключение чисел в кавычки, а так же последующая работа с ними уже как со строками.
И по большому счету это все — один за другим были реализованы все необходимые мне методы API, а именно создание папки, копирование, перемещение, удаление, загрузка, изменение статуса публичности. В сумме получилось 140 (!) строк кода на QML\JS, которые в функциональном плане полностью заменили собой тысячу другую строк кода на C++\Qt реализации протокола WebDAV.
Реализация протокола WebDAV на C++ у меня получилась достаточно простой и прозрачной, однако ее неудобно было использовать напрямую из QML. В старой версии качестве посредника был создан специальный класс Bridge (название а-ля КО), позволяющий упростить работу с сервисом. Я решил не отказываться от этого подхода в новой версии и аккуратно подменить свой старый Bridge новым одноименным QML типом с идентичным набором методов и свойств. Поддержать свой же API, так сказать, UI бы продолжал вызывать те же самые функции, но абсолютно другой сущности. Опять же схематично это выглядит следующим образом:
Итак, для подмены своего же класса мне были нужны свойства «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. Заполняется эта модель следующим образом:
Имена свойств в модели тоже пришлось оставить как в старой версии для совместимости. Нельзя сказать, что в C++ реализации модели у меня получился очень уж большой класс, но избавиться от него с помощью стандартной модели и такой вот маленькой конструкции очень даже приятно!
В конечном итоге я полностью отказался от C++ в своем клиенте Яндекс.Диска. Я ни в коем случае не клоню к тому, что в плюсах есть что-то плохое или в таком духе. Нет! Целью моей статьи было показать возможности чистого QML — с его помощью можно сделать действительно много, хотя его первостепенная задача есть разработка UI (в данной статье фактически не затронутая). И выглядит код просто и понятно, совсем не так как реализация калькулятора на CSS!
Спасибо за внимание! Код можно найти на launchpad'e.
P.S.Вопросы приветствуются, по желанию могу раскрыть любую часть статьи более детально!
P.S.S. В следующей статье планирую затронуть ключевые аспекты и инструменты Ubuntu SDK.
В последнее время на хабре совсем перестали появляться статьи на тему QtQuick\QML Про 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 фактически тривиально. Победить не удалось по техническим причинам. Тем не менее, в результате получился отличный клиент Яндекс.Диска под 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 (!) строк кода на 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 (в данной статье фактически не затронутая). И выглядит код просто и понятно
Спасибо за внимание! Код можно найти на launchpad'e.
P.S.Вопросы приветствуются, по желанию могу раскрыть любую часть статьи более детально!
P.S.S. В следующей статье планирую затронуть ключевые аспекты и инструменты Ubuntu SDK.