Про 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 = "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 .
 
PSВопроси вітаються, за бажанням можу розкрити будь-яку частину статті більш детально!
P.S.S. У наступній статті планую торкнутися ключові аспекти та інструменти Ubuntu SDK.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.