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

в 10:46, , рубрики: api, JS, QML, qt, rest, SDK, Ubuntu, ubuntu.sdk, Yandex, Убунтариум, Яндекс API

Доброго времени суток, друзья!
В последнее время на хабре совсем перестали появляться статьи на тему QtQuickQML Про 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 (!) строк кода на 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

Источник

Поделиться

* - обязательные к заполнению поля