Урок 8. Мобильные приложения

Добро пожаловать на восьмой урок курса для начинающих разработчиков под названием «Как гуглить?».

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

Сегодня мы с вами разберем кроссплатформенные мобильные приложения. По большому счету — это веб-сайты, завёрнутые в мобильные приложения, при этом такие приложения без труда могут использовать возможности мобильного устройства, такие как — доступ к камере, push-уведомления, deeplinking, bluetooth и т.д.

Перво-наперво, нам необходимо скачать и установить платформу NodeJS, которая позволит превратить язык Javascript из узкоспециализированного языка в язык общего назначения.

В этом уроке мы будем разрабатывать приложение, в первую очередь, для Android, поэтому нам также потребуется Android Studio. Если вы хотите скомпилировать приложение для iOS — вместо Android Studio вам потребуется Xcode. Также, для сборки Android-приложения нам потребуется Java SE Development Kit.

Далее, я рекомендую бегло ознакомиться с документацией к Apache Cordova — программной платформе, позволяющей разрабатывать мобильные приложения при помощи Javascript.

Перейдем сразу к делу — после установки NodeJS, открываем командную строку NodeJS и устанавливаем Apache Cordova через менеджер репозиториев npm (идущий в комплекте с NodeJS). Для этого в командной строке NodeJS напишем следующую команду:

				
					npm install -g cordova
				
			

Сегодня мы разработаем небольшое мобильное приложения для надёжного хранения ваших паролей (или другой конфиденциальной информации), с возможностью доступа на разных устройствах.

Создаем новое приложение, для этого в командной строке NodeJS напишем следующее:

				
					cordova create passapp com.example.passapp PassApp
				
			

Здесь

  • passapp — директория разрабатываемого приложения на вашем ПК
  • PassApp — имя вашего приложения
  • com.example.passapp — домен вашего приложения.

Apache Cordova создаст новую директорию (passapp) со всеми необходимыми файлами. Перейдем в эту директорию в командной строке:

				
					cd passapp
				
			

Добавим платформу для нашего приложения:

				
					cordova platform add android
				
			

Теперь выведем перечень используемых платформ.

				
					cordova platform ls
				
			

Нас будет интересовать установленная версия платформы

Переходим в Google и формируем следующий поисковый запрос «apache cordova android 10.1.2 api level» (вместо «10.1.2» укажите вашу версию платформы).

Нам необходимо определить подходящую версию Android SDK для используемой платформы Apache Cordova. В случае с версией платформы 10.1.2 необходимый API Level будет 30.

Открываем Adnroid Studio, переходим на вкладку Customize, нажимаем All settings. В появившемся окне находим: Apperiance & Behavior -> System Settings -> Android SDK. Нажимаем 2 галочки внизу: «Hide Obsolete Packages» и «Show Package Details».

Выбираем на вкладке «SDK Platforms» — Android SDK Platform 30, а на вкладке «SDK Tools» — 30.0.3 и нажимаем «Apply»

Теперь нам потребуется установить Gradle скачиваем дистрибутив (complete) из последнего релиза и куда-нибудь его распаковываем.

Теперь нам осталось настроить системные пути, если вы используете Windows. Правой кнопкой мыши кликаем по «Мой компьютер» и нажимаем «Свойства», в открывшемся окне выбираем «Дополнительные параметры системы», а там — «Переменные среды». В разделе «Системные переменные», если переменная «ANDROID_HOME» еще не была создана — нужно её создать, а в качестве значения переменной — указать следующее:

				
					C:\Users\USER\AppData\Local\Android\Sdk;C:\Users\USER\AppData\Local\Android\Sdk\platform-tools
				
			

где USER — имя вашего пользователя в операционной системе.

Также, убедимся, что в системной переменной «Path», есть следующие пути:

  • к директории «bin» для Gradle, куда мы его распаковали
    C:\Program Files\Android\Gradle\bin
  • к директории «platform-tools» для Android Studio
    C:\Users\USER\AppData\Local\Android\Sdk\platform-tools

Если их нет — добавим и сохраним изменения.

Чтобы изменения применились — перезагружаем компьютер. 

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

После перезагрузки компьютера — снова открываем командную строку NodeJS и пишем следующее:

				
					cd /
cd passapp
cordova build
				
			

Если нужно собрать приложение только для какой-то конкретной платформы — можем указать это для команды build

				
					cordova build android
				
			

Готовые приложения будут размещаться в директории platforms/название платформы/output

Попробуем собрать наше первое приложение, перенести его на телефон и установить.

Отлично! Это всё, что нужно нам знать об Apache Cordova для того, чтобы продолжить разработку нашего мобильного приложения.

Теперь, скачаем директорию assets, в которой хранятся файлы фреймворка Bootstrap, из панели управления хостингом для нашего сайта, который мы разрабатывали на прошлых уроках — нам она потребуется, чтобы подготовить простенький интерфейс для нашего приложения. Разместим папку assets в папке www нашего мобильного приложения. А также скопируем в папку www нашего приложения файл библиотеки jQuery.

Теперь найдем файл index.html в папке www нашего мобильного приложения и откроем его в текстовом редакторе. По большему счету, единственное, что должно остаться здесь — это подключение библиотеки Cordova — удалим весь код, кроме этого скрипта.

Теперь скопируем в index.html код нашего сайта, сохранив скрипт Cordova. Обратите внимание, что, по большему счету, Apache Cordova — это браузер, который не сможет распознать PHP-код. Поэтому, шапку и подвал, которые мы выносили в отдельные файлы на хостинге нужно разместить вместо PHP-функций.

Удалим всё лишнее и оставим в DOM-дереве только контейнер. Удалим лишние стили, а также, для удобства разработки в браузере — заменим пути к стилям и скриптам на относительные (уберем символ «/» в их адресах).

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

  • id (int, primary key) — идентификатор пользователя
  • login (varchar, 64) — логин пользователя
  • password (varchar, 64) — хеш пароля
  • data (text) — набор паролей или прочая конфиденциальная информация пользователя

Сразу же создадим нашего первого пользователя. В ячейке password выберем MD5, что позволит хранить нам только хеш пароля, а не весь пароль в открытом виде.

Далее, создадим таблицу sessions, в которой мы будем хранить информацию о токенах, которыми пользователи приложения могут подписывать свои запросы:

  • id (int, primary key)
  • date (timestamp, current timestamp)
  • user (int)
  • token (varchar, 16)

Теперь создадим в корневой директории с файлами сайта папку passwords — там мы будем хранить API для нашего сервиса. Сразу же создадим в папке passwords файл auth.php и разместим в нём следующий код:

				
					<?php
    header('Access-Control-Allow-Origin: *');
    
	function generateRandomString($length = 16) {
		$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
		$charactersLength = strlen($characters);
		$randomString = '';
		for ($i = 0; $i < $length; $i++) {
			$randomString .= $characters[rand(0, $charactersLength - 1)];
		}
		return $randomString;
	}

    if (!isset($_POST['login'])){
		$response['status'] = "error";
		$response['text'] = "Не указан логин";
		echo json_encode($response);
		exit;
	}
	
	if (!isset($_POST['password'])){
		$response['status'] = "error";
		$response['text'] = "Не указан пароль";
		echo json_encode($response);
		exit;
	}
	
	
    $mysqli = new mysqli("localhost", "u1777138_new", "cX8jI9zN3c", "u1777138_new");
    mysqli_query($mysqli, "SET character_set_results='utf8'");
    mysqli_query($mysqli, "SET NAMES 'utf8'");
    
	$login = $mysqli -> real_escape_string($_POST['login']);
	$password = md5($_POST['password']);
	
	$counter = 0;
	$sql = "SELECT * FROM `users` WHERE `login` = '$login'";
	if ($result = $mysqli -> query($sql)){
		foreach($result as $row){
			$counter++;
			$user_id = $row['id'];
			$db_password = $row['password'];
		}
	}
	
	if ($counter != 1){
		$response['status'] = "error";
		$response['text'] = "Пользователь не найден";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	if ($password != $db_password){
		$response['status'] = "error";
		$response['text'] = "Не правильный пароль";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	$token = generateRandomString();
	$sql = "INSERT INTO `sessions` (user, token) VALUES ($user_id, '$token')";
	$mysqli -> query($sql);
	
	$response['status'] = "ok";
	$response['text'] = $token;
	echo json_encode($response);
	$mysqli -> close();
?>
				
			

Давайте разберемся, что здесь написано. 

В самом начале мы задаем заголовок Access-Control-Allow-Origin, который позволит принимать запросы из любых доменов.

Далее мы создаем функцию, которая позволит генерировать случайную строку — она нам понадобится для генерации токена, по которому в дальнейшем мы будем распознавать пользователя.

Далее мы проверяем, чтобы в API были отправлены и логин, и пароль. В противном случае — сообщаем об ошибке.

После этого, мы поднимаем подключение к базе данных. Далее мы экранируем переданный логин и сохраняем его в переменную $login. А также, создаем MD5-хеш для пароля, чтобы провести сверку с информацией о пароле в БД.

Ищем в таблице пользователей, полученный логин, сохраняем в переменную $user_id — идентификатор пользователя с этим логином, а в переменную $db_password — MD5-хеш пароля, который хранится в базе данных. С помощью переменной $counter считаем сколько пользователей нашлось в базе данных по заданному логину. Если число пользователей не равно 1 (т.е. каким-то образом в БД попало 2 одинаковых логина, или, наоборот, пользователей по данному логину не найдено) — прерываем выполнение скрипта с ошибкой.

Далее, сверяем MD5-хеш от переданного в API пароля с информацией, которая хранится в БД.

Если все проверки были пройдены успешно — создаем новый токен и сохраняем его в таблицу sessions вместе с идентификатором пользователя.

Результат выполнения скрипта, вне зависимости от того был ли он выполнен успешно или завершился с ошибкой — записываем в массив, а сам массив перед тем как отдать ответ преобразуем в JSON-строку. Такой подход окажется полезным для удобного отображения ошибок в нашем приложении.

Теперь мы создадим еще один API-метод, который позволит нам получить конфиденциальную информацию по запросу пользователя в приложении. Создаем в директории passwords файл get.php и разместим в нем следующий код:

				
					<?php
    header('Access-Control-Allow-Origin: *');
    
	if (!isset($_POST['token'])){
		$response['status'] = "error";
		$response['text'] = "Не указан токен";
		echo json_encode($response);
		exit;
	}
	
	$mysqli = new mysqli("localhost", "u1777138_new", "cX8jI9zN3c", "u1777138_new");
    mysqli_query($mysqli, "SET character_set_results='utf8'");
    mysqli_query($mysqli, "SET NAMES 'utf8'");
	
	$token = $mysqli -> real_escape_string($_POST['token']);
	
	$counter = 0;
	$sql = "SELECT * FROM `sessions` WHERE `token` = '$token'";
	if ($result = $mysqli -> query($sql)){
		foreach($result as $row){
			$counter++;
			$user_id = $row['user'];
		}
	}

	if ($counter != 1){
		$response['status'] = "error";
		$response['text'] = "Токен не найден";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	$counter = 0;
	$sql = "SELECT * FROM `users` WHERE `id` = '$user_id'";
	if ($result = $mysqli -> query($sql)){
		foreach($result as $row){
			$counter++;
			$output = $row['data'];
		}
	}
	
	if ($counter != 1){
		$response['status'] = "error";
		$response['text'] = "Пользователь не найден";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	$response['status'] = "ok";
	$response['text'] = $output;
	echo json_encode($response);
	$mysqli -> close();
?>
				
			

Этот API-метод позволит получить конфиденциальную информацию пользователя.

Создадим еще один API-метод, позволяющий обновить конфиденциальную информацию пользователя в базе данных. Создадим для этого файл update.php

				
					<?php
    header('Access-Control-Allow-Origin: *');
    
	if (!isset($_POST['token'])){
		$response['status'] = "error";
		$response['text'] = "Не указан токен";
		echo json_encode($response);
		exit;
	}
	
	$mysqli = new mysqli("localhost", "u1777138_new", "cX8jI9zN3c", "u1777138_new");
    mysqli_query($mysqli, "SET character_set_results='utf8'");
    mysqli_query($mysqli, "SET NAMES 'utf8'");
	
	$token = $mysqli -> real_escape_string($_POST['token']);
	$data = $mysqli -> real_escape_string($_POST['data']);
	
	$counter = 0;
	$sql = "SELECT * FROM `sessions` WHERE `token` = '$token'";
	if ($result = $mysqli -> query($sql)){
		foreach($result as $row){
			$counter++;
			$user_id = $row['user'];
		}
	}

	if ($counter != 1){
		$response['status'] = "error";
		$response['text'] = "Токен не найден";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	$counter = 0;
	$sql = "SELECT * FROM `users` WHERE `id` = '$user_id'";
	if ($result = $mysqli -> query($sql)){
		foreach($result as $row){
			$counter++;
		}
	}
	
	if ($counter != 1){
		$response['status'] = "error";
		$response['text'] = "Пользователь не найден";
		echo json_encode($response);
		$mysqli -> close();
		exit;
	}
	
	$sql = "UPDATE `users` SET `data` = '$data' WHERE `id` = $user_id";
	$mysqli -> query($sql);
	
	$response['status'] = "ok";
	echo json_encode($response);
	$mysqli -> close();
?>
				
			

Теперь, возвращаемся к нашему мобильному приложению и открываем файл www/index.html в текстовом редакторе.

Перво-наперво, нам необходимо авторизовать пользователя, поэтому добавляем в наше приложение HTML-форму для авторизации:

				
					<form id="auth">
				<div class="form-group">
					<label for="login">Логин:</label>
					<input type="text" id="login" class="form-control">
				</div>
				<br>
				
				<div class="form-group">
					<label for="password">Пароль:</label>
					<input type="password" id="password" class="form-control">
				</div>
				<br>
				
				<div class="form-group">
					<label for="key">Ключ:</label>
					<input type="text" id="key" class="form-control">
				</div>
				<br>

				<button type="submit" class="btn btn-icon btn-success">Войти</button>
			</form>
				
			

По мимо логина и пароля, как вы видите, в форме появился некий «Ключ». Он нам понадобится, чтобы не хранить конфиденциальную информацию в открытом виде в базе данных. Когда пользователь решит обновить информацию в приложении — она будет зашифрована на стороне устройства пользователя при помощи этого ключа, и в базе данных информация будет хранится в зашифрованном виде. Точно также, для того, чтобы расшифровать информацию и базы данных — пользователю потребуется специальный ключ, который он придумает сам и будет знать только он.

Теперь, добавим в наше мобильное приложение Javascript-код, который будет отвечать за авторизацию пользователя:

				
					    $("#auth").submit(function(e){
        e.preventDefault();
        var login = $("#login").val();
        var password = $("#password").val();
        var user_key = $("#key").val();
        
        $.ajax({
            method: "POST",
            url: "https://илонмаск.рф/passwords/auth.php",
            data: {
                login: login,
                password: password
            }
        }).done(function(response) {
            var json_data = jQuery.parseJSON(response);
            
            if (json_data.status == "ok"){
                localStorage.setItem('token', json_data.text);
                localStorage.setItem('key', user_key);
            } else {
                alert(json_data.text);
            }
        });
    });
				
			

Этот скрипт позволит при отправке формы совершить запрос с API и в случае успеха — получить токен. Секретный ключ пользователя в API не передается. Токен для API и секретный ключ сохраняются в локальное хранилище устройства для того, чтобы при дальнейшем использовании не проходить заново процедуру авторизации.

В случае же, если API вернёт ошибку — мы покажем в приложении всплывающее окно с ошибкой.

Теперь, добавим в наше приложение textarea, в которой мы будем хранить конфиденциальную информацию, а также кнопку для сохранения изменений:

				
					<div id="private" style="display: none;">
				<button type="button" class="btn btn-icon btn-success" id="save" style="width: 100%;">Сохранить данные</button>
				<br><br>
				<div class="form-group">
					<textarea class="form-control" id="user_data" rows="10"></textarea>
				</div>				
			</div>
				
			

Теперь создадим функцию, которая позволит получить конфиденциальную информацию пользователя в разместить её в textarea.

				
					function load_data(){
			$.ajax({
				method: "POST",
				url: "https://илонмаск.рф/passwords/get.php",
				data: {
					token: localStorage.getItem('token')
				}
			}).done(function(response) {
				var json_data = jQuery.parseJSON(response);

				if (json_data.status == "ok"){
					$("#user_data").val(CryptoJS.AES.decrypt(json_data.text, localStorage.getItem('key')).toString(CryptoJS.enc.Utf8));
					$("#auth").css('display', 'none');
					$("#private").css('display', 'block');
				} else {
					alert(json_data.text);
					localStorage.removeItem('key');
					localStorage.removeItem('token');
					$("#auth").css('display', 'block');
					$("#private").css('display', 'none');
				}
			});
		}
				
			

Добавим нашу функцию load_data в предыдущую конструкцию, в случае, если пользователь был успешно авторизован — это позволит скрыть форму авторизации и показать textarea с пользовательской информацией.

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

				
					    $(document).ready(function() {
        if ((localStorage.getItem('token') !== null) && (localStorage.getItem('key') !== null)){
            load_data();
        } else {
            localStorage.removeItem('key');
            localStorage.removeItem('token');
        }
    });
				
			

Теперь добавим обработчик для кнопки сохранения изменений

				
					        $("#save").click(function(e) {
			e.preventDefault();
            
			$.ajax({
				method: "POST",
				url: "https://илонмаск.рф/passwords/update.php",
				data: {
					token: localStorage.getItem('token'),
					data: CryptoJS.AES.encrypt($("#user_data").val(), localStorage.getItem('key')).toString()
				}
			}).done(function(response) {
				var json_data = jQuery.parseJSON(response);

				if (json_data.status == "ok"){
					alert ('Информация успешно обновлена')
				} else {
					alert(json_data.text);
				}
			});
		});
				
			

А теперь — самое интересное. Мы добавим в наш сервис шифрование на стороне пользователя при помощи библиотеки CryptoJS. Для этого подключим CryptoJS с помощью следующего кода:

				
					<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/pbkdf2.js"></script>
				
			

Теперь, добавим расшифровку в нашу функцию load_data с помощью следующей конструкции:

				
					CryptoJS.AES.decrypt(json_data.text, localStorage.getItem('key')).toString(CryptoJS.enc.Utf8)
				
			

А конфиденциальную информацию будем отправлять в API после обработки при помощи следующей конструкции:

				
					CryptoJS.AES.encrypt($("#user_data").html(), localStorage.getItem('key')).toString()
				
			

Итого, после всех изменений мы должны получить следующий код в файле index.html нашего мобильного приложения:

				
					<html lang="ru">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link href="assets/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
	<main>
		<div class="container py-4">
			<form id="auth">
				<div class="form-group">
					<label for="login">Логин:</label>
					<input type="text" id="login" class="form-control">
				</div>
				<br>
				
				<div class="form-group">
					<label for="password">Пароль:</label>
					<input type="password" id="password" class="form-control">
				</div>
				<br>
				
				<div class="form-group">
					<label for="key">Ключ:</label>
					<input type="text" id="key" class="form-control">
				</div>
				<br>

				<button type="submit" class="btn btn-icon btn-success">Войти</button>
			</form>
			
			<div id="private" style="display: none;">
				<button type="button" class="btn btn-icon btn-success" id="save" style="width: 100%;">Сохранить данные</button>
				<br><br>
				<div class="form-group">
					<textarea class="form-control" id="user_data" rows="10"></textarea>
				</div>				
			</div>
		</div>
	</main>

	<script src="jquery-3.6.1.min.js"></script>
    <script src="cordova.js"></script>
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/pbkdf2.js"></script>
	
	<script>
		function load_data(){
			$.ajax({
				method: "POST",
				url: "http://xn--80aphcffg1a.xn--p1ai/passwords/get.php",
				data: {
					token: localStorage.getItem('token')
				}
			}).done(function(response) {
				var json_data = jQuery.parseJSON(response);

				if (json_data.status == "ok"){
					$("#user_data").val(CryptoJS.AES.decrypt(json_data.text, localStorage.getItem('key')).toString(CryptoJS.enc.Utf8));
					$("#auth").css('display', 'none');
					$("#private").css('display', 'block');
				} else {
					alert(json_data.text);
					localStorage.removeItem('key');
					localStorage.removeItem('token');
					$("#auth").css('display', 'block');
					$("#private").css('display', 'none');
				}
			});
		}


		$(document).ready(function() {
			if ((localStorage.getItem('token') !== null) && (localStorage.getItem('key') !== null)){
				load_data();
			} else {
				localStorage.removeItem('key');
				localStorage.removeItem('token');
			}
		});


		$("#auth").submit(function(e){
			e.preventDefault();
			var login = $("#login").val();
			var password = $("#password").val();
			var user_key = $("#key").val();

			$.ajax({
				method: "POST",
				url: "http://xn--80aphcffg1a.xn--p1ai/passwords/auth.php",
				data: {
					login: login,
					password: password
				}
			}).done(function(response) {
				var json_data = jQuery.parseJSON(response);

				if (json_data.status == "ok"){
					localStorage.setItem('token', json_data.text);
					localStorage.setItem('key', user_key);
					load_data();
				} else {
					alert(json_data.text);
				}
			});
		});


		$("#save").click(function(e) {
			e.preventDefault();

			$.ajax({
				method: "POST",
				url: "https://илонмаск.рф/passwords/update.php",
				data: {
					token: localStorage.getItem('token'),
					data: CryptoJS.AES.encrypt($("#user_data").val(), localStorage.getItem('key')).toString()
				}
			}).done(function(response) {
				var json_data = jQuery.parseJSON(response);

				if (json_data.status == "ok"){
					alert ('Информация успешно обновлена')
				} else {
					alert(json_data.text);
				}
			});
		});
	</script>
  </body>
</html>
				
			

Теперь нам осталось только собрать наше приложение и протестировать на смартфоне. Возвращаемся в командную строку NodeJS и собираем приложение

				
					cordova build
				
			

Скачать исходные файлы урока №8

Обсудите ваш проект с менеджером

    Я согласен на обработку персональных данных согласно политике конфиденциальности

    Звоните
    Пишите

    Meta* признана экстремистской организацией на территории РФ

    Служба поддержки

      Тариф Базовый Продвинутый Профессиональный
      При оплате на 1 год 390 руб./мес. 450 руб./мес. 490 руб./мес.
      При оплате на 2 года 332 руб./мес. 383 руб./мес. 417 руб./мес.
      При оплате на 3 года 273 руб./мес. 315 руб./мес. 343 руб./мес.

      Реквизиты для рассчётного счета

      Заполните реквизиты вашей организации, а мы подготовим и направим счет вам на электронную почту. Обратите внимание, при оплате через расчетный счет срок выполнения работ увеличен до 10 рабочих дней.

        Я согласен на обработку персональных данных согласно политике конфиденциальности