Как создать SPA на JS и PHP за час

Одностраничные приложения уже давно не в новинку, а скоро вообще станут стандартом веб-разработки. Узнайте, как их создавать, — пока не поздно.

SPA (англ. Single Page Application — одностраничное приложение) — это сайт, для работы которого не требуется обновление страницы, потому что все данные загружаются с помощью скриптов.

Принцип работы SPA прост: когда вы совершаете какое-то действие, например нажимаете на ссылку, скрипт перехватывает это событие. Он отменяет действие по умолчанию и вместо этого сам обменивается данными с сервером, а потом выводит их на странице.

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

SPA отлично подходит для интернет-магазинов: пользователь может нажать на кнопку «Добавить в корзину» и тут же продолжить смотреть другие товары. Но и для обычных блогов такая технология вполне уместна.

Создать что-то подобное можно и за час. Вы можете посмотреть репозиторий с полным кодом приложения.

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Каким должно быть SPA

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

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

Это небольшой список, но он очень важен, потому что иначе ваше SPA станет менее удобным, чем обычный сайт.

Вёрстка сайта

Для начала нужно создать макет сайта:

<!DOCTYPE html>
<html>
    <head>
   	 <title>My sick SPA</title>
   	 <meta charset="utf-8">
   	 <link rel="stylesheet" type="text/css" href="/spa/styles/app.css">
    </head>
    <body>
   	 <div class="wrapper">
   		 <header class="header">
   			 <div class="header__content">
   				 <h1 class="header__title" id="title">Main</div>
   			 </div>
   		 </header>
   		 <main class="main">
   			 <div class="main__content" id="body">
   				 <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p>

   				 <a href="/Articles/1" class="link link_internal">Article 1</a><br>
   				 <a href="/Articles/2" class="link link_internal">Article 2</a><br>
   				 <a href="/Articles/3" class="link link_internal">Article 3</a><br>
   				 <a href="/Articles/4" class="link link_internal">Article 4</a><br>
   			 </div>
   		 </main>
   	 </div>
   	 <script type="text/javascript" src="/spa/scripts/app.js"></script>
    </body>
</html>

Обратите внимание на ссылки с классом link_internal — они загружаются без обновления. Работу других ссылок менять не нужно.

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

Подключаем стили:

body, html
{
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
    font-size: 16px;
    font-family: arial;
    background: #f7f7f7;
    color: #373737;
}

.wrapper
{
    width: 100%;
}

.header
{
    background: #373737;
}

.header__content
{
    padding: 5px;
}

.header__title
{
    display: block;
    padding: 5px;
    margin: 5px;
    color: #f7f7f7;
}

.main
{
    width: 90%;
    margin: 10px auto;
}

.link, .link:visited
{
    color: #111;
}

.link:hover
{
    text-decoration: none;
}

Получается так:

Пишем серверную часть

Серверная часть будет представлять собой PHP-скрипт, который получает запрос от клиента, обрабатывает его и возвращает какие-то данные в формате JSON. В нашем случае в клиентском запросе содержится раздел (Main или Articles) и идентификатор страницы.

Создадим файл core.php:

<?php

//Указываем в заголовках, что страница возвращает данные в формате JSON
header("Content-type: application/json; charset=utf-8");

//Создаём класс, который будем возвращать. В нём всего два свойства - заголовок и тело страницы
class Page
{
    public $title;
    public $body;

    public function __construct($title, $body)
    {
   	 $this->title = $title;
   	 $this->body = $body;
    }
}

$articles = //Создаём статьи
[
    new Page("Article 1", "<p>asdas 1</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 2", "<p>asdas 2</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 3", "<p>asdas 3</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 4", "<p>asdas 4</p> <a href='/Main' class='link link_internal'>Return to main page</a>")
];

//Получаем запрос от клиента
if(isset($_GET["page"]))
{
    $page = $_GET["page"];
}
else
{
    $page = "404";
}

if(isset($_GET["id"]))
{
    $id = $_GET["id"];
}
else
{
    $id = 0;
}

//Если никакая страница не подойдёт под запрос, то пользователь увидит сообщение, что страница не найдена
$response = new Page("404", "<p>Page not found</p> <a href='/Main' class='link link_internal'>Return to main page</a>");

switch($page) //Выбираем страницы
{
    case "main": //Главная
   	 $response = new Page("Main", "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p> <a href='/Articles/1' class='link link_internal'>Article 1</a><br><a href='/Articles/2' class='link link_internal'>Article 2</a><br><a href='/Articles/3' class='link link_internal'>Article 3</a><br><a href='/Articles/4' class='link link_internal'>Article 4</a><br>");
   	 break;

    case "articles": //Статьи
   	 if($id > 0)
   	 {
   		 if(isset($articles[$id - 1]))
   		 {
   			 $response= $articles[$id - 1];
   		 }
   	 }
   	 break;
}

die(json_encode($response)); //Возвращаем страницу
?>

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

Можно открыть скрипт и проверить его работу:

Серверная часть может быть написана на любом языке — главное, чтобы она возвращала данные в формате JSON.

Пишем клиентскую часть

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

Ссылки вида

/Articles/1

должны преобразовываться в следующий запрос:

?page=articles&id=1

Но чтобы они корректно открывались, нужно создать .htaccess-файл и добавить переадресацию для ошибки 404:

ErrorDocument 404 /spa/index.html

Теперь можно заняться самим скриптом:

var links = null; //Создаём переменную, в которой будут храниться ссылки

var loaded = true; //Переменная, которая обозначает, загрузилась ли страница

var data = //Данные о странице
{
    title: "",
    body: "",
    link: ""
};

var page = //Элементы, текст в которых будет меняться
{
    title: document.getElementById("title"),
    body: document.getElementById("body")
};

//По умолчанию в макете содержится контент для главной страницы.
//Но если пользователь перейдёт по ссылке, которая ведёт на какую-нибудь статью, он увидит не то, что ожидает.
//Поэтому нужно проверить, на какой странице находится пользователь, и загрузить релевантные данные.
OnLoad();

function OnLoad()
{
    var link = window.location.pathname; //Ссылка страницы без домена

    //У меня сайт находится по ссылке http://localhost/spa, поэтому мне нужно обрезать часть с spa/
    var href = link.replace("spa/", "");

    LinkClick(href);
}

function InitLinks()
{
    links = document.getElementsByClassName("link_internal"); //Находим все ссылки на странице

    for (var i = 0; i < links.length; i++)
    {
   	 //Отключаем событие по умолчанию и вызываем функцию LinkClick
   	 links[i].addEventListener("click", function (e)
   	 {
   		 e.preventDefault();
   		 LinkClick(e.target.getAttribute("href"));  
   		 return false;
   	 });
    }
}

function LinkClick(href)
{
    var props = href.split("/"); //Получаем параметры из ссылки. 1 - раздел, 2 - идентификатор

    switch(props[1])
    {
   	 case "Main":
   		 SendRequest("?page=main", href); //Отправляем запрос на сервер
   		 break;

   	 case "Articles":
   		 if(props.length == 3 && !isNaN(props[2]) && Number(props[2]) > 0) //Проверяем валидность идентификатора и тоже отправляем запрос
   		 {
   			 SendRequest("?page=articles&id=" + props[2], href);
   		 }
   		 break;
    }
}

function SendRequest(query, link)
{
    var xhr = new XMLHttpRequest(); //Создаём объект для отправки запроса

    xhr.open("GET", "/spa/core.php" + query, true); //Открываем соединение

    xhr.onreadystatechange = function() //Указываем, что делать, когда будет получен ответ от сервера
    {
   	 if (xhr.readyState != 4) return; //Если это не тот ответ, который нам нужен, ничего не делаем

   	 //Иначе говорим, что сайт загрузился
   	 loaded = true;

   	 if (xhr.status == 200) //Если ошибок нет, то получаем данные
   	 {
   		 GetData(JSON.parse(xhr.responseText), link);
   	 }
   	 else //Иначе выводим сообщение об ошибке
   	 {
   		 alert("Loading error! Try again later.");
   		 console.log(xhr.status + ": " + xhr.statusText);
   	 }
    }

    loaded = false; //Говорим, что идёт загрузка

    //Устанавливаем таймер, который покажет сообщение о загрузке, если она не завершится через 2 секунды
    setTimeout(ShowLoading, 2000);
    xhr.send(); //Отправляем запрос
}

function GetData(response, link) //Получаем данные
{
    data =
    {
   	 title: response.title,
   	 body: response.body,
   	 link: link
    };

    UpdatePage(); //Обновляем контент на странице
}

function ShowLoading()
{
    if(!loaded) //Если страница ещё не загрузилась, то выводим сообщение о загрузке
    {
   	 page.body.innerHTML = "Loading...";
    }
}

function UpdatePage() //Обновление контента
{
    page.title.innerText = data.title;
    page.body.innerHTML = data.body;

    document.title = data.title;
    window.history.pushState(data.body, data.title, "/spa" + data.link); //Меняем ссылку

    InitLinks(); //Инициализируем новые ссылки
}

Теперь можно смотреть, как работает сайт:

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

Заключение

SPA может казаться чем-то сложным, но нам хватило небольшого скрипта, чтобы заставить всё работать. Даже не пришлось никакие фреймворки использовать.

Если вы хотите научиться писать такие же эффективные веб-приложения, то записывайтесь на курс «Frontend-разработчик». Вы на реальных проектах освоите все инструменты и станете полноценным специалистом.

Курс

Frontend-разработчик


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

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox