Про 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 фактически тривиально. Победить не удалось по техническим причинам. Тем не менее, в результате получился отличный клиент Яндекс.Диска под 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.
    Поделиться публикацией

    Похожие публикации

    Комментарии 15

      0
      массив tasks и его длина taskCount. Только не надо сейчас быть КО и говорить, что у массива есть длина и так — свойство позволяет делать binding'и в QML

      Если что-то постоянно добавляется/удаляется и это нужно отслеживать — зачем массив? Можно взять ListModel. И привязки будут работать.
        0
        Совесть не позволяет использовать целую модель для такой мелочи! А вот сделать самому небольшую обертку над массивом — идея неплохая, как мне кажется, спасибо за подсказку)
        0
        А с чего это " C++ часть собиралась под ARM только, в итоге на уровне пакета терялась кроссплатформенность."? ) Непонятно… Тем более, что в кроссплатформенной (по вашим словам) реализации всё равно есть C++ код, пусть и не ваш.
          +1
          В *.click пакетах для Ubuntu нету C++ кода (если Вы имеете ввиду код стандартного QtQuick2ApplicationViewer'a), там просто упаковываются ресурсы, *.qml и прочее, а на девайсах запускается с помощью qmlscene, которая есть в бинарном виде на всех поддерживаемых SDK платформах!
            0
            Ну т.е. вы просто используете C++ библиотеку в предкомпилированном виде. Но мой вопрос то был не про это, а про проблемы со сборкой только под ARM. Вот откуда это взялось я так и не понял.
              0
              Не совсем понял, что имелось ввиду под словом «предкомпилированном»…
              Как таковых проблем со сборкой под ARM нет (если не считать того, что собирать приходится на девайсе).
              Ранее в пакете лежала *.so под ARM, то бишь под десктопом (x86 и x64) тот же пакет по идее не заработал бы. А в новом подходе простой перенос *.qml файлов под любую платформу будет работать — такая была идея!
                0
                А, ну да, Убунта же. Я привык, что в Андроиде в пакете идут бинарники сразу под всех архитектуры процессоров. Но под Debian тоже не проблема — просто делается 3-4 отдельных пакета. Естественно автоматом (настраиваем один раз сборочный скрипт).

                Да, и кстати кросскомпиляция под арм как раз вообще стандартное дело — незачем насиловать дохленькое устройство. )

                А так в целом ваша идея и реализация безусловно понятны и имеют смысл. Но без аргументации на тему проблем кроссплатформенности C++. )
          +2
          Спасибо за грамотно составленные тэги, что большая редкость для Хабра! Не пришлось добавлять свои… :)
          Считаю, что многим бы стоило брать за пример…
            –1
            Когда кто-то пытается реализовать всю логику на кумле, да еще и с поддержкой сети — бог убивает одного котенка.
              +1
              Тогда второе имя всех приложений под Ubuntu Phone — долой котят! :)

              Для небольших приложений вполне себе живучий вариант тем не менее… В моем случае из кода не получилось спагетти, UI изменений и вовсе не почувствовал, много процессорного времени и памяти из-за JS не расходуется зря, нет работы со сложными структурами данных или большими их объемами — по этой совокупности причин я считаю, что получилось неплохо)
                +1
                Бога нет! Котята в безопасности! ;)
                  0
                  tass, в чём основые минусы в таком подходе? Медленно? Тяжело поддерживаемый код?
                    0
                    медленно. Тяжело поддерживаемый код. Нет той гибкости, которая есть в плюсах.
                  0
                  Автор, код на launchpad'e больше не доступен. Не могли бы Вы переложить куда-нибудь?
                    0
                    Обновил ссылку, спасибо за замечание!
                    Ранее была отдельная ветка, недавно я ее слил в транк, а старую удалил, отсюда битая ссылка… Проект, кстати, живет — добавляю функционал по мере возможности, раннюю Android версию выложил, можно посмотреть в Google play.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое