Интеграция приложения на QML с веб-ресурсами

от автора

Доброго времени суток, дорогой хабражитель! Я хочу рассказать, как интегрировать программу на новомодном языке QML с веб-ресурсами.

Сам по себе, QML — это декларативный JavaScript-подобный язык программирования, который входит в фреймворк Qt. Разработчики Qt настроены серьезно и продвигают его как основной инструмент создания интерфейсов. Более того, достаточно много вещей можно сделать не прибегая вообще к C++, в том числе и возможность работы с веб-серверами.

Веб-технологии все сильнее проникают в нашу жизнь, мы часто пользуемся различными веб-ресурсами. Не всегда удобно для этого запускать браузер, иногда отдельное приложение-клиент гораздо удобнее, о чем красноречиво говорит, например, количество клиентов для различных социальных сетей, особенно на мобильных платформах.

Учитывая, что в Qt 5.1, альфа версия которой вышла на этой неделе, включена начальная поддержка Android и iOS, эта тема может быть особенно интересна тем, кто присматривается к Qt или активно ее осваивает. В этой статье я расскажу, как можно организовать работу с веб-ресурсами из приложения на QML на примере API ВКонтакте.

На всякий случай отмечу, что я рассматриваю последнюю стабильную версию Qt 5.0.2. В более ранних версиях каких-то возможностей может не быть.

Что такое XMLHttpRequest и зачем он нужен

Наверняка, многие из читателей слышали про такую технологию, как AJAX (Asynchronous JavaScript And XML). Она позволяет отправлять асинхронные запросы на сервер и обновлять содержимое страницы без ее перезагрузки. В современных браузерах есть различные средства для этого, XMLHttpRequest является одним из них. Поскольку QML является JavaScript-подобным языком и JavaScript окружение в нем похоже на браузерное, то и XMLHttpRequest тоже присутствует. Далее в тексте я буду также записывать его название в сокращенной форме — XHR.

Собственно, что это такое и что оно нам дает? Это инструмент для асинхронных (в браузерах поддерживаются также и синхронные) HTTP-запросов. Несмотря на свое название, он позволяет передавать данные не только в формате XML, хотя изначально был предназначен именно для этого. Реализация в движке QML поддерживает следующие HTTP-запросы: GET, POST, HEAD, PUT и DELETE. В основном, мы будем пользоваться первыми двумя.

Отличительной особенностью реализации XHR в QML является то, что запросы можно отправлять на любой хост, здесь нет таких ограничений, как в браузере.

Процедура работы с XMLHttpRequest

Процесс работы с XHR выглядит следующим образом.

1. Создаем объект XHR:

var request = new XMLHttpRequest() 

2. Инициализируем объект, указывая тип запроса (он же HTTP-метод), адрес и, если нужно, параметры запроса [1], которые нужно передать серверу:

request.open('GET', 'http://site.com?param1=value1¶m2=value2') 

Первым параметром передаем тип запроса, вторым — URL. Для GET-запроса параметры нужно передать здесь же, отделив их от адреса символом ‘?’. Параметры разделяются символом ‘&’.

Для POST-запроса нужно указать тип содержимого. Если мы передаем данные параметрами запроса, то делается это следующим образом:

request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') 

3. Устанавливаем обработчик на смену состояния запроса. В большинстве случаев, нам надо просто дождаться, пока запрос не завершится и затем выполнить обработку результата либо ошибок. При завершении запроса параметр readyState будет равен XMLHttpRequest.DONE (подробнее про значения см. [2]).

request.onreadystatechange = function () {     if (request.readyState === XMLHttpRequest.DONE) {         if (request.status === 200) {             console.log(request.responseText)         } else {             console.log("HTTP request failed", request.status)         }     } } 

Наша анонимная функция будет вызывать при каждом изменении свойства readyState. Нас интересует завершение запроса, после которого мы проверяем, успешно ли он выполнился. Для этого мы сверяем код его статус с кодом успешного завершения (200). HTTP является текстовым протоколом и помимо числовых значений кодов, передается еще и текстовое описание, так что можно сравнивать свойство statusText со строкой, соответствующей этому статусу, в данном случае, это строка «OK»:

if (request.statusText === 'OK') 

В случае ошибки, status и statusText будут содержать код и текстовое описание кодов состояния HTTP (например, 404 и «Not Found» соответственно).

4. Отправляем запрос.

request.send() 

В случае POST, здесь же нужно передать параметры запроса:

request.send(param1=value1¶m2=value2) 

В параметрах запроса можно передавать далеко не все символы. Поэтому и параметр и значение стоит кодировать и, если надо, соответственно декодировать специальными функциями — encodeURIComponent() и decodeURIComponent(). Пример использования:

request.send(encodeURIComponent(param)=encodeURIComponent(value)) 

Рекомендуется закодированную строку еще дополнительно обработать и заменить последовательность "%20" (т.е. закодированный пробел) на символ ‘+’. Перед декодированием, соответственно, сделать наоборот.

Обычно параметрами запроса передаются значения простых типов. Можно передать и массив, но синтаксис несколько мутный. Например, отправка массива params из двух значений будет выглядеть так:

request.send(params[]=value1¶ms[]=value2) 

Если изловчиться, то можно в качестве значений передавать даже объекты (!), но это может быть не совсем надежно, в том плане, что на принимающей стороне он может превратится в массив 🙂

Используя POST-запросы мы можем передавать данные не только параметрами запроса но и в самом теле запроса. Например, можно отправить данные в формате JSON. Для этого нужно установить правильный Content-Type и размер содержимого (Content-Length). Пример отправки такого запроса:

request.setRequestHeader('Content-Type', 'application/json') var params = {     param1: value1,     param2: value2 } var data = JSON.stringify(params) request.setRequestHeader('Content-Length', data.length) request.send(data) 

Здесь JSON — это глобальный объект доступный в QML, предоставляющий средства по работе с данным форматом [3].

Фактически, формат, в котором мы можем передавать данные, определяется сервером. Если он принимает JSON — отлично, шлем JSON. Ожидает, что данные придут параметрами запроса — значит так и надо отправлять.

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

Получение и отображение списка друзей

Для начала рассмотрим простой пример с методами, не требующими авторизации и других лишних телодвижений. Получение списка друзей попадает в эту категорию. Напишем простую программу, при старте отправляющую XHR на получение списка друзей и после его получения отображающую имена пользователей и их аватарки.

Большую часть кода составляет интерфейс отображения и его особо описывать нет смысла. Отмечу только, что если в качестве модели используется JavaScript объект или массив, то для получения к данным модели используется modelData вместо model.

Наиболее интересная часть здесь — это работа с сервером. Для доступа к API ВКонтакте есть специальный адрес: api.vk.com/method/. К полученному адресу мы добавляем название метода (список методов можно посмотреть в [4]), в нашем случае это метод friends.get. На этот адрес нужно отправить POST или GET-запрос с необходимыми параметрами. Ответ придет в формате JSON. Нам нужно в параметре uid передать ID пользователя. Также в параметре fields передадим еще photo_medium, чтобы получить фото и чтобы оно было не самого маленького размера.

Ниже собственно исходный текст программы. В качестве userId в main задается ID пользователя.

import QtQuick 2.0  Rectangle {     id: main      property int userId: XXX     property var friends      width: 320     height: 640     color: 'skyblue'      function getFriends() {         var request = new XMLHttpRequest()         request.open('POST', 'https://api.vk.com/method/friends.get')         request.onreadystatechange = function() {             if (request.readyState === XMLHttpRequest.DONE) {                 if (request.status && request.status === 200) {                     console.log("response", request.responseText)                     var result = JSON.parse(request.responseText)                     main.friends = result.response                 } else {                     console.log("HTTP:", request.status, request.statusText)                 }             }         }         request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')         request.send("fields=photo_medium&uid=%1".arg(main.userId))     }      ListView {         id: view          anchors.margins: 10         anchors.fill: parent         model: friends         spacing: 10          delegate: Rectangle {             width: view.width             height: 100             anchors.horizontalCenter: parent.horizontalCenter             color: 'white'             border {                 color: 'lightgray'                 width: 2             }             radius: 10              Row {                 anchors.margins: 10                 anchors.fill: parent                 spacing: 10                  Image {                     id: image                      height: parent.height                     fillMode: Image.PreserveAspectFit                     source: modelData['photo_medium']                 }                  Text {                     width: parent.width - image.width - parent.spacing                     anchors.verticalCenter: parent.verticalCenter                     elide: Text.ElideRight                     renderType: Text.NativeRendering                     text: "%1 %2".arg(modelData['first_name']).arg(modelData['last_name'])                 }             }         }     }      Component.onCompleted: {         getFriends()     } } 

Я сделал вывод в консоль того, что придет в ответе, это удобно, если возникнет желание поиграться с этим примером.

Запустив программу, если был указан действительный ID, мы получим примерно такую картину:

Самая большая сложность здесь именно в работе с XHR. Попробуем разобраться с этим и немного упростить код.

Упрощение работы с XMLHttpRequest

В работе с XHR есть две сложности.

1. При передаче данных параметрами запроса, этот запрос нужно составлять. В случае, если эти параметры могут меняться, то скорее всего в коде будет много операций, склеивающие параметры запроса из кусочков. К тому же, нужно не забывать про то, что неплохо бы еще и при составлении ключи и значения кодировать при помощи encodeURIComponent, как я уже писал выше. Итого код, формирующий эти параметры может получиться громоздким и не очень понятным. Гораздо удобнее было бы в качестве параметров использовать объект, в котором установлены соответствующие поля.

Я написал небольшую библиотеку на JavaScript, которая преобразует объект в параметры запроса, все кодирует, в общем, выдает готовую строку, которую можно сразу отправлять. Также есть функция, которая декодирует параметры запроса и создает из них объект (но она поддерживает только простые типы, массив или объект в параметрах не распарсит, впрочем, вряд ли это понадобится). Взять можно здесь: github.com/krnekit/qml-utils/blob/master/qml/URLQuery.js.

2. В зависимости от типа запроса, данные отправлять нужно по-разному, да еще и может понадобиться устанавливать дополнительно заголовки. Я написал библиотеку, которая упрощает отправку XHR, предоставляя единый интерфейс. Она может отправлять данные в любом формате, для этого можно передать параметром тип содержимого, по умолчанию считается все тот же «application/x-www-form-urlencoded», при этом стоит помнить, что данные другого типа при помощи GET-запроса передавать нельзя, в таком случае нужно будет использовать POST. Content-Length так же автоматически посчитается и установится. Принимает типа запроса, URL, функцию-callback (опционально), которая будет вызвана при завершении запроса и тип данных (опционально). Функция возвращает сам объект запроса или null в случае ошибки. Взять можно здесь: github.com/krnekit/qml-utils/blob/master/qml/XHR.js

Используя две данные библиотеки я упростил предыдущий пример. Весь код здесь приводить не буду, рассмотрим только то, что изменилось.
В начале файла мы подключаем библиотеки (в данном примере, файлы библиотек лежат в том же каталоге, что и qml-файл):

import 'URLQuery.js' as URLQuery import 'XHR.js' as XHR 

Мы импортируем библиотеки и задаем для них пространства имен (namespace), через которые мы будем обращаться к функциям из библиотек.

Функция, отправляющая XHR выглядит теперь так:

function getFriends() {     var params = {         fields: 'photo_medium',         uid: main.userId     }      function callback(request) {         if (request.status && request.status === 200) {             console.log("response", request.responseText)             var result = JSON.parse(request.responseText)             main.friends = result.response         } else {             console.log("HTTP:", request.status, request.statusText)         }     }      XHR.sendXHR('POST', 'https://api.vk.com/method/friends.get', callback, URLQuery.serializeParams(params)) } 

Для начала мы определяем объект с параметрами запроса. Затем функцию-callback, которая вызовется при завершении запроса. Функция получает параметром сам запрос. И затем отправляем сам запрос, преобразовав объект с параметрами при помощи функции serializeParams.
В итоге, размер кода, можно сказать, не изменился, но зато он стал гораздо более структурированным и понятным.

Я буду эти функции применять в дальнейшем, чтобы код был проще. Если кому-то они пригодятся, можно брать и пользоваться, лицензия MIT.

Авторизация ВКонтакте из QML

Не все методы работают без авторизации, так что скорее всего, нам нужно будет авторизоваться. В результате мы должны получить т.н. Authorization Token, который затем будем передавать в запросах к ВКонтакте. Для того, чтобы мы могли авторизоваться, нужно создать в ВКонтакте приложение. Сделать это можно здесь: vk.com/editapp?act=create. Выбираем тип Standalone-приложение. Затем его ID мы будем передавать одним из параметров запроса.

1. Способы авторизации

Поскольку мы делаем standalone-приложение, то есть два способа авторизации, у обоих есть свои проблемы, так что нужно выбрать наименьшее зло 🙂

1. Прямая авторизация. Посылается HTTP-запрос с данными для логина на определенный адрес. В ответ придут данные в формате JSON, содержащие токен или описание ошибки.

Преимущества:

  • Простота.

Недостатки:

  • Нужно передавать секретный код приложения (возможно даже придется зашить в программу), соответственно есть риск его утечки.
  • Такой способ будет работать только для доверенных приложений. При создании нового приложения он будет недоступен и чтобы его включили нужно писать в поддержку.

2. Авторизация OAuth. Реализуется следующим образом. В программу нужно встроить браузер, в котором пользователю показать специальную страницу логина. После авторизации произойдет перенаправление на другую страницу и в текущем URL будет находиться токен либо описание ошибки. ВКонтакте этот способ позиционирует как основной.

Преимущества:

  • Главное и очень существенное преимущество в том, что он работает для всех приложений и для приложений, которым не разрешили прямую авторизацию, это вообще единственный способ.
  • Не надо передавать секретный ключ.
  • OAuth является стандартом и точно также можно авторизоваться в Facebook, например.

Недостатки, впрочем, тоже существенные.

  • Нужно открывать страницу ВКонтакте, а значит либо пытаться встроить ее в окно программы либо открывать в отдельном окне.
  • Поскольку мы открываем страницу, то нам нужен и браузер. Соответственно, придется тащить QtWebkit и все, что он за собой потянет, отчего программа прибавит в весе.
  • Нужно будет перехватывать события смены URL встроенного браузера, парсить этот URL и выбирать из него параметры, что несколько сложнее, чем XHR.
2. Прямая авторизация

Я, конечно, запросил чтобы мне включили возможность прямой авторизации, но поддержка ВКонтакте сначала неторопливо расспрашивала меня, что да зачем мне надо, а потом полный доступ все-таки зажала 🙁 Так что рассмотрим чисто теоретически. Выглядеть это будет примерно так:

function login() {     var params = {         grant_type: 'password',         client_id: 123456,         client_secret: 'XXX',         username: 'XXX',         password: 'XXX',         scope: 'audio'     }      function callback(request) {         if (request.status && request.status === 200) {             console.log("response", request.responseText)             var result = JSON.parse(request.responseText)             if (result.error) {                 console.log("Error:", result.error, result.error_description)             } else {                 main.authToken = result.auth_token                 // Now do requests with this token             }         } else {             console.log("HTTP:", request.status, request.statusText)         }     }      XHR.sendXHR('POST', 'https://oauth.vk.com/token', callback, URLQuery.serializeParams(params)) } 

В начале формируем параметры, в них я для примера указал, что требуется доступ к аудио записям пользователя (параметр scope). Затем функция-callback, которая в случае ошибки пишет в консоль, а в случае успеха сохраняет токен и дальше уже могут идти запросы к API.

На всякий случай, оставлю ссылку на документацию: vk.com/dev/auth_direct.

3. Авторизация через OAuth.

Для этого типа авторизации нам нужно показать пользователю веб-страницу логина. В QtQuick есть компонент WebView, позволяющий встроить в приложение на QML браузер на движке WebKit. После того, как пользователь авторизуется, URL в браузере сменится и, в случае удачной авторизации будет содержать токен в параметрах запроса или описание ошибки в якоре [5].

Чтобы не морочиться с разбиранием этого URL, используем функцию parseParams из URLQuery. Ей можно передать сразу весь URL, на выходе мы получим объект с параметрами.

Ниже описан компонент, реализующий этот функционал.

LoginWindow.qml:

import QtQuick 2.0 import QtQuick.Window 2.0 import QtWebKit 3.0 import "URLQuery.js" as URLQuery  Window {     id: loginWindow      property string applicationId     property string permissions     property var finishRegExp: /^https:\/\/oauth.vk.com\/blank.html/      signal succeeded(string token)     signal failed(string error)      function login() {         var params = {             client_id: applicationId,             display: 'popup',             response_type: 'token',             redirect_uri: 'http://oauth.vk.com/blank.html'         }         if (permissions) {             params['scope'] = permissions         }          webView.url = "https://oauth.vk.com/authorize?%1".arg(URLQuery.serializeParams(params))     }      width: 1024     height: 768      WebView {         id: webView          anchors.fill: parent          onLoadingChanged: {             console.log(loadRequest.url.toString())              if (loadRequest.status === WebView.LoadFailedStatus) {                 loginWindow.failed("Loading error:", loadRequest.errorDomain, loadRequest.errorCode,                                    loadRequest.errorString)                 return             } else if (loadRequest.status === WebView.LoadStartedStatus) {                 return             }              if (!finishRegExp.test(loadRequest.url.toString())) {                 return             }              var result = URLQuery.parseParams(loadRequest.url.toString())             if (!result) {                 loginWindow.failed("Wrong responce from server", loadRequest.url.toString())                 return             }             if (result.error) {                 loginWindow.failed("Error", result.error, result.error_description)                 return             }             if (!result.access_token) {                 loginWindow.failed("Access token absent", loadRequest.url.toString())                 return             }              succeeded(result.access_token)             return         }     } } 

Мы отображаем этот компонент в отдельном окне. После вызова метода login(), будет загружена страница логина.

После авторизации будет совершен переход на URL в котором в качестве адреса будет oauth.vk.com/blank.html, а затем через ‘?’ или ‘#’ будет идти результат. Параметром permissions мы задаем необходимые нам права доступа. Если мы там что-то указываем, то при логине через наш виджет пользователь увидит диалог предоставления прав доступа приложению.

Для того, чтобы понять, когда мы перешли на нужный адрес, мы устанавливаем обработчик onLoadingChanged. Он принимает объект loadRequest, из которого мы получаем всю нужную нам информацию. Он вызывается несколько раз и нас интересует ситуация либо когда произошла ошибка, в случае чего мы посылаем соответствующий сигнал, либо когда нужная страница загрузилась. В этом случае мы проверяем, пришел ли нам токен и, если да, посылаем сигнал об успешной авторизации, иначе сигнал об ошибке.

Ну а теперь рассмотрим саму программу, которая этот виджет использует. Программа в случае успешной авторизации устанавливает статус пользователя в «test». ID пользователя задается свойством userId в main.

import QtQuick 2.0 import 'URLQuery.js' as URLQuery import 'XHR.js' as XHR  Rectangle {     id: main      property int userId: XXX     property var authToken      width: 640     height: 320      function processLoginSuccess(token) {         loginWindow.visible = false         authToken = token          setStatus()     }      function setStatus() {         var params = {             access_token: main.authToken,             text: 'test'         }          function callback(request) {             if (request.status == 200) {                 console.log('result', request.responseText)                 var result = JSON.parse(request.responseText)                 if (result.error) {                     console.log('Error:', result.error.error_code,result.error.error_msg)                 } else {                     console.log('Success')                 }             } else {                 console.log('HTTP:', request.status, request.statusText)             }              Qt.quit()         }          XHR.sendXHR('POST', 'https://api.vk.com/method/status.set', callback,                     URLQuery.serializeParams(params))     }      LoginWindow {         id: loginWindow          applicationId: XXX         permissions: 'status'         visible: false         onSucceeded: processLoginSuccess(token)         onFailed: {             console.log('Login failed', error)             Qt.quit()         }     }      Component.onCompleted: {         loginWindow.visible = true         loginWindow.login()     } } 

После загрузки нам покажется окно логина. После логина оно скрывается и отправляется запрос на сервер для смены статуса пользователя. После этого, программа пишет в консоль результат и завершается.

После того, как мы авторизовались, нам не надо больше запрашивать токен, если нам не понадобились какие-то дополнительные права доступа или его время жизни не истекло (нам его возвращают вместе с токеном, в случае успешной авторизации).

Для чего еще можно применять XMLHttpRequest

Расскажу небольшую историю из своего опыта, не связанную с ВКонтакте, но зато связанную с XHR.

Как-то у моего коллеги возникла задача получать и обрабатывать в QML данные в формате XML.

В QtQuick есть специальный тип XmlListModel, способный вытащить из сети, распарсить и представить в виде модели XML-файл. Ему нужно задать запрос типа XPath, в соответствии с которым будет наполняться модель. Проблема была в том, что XML-файл содержал не только элементы, которые нужно было выбрать и поместить в модель но и некоторую дополнительную информацию, которую тоже нужно было получить.

Методов решения несколько. Можно использовать два объекта XmlListModel, но это однозначный костыль, к тому же не хотелось, чтобы XML-файл перекачивался два раза (а он будет, проверено). Можно реализовать этот функционал используя Qt, который содержит аж целых несколько вариантов парсеров, но было желание решить задачу на чистом QML.

Поскольку XMLHttpRequest изначально задумывался для работы именно с XML, в нем есть средства для работы с XML. Соответственно, можно получить и распарсить XML его средствами и выбрать нужную информацию. Затем можно этот же XML передать в XmlListModel (туда можно передавать не только URI, но и содержимое XML-файла).

Так что, несмотря на то, что сейчас XMLHttpRequest используется для чего угодно, не стоит забывать, для чего он был создан и что там есть еще и инструменты по работе с XML.

Небольшое резюме

QML содержит много инструментов, доступных для JavaScript в браузере. XMLHttpRequest позволяет отправлять HTTP-запросы и тем самым обеспечивать интеграцию приложения на QML с веб-ресурсами. Использование XHR позволяет во многих случаях обойтись без использования C++ для обмена данными с сервером и тем самым упростить разработку.

ссылка на оригинал статьи http://habrahabr.ru/post/176665/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *