Category: Web

15
Сен
2020

🌐 Кто такой Web Developer: гайд по профессии

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

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

Базовые технологии

Веб-разработку разделяют на frontend и backend. Frontend отвечает за работу на стороне клиента, корректное отображение веб-страниц на разных типах устройств. Backend всё, что происходит на стороне сервера: вычисления, работа с базами данных, взаимодействие с другими сервисами.

Под Web Developer обычно понимают программиста, который умеет работать и на стороне клиента, и на стороне сервера. Универсального программиста, который может сделать проект с нуля, разбирается в базах данных, конфигурировании сервера, безопасности, называют Full Stack Web Developer. Более узких специалистов – Backend Web Developer и Frontend Web Developer.

Базовые технологии Frontend

HTML – язык разметки страниц, костяк веб-страницы. С помощью HTML мы подключаем JS-скрипты и CSS-стили и определяем элементы страницы: текст, заголовки, поля ввода информации, переключатели и кнопки.

CSS – язык описания стилей элементов, с помощью которого добиваются качественного отображения страницы.

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

Базовые технологии Backend

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

PHP. 4500 вакансий по России. Самый популярный язык программирования на стороне сервера: 80% сайтов и сервисов в Интернете написаны на PHP. Язык обладает большим сообществом, огромным количеством готовых библиотек, фреймворков, учебных материалов и руководств. Если вы делаете первые шаги в профессии веб-разработчика, рекомендуем начать с PHP. Язык легок в освоении, используется не только в любительских, но и больших профессиональных проектах: Facebook, VK, Wikipedia, BlaBlaCar. Средняя зарплата 200 000 руб. (здесь и далее зарплаты указаны до вычета НДФЛ).

Java. 2200 вакансий по России. Популярный язык программирования, но с высоким порогом входа. Применяется в средних и крупных проектах. С помощью Java-фреймворка Spring создаются высокопроизводительные, масштабируемые и безопасные приложения. Средняя зарплата 250 000 руб.

Python. 1800 вакансий по России. Язык с низким порогом входа. Дружелюбное комьюнити и развитая документация, пошаговые руководства и видеоуроки позволят быстро прояснить любой вопрос. Наиболее зрелая и популярная технология – фреймворк Django. Python применяют в проектах любого размера, на нем частично написаны такие сервисы, как YouTube, Google Search, Instagram и Spotify. Средняя зарплата 160 000 руб.

Node.js. 1500 вакансий по России. Технология Node.js позволяет строить высоконагруженные, легко масштабируемые решения при помощи языка JavaScript. Если вы уже имеете опыт во фронтенд-разработке, то сможете быстро начать программировать на Node.js. Технологию применяют в проектах, где важна одновременная обработка большого количества запросов. Активно используется компаниями Paypal, Yahoo! и eBay. Средняя зарплата 205 000 руб.

ASP.NET. 1300 вакансий по России. Если вы уже знаете язык C#, то вам подойдет фреймворк ASP.NET. Свои преимущества фреймворк полностью раскрывает только при работе в инфраструктуре Microsoft. Технологию используют Starbucks и StackOverflow. Средняя зарплата 160 000 руб.

Язык/технология Зарплата, тыс. руб Количество вакансий (на 15.09.20) Популярность на GitHub (среди перечисленных технологий) Популярность на StackOverflow, %
PHP 200 4500 3 26.2
Java 250 2200 2 40.2
Python 160 1808 1 44.1
Node.JS 205 1456 4 24.5
ASP.NET 160 1314 5 18.7

Источник статистики по StackOverflow, источник статистики GitHub, статистика по вакансиям взята с hh.ru, статистика по зарплатам предоставлена компанией Hays

Общие технологии продвинутого уровня

Развертывание инфраструктуры

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

  • веб-сервер (например, Nginx или Apache);
  • серверная операционная система: веб-приложения редко работают на Windows или MacOS, отраслевым стандартом является Linux;
  • интерпретатор языка нужной версии – в зависимости от того, какие новые функции языка необходимо использовать или какую версию требует фреймворк;
  • база данных (например, MySQL или PostgreSQL).

Книги по теме:

Тестирование

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

Книги по теме:

Наши статьи по теме:

Системы контроля версий

Сложное веб-приложение невозможно за приемлемое время написать одному человеку: для этого необходима команда специалистов, которые разделяют между собой обязанности и ведут распределенную кодовую базу. Для этого используются системы контроля версий, самой распространенной из которых является Git. Знание GIt гарантированно потребуется при трудоустройстве веб-девелопером.

Наши статьи по теме:

Пакетные менеджеры

Вокруг каждого языка программирования и фреймворка сформировалась инфраструктура: библиотеки, расширения, пакеты. Зачем писать собственный http-клиент, если существует хорошо отлаженный пакет с нужным функционалом? Так, в PHP используется composer, в Java – Gradle, в JavaScript и Node.Js – npm, в Python – pip.

Продвинутый Frontend

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

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

Фреймворк Количество вакансий Репозиториев на GitHub Популярность StackOverflow, % Уровень сложности Зависимостей в npm
React 4 280 73 458 35.9 ⭐⭐ 48 718
Angular 2 364 20 142 25.1 ⭐⭐⭐ 13 579
Vue 2 021 21 776 17.3 21 575

Источник статистики по StackOverflow, источник статистики GitHub, статистика по вакансиям взята с hh.ru

Продвинутый Backend

Backend-фреймворки менее разнообразны – обычно есть 1-2 лидера, которые занимают существенную часть рынка, а остальные либо узко специализированы, либо технологически отстают от лидеров. Для PHP такими фреймворками являются Symfony и Laravel, для Java – Spring и JSF, для Python – Django, Pyramid и Flask, для Node.JS – Express.JS, Meteor.JS и Koa.JS.

Ниже представлена сравнительная таблица оценки популярности PHP-фреймворков.

Фреймворк Установок через composer Зависимых проектов Популярность на GitHub Количество вакансий
Laravel 104 726 623 9 548 22 048 1 085
Symfony 52 561 771 3 971 23 880 819
Phalcon 29 504 2 10 232 47
CodeIgniter 773 857 65 18 071 52
Yii 10 393 997 11 540 217 890
CakePHP 6 027 870 1 649 8 234 10
Slim Framework 13 410 045 1 329 10 723 19
Lumen 535 795 53 6 900 45

Данные по количеству вакансий указаны по данным сервиса hh.ru, остальные данные взяты с сервиса packagist.org.

Продвинутый Web Developer: паттерны проектирования

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

Книги по паттернам:

Наши публикации о паттернах:

Практическое освоение технологий

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

Домашние проекты

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

Ресурсы с задачами для программистов

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

Присоединяйтесь к проектам на GitHub

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

Заключение

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

Вы узнаете, как профессионально верстать, используя HTML и CSS, научитесь программировать на JavaScript (в том числе используя React). На стороне сервера вы освоите PHP (Laravel) и Node.js (фреймворк Express). Не будут обойдены вниманием базы данных (MySQL) и операционная система Linux. Всё сразу в одном месте. Отличный вариант, чтобы стать настоящим веб-разработчиком за приемлемое время.

24
Июл
2020

⌨️ Инструкция: как создать форму в React

Показываем на примерах JS-кода, как создать форму с помощью контролируемых или неконтролируемых компонентов React и обработать состояние с помощью React хуков.

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

Вот пример простой HTML-формы:

        <form>  
  <label for="name">Name:</label><br>  
  <input type="text" id="name" name="name" value="John"><br>  
  <input type="submit" value="Submit">  
</form>
    

Давайте рассмотрим, как создаются формы в React.

Как создать форму в React с помощью контролируемых компонентов

Мы знаем, что такие HTML-элементы, как input, запоминают то, что мы вводим. Таким же образом можно использовать состояние компонента React для хранения данных элементов формы. Когда
данные обрабатываются компонентом React, он называется
контролируемым, а функционирование в нем завязано на состоянии компонента, а не
на элементе DOM.

Давайте реализуем форму с помощью контролируемых компонентов, используя следующие элементы HTML: input, radio button, select dropdown.

Создадим компонент с
тегом формы и один input:

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input type="text" placeholder="enter student name" />  
        </form>  
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent; 
    

Выглядеть это будет
так:

Контролируемый компонент формы
Контролируемый компонент формы

Создадим состояние в компоненте и повесим событие onChange на input, чтобы изменить
состояние компонента при изменении значения input.

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  
  constructor(props) {  
    super(props);  
    this.state = {  
      studentName: ""  
    };  
    this.onNameChangeHandler = this.onNameChangeHandler.bind(this);  
  }  
  
  onNameChangeHandler(e){  
    this.setState({  
      studentName: e.target.value  
    })  
  }  
  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input   
              type="text"   
              placeholder="enter student name"  
              onChange={this.onNameChangeHandler} />  
        </form>   
        <hr/>  
        <p>State of Component</p>        
          <pre>{JSON.stringify(this.state, null, 2) }</pre>       
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent;  
    

Мы создали состояние и
добавили studentName в качестве свойства. После этого добавили обработчик
onChange, который изменит состояние и забиндит его с событием onChange у input.

Таким же образом можно
добавить еще один HTML-элемент и привязать его к состоянию компонента.

Добавим на нашу форму radio button и два новых свойства: пол и штат.

        <label>Gender: </label>  
          <label>Male</label>  
          <input  
            type="radio"  
            name="gender"  
            value="male"  
            checked={this.state.gender === "male"}  
            onChange={this.onChangeHandler}  
          />  
          <label>Female</label>  
          <input  
            type="radio"  
            name="gender"  
            value="female"  
            checked={this.state.gender === "female"}  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>State: </label>  
          <select  
            name="state"  
            value={this.state.state}  
            onChange={this.onChangeHandler}  
          >  
            <option value="Maharashtra">Maharashtra</option>  
            <option value="Madhya Pradesh">Madhya Pradesh</option>  
            <option value="Karnataka">Karnataka</option>  
            <option value="West Bengal">West Bengal</option>  
          </select>  
    

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

        onChangeHandler(e){  
    this.setState({  
      [e.target.name]: e.target.value  
    })  
  } 
    

Вот так выглядит
готовый компонент:

        import React, { Component } from "react";  
  
class ControlledFormComponent extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      studentName: "",  
      gender: "",  
      state: "Maharashtra"  
    };  
    this.onChangeHandler = this.onChangeHandler.bind(this);  
  }  
  onChangeHandler(e) {  
    this.setState({  
      [e.target.name]: e.target.value  
    });  
  }  
  render() {  
    return (  
      <div>  
        <h3>Controlled Component</h3>  
        <br />  
        <form>  
          <label>Student Name: </label>  
          <input  
            type="text"  
            name="studentName"  
            placeholder="enter student name"  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>Gender: </label>  
          <label>Male</label>  
          <input  
            type="radio"  
            name="gender"  
            value="male"  
            checked={this.state.gender === "male"}  
            onChange={this.onChangeHandler}  
          />  
          <label>Female</label>  
          <input  
            type="radio"  
            name="gender"  
            value="female"  
            checked={this.state.gender === "female"}  
            onChange={this.onChangeHandler}  
          />  
          <br />  
          <br />  
          <label>State: </label>  
          <select  
            name="state"  
            value={this.state.state}  
            onChange={this.onChangeHandler}  
          >  
            <option value="Maharashtra">Maharashtra</option>  
            <option value="Madhya Pradesh">Madhya Pradesh</option>  
            <option value="Karnataka">Karnataka</option>  
            <option value="West Bengal">West Bengal</option>  
          </select>  
        </form>  
        <br />  
        <hr />  
        <p>State of Component</p>  
        <pre>{JSON.stringify(this.state, null, 2)}</pre>  
      </div>  
    );  
  }  
}  
  
export default ControlledFormComponent;  
    
Компонент в действии
Компонент в действии

Использование хуков React

Состояние формы можно
обрабатывать и с помощью хуков. Чтобы хранить состояние в компоненте, существует useState(). Создадим простую форму
с одним инпутом и обработаем ее с помощью хука:

        import React, { useState } from 'react';  
  
export default function ControlledFormWithHook() {  
  const [name, setName] = useState('');  
  
  return (  
    <div>  
      <form>  
        <label>Name:</label>  
        <input type="text" onChange={(e) => setName(e.target.value)} />  
      </form>  
      <br />  
      Name is: {name}  
    </div>  
  );  
}  
    
Форма с использованием хуков
Форма с использованием хуков

Как создать форму в React с помощью неконтролируемых компонентов

Мы знаем, что
HTML-элементы сохраняют собственное состояние и обновляют его при изменении
входного значения. То есть мы можем получить прямой доступ к значению
HTML-элемента без сохранения состояния компонента. Когда данные обрабатываются
элементами DOM, это можно назвать неконтролируемым компонентом. React позволяет напрямую получить ссылку на элемент DOM и хранить в нем состояние компонента.

В некоторых случаях мы
должны использовать неконтролируемый компонент, например, когда хотим добавить выбор файла: <input type="file"/>.

Возьмем тот
же пример и немного
переделаем с помощью React.createRef() API.

        import React, { Component } from "react";  
  
export default function UncontrolledFormComponent() {  
  let inputRef = React.createRef();  
  let name = "";  
  const handleClick = e => {  
    e.preventDefault();  
    alert("Name is: " + inputRef.current.value);  
  };  
  
  return (  
    <div>  
      <h3>Uncontrolled Form Component</h3>  
      <form>  
        <input type="text" ref={inputRef} />  
        <button style={{ margin: "8px" }} onClick={handleClick}>  
          Submit  
        </button>  
      </form>  
    </div>  
  );  
} 
    

Когда вы нажимаете на Submit, открывается alert со значением, которое было
введено в текстовом поле.

Работа с неконтролируемым компонентом
Работа с неконтролируемым компонентом
***

Итак, мы рассмотрели
формы, а также обсудили различные способы их обработки в React. Если вас привлекает веб-разработка, мы советуем пройти курс факультета Веб-разработки GeekBrains и получить готовую базу навыков.

В качестве фронтенда на курсе используется React, для бэкенда – PHP и фреймворк Laravel. То есть самые популярные технологии в своих нишах. Курс поможет освоить профессию с нуля, получить диплом и создать портфолио с рабочими проектами, а в случае успешного прохождения команда университета поможет с трудоустройством.

16
Июн
2020

Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2';

async function fetchProductList(url) {
    const browser = await puppeteer.launch({ 
        headless: true, // false: enables one to view the Chrome instance in action
        defaultViewport: null, // (optional) useful only in non-headless mode
    });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    ...
}

fetchProductList(url);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

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

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        async function fetchProductList(url) {
	...
    
    await page.waitFor('div[data-cel-widget^="search_result_"]');

    const result = await page.evaluate(() => {
        // counts total number of products
        let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length;

        let productsList = [];

        for (let i = 1; i < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 0 ? onlyProduct = true : emptyProductMeta = true;
            }

            let productsDetails = productNodes.map(el => el.innerText);

            if (!emptyProductMeta) {
                product.brand = onlyProduct ? '' : productsDetails[0];
                product.product = onlyProduct ? productsDetails[0] : productsDetails[1];
            }
			
            // traverse for product image
            let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`);
            product.image =rawImage ? rawImage.src : '';
			
            // traverse for product url
            let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`);
            product.url = rawUrl ? rawUrl.href : '';

            // traverse for product price
            let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`);
            product.price = rawPrice ? rawPrice.innerText : '';

            if (typeof product.product !== 'undefined') {
                !product.product.trim() ? null : productsList = productsList.concat(product);
            }
        }

        return productsList;
    });
    
    ...
}
    
...
    

После изучения DOM стало ясно, что каждый
перечисленный элемент выводится с селектором
div[data-cel-widget^="search_result_"].
Данный селектор ищет все теги div с атрибутом data-cel-widget, которые имеют
значение, начинающееся с search_result_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base (i обозначает номер узла в total listed items)
  • product: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

async function fetchProductList(url, searchTerm) {
	...
	await page.goto(url, { waitUntil: 'networkidle2' });

    await page.waitFor('input[name="field-keywords"]');
    await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm);

    await page.click('div.nav-search-submit.nav-sprite');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        const puppeteer = require('puppeteer');
const userAgent = require('user-agents');

...

const browser = await puppeteer.launch({ headless: true, defaultViewport: null });
const page = await browser.newPage();
await page.setUserAgent(userAgent.toString());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

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

16
Июн
2020

Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2';

async function fetchProductList(url) {
    const browser = await puppeteer.launch({ 
        headless: true, // false: enables one to view the Chrome instance in action
        defaultViewport: null, // (optional) useful only in non-headless mode
    });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    ...
}

fetchProductList(url);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

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

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        async function fetchProductList(url) {
	...
    
    await page.waitFor('div[data-cel-widget^="search_result_"]');

    const result = await page.evaluate(() => {
        // counts total number of products
        let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length;

        let productsList = [];

        for (let i = 1; i < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 0 ? onlyProduct = true : emptyProductMeta = true;
            }

            let productsDetails = productNodes.map(el => el.innerText);

            if (!emptyProductMeta) {
                product.brand = onlyProduct ? '' : productsDetails[0];
                product.product = onlyProduct ? productsDetails[0] : productsDetails[1];
            }
			
            // traverse for product image
            let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`);
            product.image =rawImage ? rawImage.src : '';
			
            // traverse for product url
            let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`);
            product.url = rawUrl ? rawUrl.href : '';

            // traverse for product price
            let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`);
            product.price = rawPrice ? rawPrice.innerText : '';

            if (typeof product.product !== 'undefined') {
                !product.product.trim() ? null : productsList = productsList.concat(product);
            }
        }

        return productsList;
    });
    
    ...
}
    
...
    

После изучения DOM стало ясно, что каждый
перечисленный элемент выводится с селектором
div[data-cel-widget^="search_result_"].
Данный селектор ищет все теги div с атрибутом data-cel-widget, которые имеют
значение, начинающееся с search_result_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base (i обозначает номер узла в total listed items)
  • product: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

async function fetchProductList(url, searchTerm) {
	...
	await page.goto(url, { waitUntil: 'networkidle2' });

    await page.waitFor('input[name="field-keywords"]');
    await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm);

    await page.click('div.nav-search-submit.nav-sprite');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        const puppeteer = require('puppeteer');
const userAgent = require('user-agents');

...

const browser = await puppeteer.launch({ headless: true, defaultViewport: null });
const page = await browser.newPage();
await page.setUserAgent(userAgent.toString());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

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

16
Июн
2020

🕵 Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2';

async function fetchProductList(url) {
    const browser = await puppeteer.launch({ 
        headless: true, // false: enables one to view the Chrome instance in action
        defaultViewport: null, // (optional) useful only in non-headless mode
    });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    ...
}

fetchProductList(url);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

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

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        async function fetchProductList(url) {
	...
    
    await page.waitFor('div[data-cel-widget^="search_result_"]');

    const result = await page.evaluate(() => {
        // counts total number of products
        let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length;

        let productsList = [];

        for (let i = 1; i < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 0 ? onlyProduct = true : emptyProductMeta = true;
            }

            let productsDetails = productNodes.map(el => el.innerText);

            if (!emptyProductMeta) {
                product.brand = onlyProduct ? '' : productsDetails[0];
                product.product = onlyProduct ? productsDetails[0] : productsDetails[1];
            }
			
            // traverse for product image
            let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`);
            product.image =rawImage ? rawImage.src : '';
			
            // traverse for product url
            let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`);
            product.url = rawUrl ? rawUrl.href : '';

            // traverse for product price
            let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`);
            product.price = rawPrice ? rawPrice.innerText : '';

            if (typeof product.product !== 'undefined') {
                !product.product.trim() ? null : productsList = productsList.concat(product);
            }
        }

        return productsList;
    });
    
    ...
}
    
...
    

После изучения DOM стало ясно, что каждый
перечисленный элемент выводится с селектором
div[data-cel-widget^="search_result_"].
Данный селектор ищет все теги div с атрибутом data-cel-widget, которые имеют
значение, начинающееся с search_result_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base (i обозначает номер узла в total listed items)
  • product: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

async function fetchProductList(url, searchTerm) {
	...
	await page.goto(url, { waitUntil: 'networkidle2' });

    await page.waitFor('input[name="field-keywords"]');
    await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm);

    await page.click('div.nav-search-submit.nav-sprite');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        const puppeteer = require('puppeteer');
const userAgent = require('user-agents');

...

const browser = await puppeteer.launch({ headless: true, defaultViewport: null });
const page = await browser.newPage();
await page.setUserAgent(userAgent.toString());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

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

16
Июн
2020

🕵 Puppeteer: парсинг сайтов с JavaScript

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

Puppeteer: не просто
очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая
командой Chrome
Devtools
.
Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор
высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и
навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому
URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2';

async function fetchProductList(url) {
    const browser = await puppeteer.launch({ 
        headless: true, // false: enables one to view the Chrome instance in action
        defaultViewport: null, // (optional) useful only in non-headless mode
    });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    ...
}

fetchProductList(url);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается
окончательным, если она имеет не более 2 подключений, работающих в течение не
менее 500 мс.

Собираем данные со страницы

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

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        async function fetchProductList(url) {
	...
    
    await page.waitFor('div[data-cel-widget^="search_result_"]');

    const result = await page.evaluate(() => {
        // counts total number of products
        let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length;

        let productsList = [];

        for (let i = 1; i < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 0 ? onlyProduct = true : emptyProductMeta = true;
            }

            let productsDetails = productNodes.map(el => el.innerText);

            if (!emptyProductMeta) {
                product.brand = onlyProduct ? '' : productsDetails[0];
                product.product = onlyProduct ? productsDetails[0] : productsDetails[1];
            }
			
            // traverse for product image
            let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`);
            product.image =rawImage ? rawImage.src : '';
			
            // traverse for product url
            let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`);
            product.url = rawUrl ? rawUrl.href : '';

            // traverse for product price
            let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`);
            product.price = rawPrice ? rawPrice.innerText : '';

            if (typeof product.product !== 'undefined') {
                !product.product.trim() ? null : productsList = productsList.concat(product);
            }
        }

        return productsList;
    });
    
    ...
}
    
...
    

После изучения DOM стало ясно, что каждый
перечисленный элемент выводится с селектором
div[data-cel-widget^="search_result_"].
Данный селектор ищет все теги div с атрибутом data-cel-widget, которые имеют
значение, начинающееся с search_result_. Аналогичным образом исключаем
селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base (i обозначает номер узла в total listed items)
  • product: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание:
мы ожидаем доступа к селектору именованных элементов div
[data-cel-widget^="search_result_"]
с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

async function fetchProductList(url, searchTerm) {
	...
	await page.goto(url, { waitUntil: 'networkidle2' });

    await page.waitFor('input[name="field-keywords"]');
    await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm);

    await page.click('div.nav-search-submit.nav-sprite');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем
добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу
списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в
работе

Есть
несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        const puppeteer = require('puppeteer');
const userAgent = require('user-agents');

...

const browser = await puppeteer.launch({ headless: true, defaultViewport: null });
const page = await browser.newPage();
await page.setUserAgent(userAgent.toString());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком
продуктов.


Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к
серверной платформе.

Заключение

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

11
Июн
2020

⏳ 10 способов ускорить загрузку вашего сайта

Веб-проект, над которым вы работаете, стал медленнее загружаться? Рассказываем о десятке моментов, на которые стоит обратить внимание, чтобы сократить время загрузки страниц.

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

1. Используйте CDN

CDN (Content Delivery Network) – географически распределённая сетевая инфраструктура, которая оптимизирует доставку контента конечным пользователям, давая доступ к сотням серверов по всему миру, размещающих копию
вашего сайта.

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

2. Включите gzip-сжатие

В некоторых CDN gzip-сжатие можно включить флажком Enable
compression
. Сжатие файлов обеспечит более быструю загрузку контента пользователями сайта.

3. Используйте оптимизацию изображений

Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться
Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться

Распространённая причина долгой загрузки страниц – избыточное качество изображений или выбор неподходящего формата. Можно уменьшить количество пикселей по каждой из сторон и применить сжатие изображений. Прогресс не стоит на месте – обратите внимание на формат сжатия изображений WebP.

Если у вас сайт на WordPress, можно использовать плагины, которые уменьшат размер автоматически в процессе загрузки.

4. Уменьшите количество
запросов, совершаемых страницей

Сократите число запросов, необходимых для загрузки верхней части страницы. Есть два способа:

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

5. Избегайте
перенаправлений


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

Некоторые редиректы
неизбежны, например, www -> root domain или root domain -> www, но
основная часть трафика не должна поступать через перенаправления.

6. Сократите время до первого байта

Время до первого байта (TTFB) –
это время, которое браузер тратит на ожидание данных с сервера после отправки запроса на ресурс.

На этот показатель влияют два параметра:

  1. Время, потраченное на сервере.
  2. Время, потраченное на отправку данных.

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

О втором параметре мы уже поговорили – на него мы можем повлиять, используя CDN.

7. Решите вопрос блокировки рендеринга JavaScript

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

        https://example.com/external.js
    

Можно отложить
загрузку сценариев до тех пор, пока пользователь не начнёт совершать
активные действия:

        window.addEventListener(
  'scroll',
  () =>
    setTimeout(() => {
      //insert marketing snippets here
    }, 1000),
  { once: true }
);
    

8. Минимизируйте CSS и JavaScript


Минимизация подразумевает
использование инструментов для удаления пробелов, символов перевода строки и
сокращения длины имён переменных. Как правило, это делается автоматически в рамках
процесса сборки. Есть специальные инструменты: например,
UglifyJS для JavaScript или cssnano для CSS.

9. Удалите
неиспользуемый код CSS и JavaScript

Чем больше проект, тем больше вероятность, что какой-то код стал неактуальным. И лучший способ его минимизировать и сократить время загрузки – просто удалить ненужный код.

Начиная с Chrome 59, в Chrome DevTools
можно анализировать использование JavaScript и CSS.
Для этого откройте DevTools, перейдите во вкладку Console, нажмите на три точки и
откройте Coverage. При нажатии кнопки со значком перезагрузки будет проведен аудит использования CSS и JavaScript. Помеченные красным блоки кода загружаются, но не используются.

Аудит использования CSS и JS
Аудит использования CSS и JS
Примечание
Об использовании средств DevTools для ускорения загрузки сайта читайте в нашем подробном мануале «Ускоряем загрузку сайта с помощью Chrome DevTools».

10. Регулярно
отслеживайте скорость загрузки сайта

Если вы возьмете за
правило обращать внимание на проверку скорости загрузки, это станет гораздо меньшей проблемой.

Есть бесплатные
инструменты для мониторинга скорости, например,
WebPageTest и Google Lighthouse. Но нужно не забывать запускать их до и после внесения изменений. Для автоматизации запуска GoogleLighthouse можно использовать PerfBeacon.

Заключение

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

Другие материалы по теме

11
Май
2020

Как сделать галерею в стиле Instagram

Галереи картинок с горизонтальной прокруткой выглядят современно и узнаваемо. Рассказываем, как с помощью CSS и JS перенести знакомую пользователям механику в ваши веб-приложения.

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

Теория: горизонтальные списки и галерея

Приведённая ниже анимация иллюстрирует горизонтальный список со свободным скроллингом. Структуру можно свободно прокручивать влево и вправо. Список можно слегка оттягивать на крайних элементах. Это даёт подсказку пользователю о границах объекта.

Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)
Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)

Горизонтальный список со снэппингом – почти то же самое, но текущий элемент списка при тапе заменяется на следующий.

Горизонтальный список со снэппингом (snapping horizontal lists)
Горизонтальный список со снэппингом (snapping horizontal lists)

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

Третий рассматриваемый элемент – галерея. Она похожа на snapping-списки, но каждый элемент занимает всё поле, как в обычном посте Instagram из нескольких фотографий. Под картинками расположен ряд точек – по одной для каждого изображения. Пользователю понятно, что есть другие изображения и какое из них видно в данный момент.

Пример галереи
Пример галереи

Практическая часть

1. Горизонтальные списки со свободной прокруткой

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

            .list {
  display: flex;
  padding: 20px;
  overflow-x: scroll;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) { margin-right: 10px; }
        

Интерактивное представление и полный код доступны в в
сэндбоксе:


Работает, но
можно улучшить.

Поведение при прокрутке влево. В некоторых браузерах свайп влево повторяет действие кнопки «Назад». Такое поведение можно предотвратить, установив для overscroll-behavior значение contain.

Полосу прокрутки можно скрыть, установив overflow-x в auto. Однако при скроллинге полоса вновь появится. Чтобы скрыть полностью, можно установить scrollbar-width в none. На момент написания статьи это работало только в Firefox. Для других браузеров придется добавить трюкачества в CSS:

            .list { -ms-overflow-style: none; }
.list::-webkit-scrollbar { display: none; }
        

Плавная прокрутка. В iOS не хватает
стандартной плавной прокрутки (momentum scrolling). Потребуется добавить -webkit-overflow-scrolling:
touch;
.

Предотвращение
вертикальной прокрутки
. Мы можем прокручивать
страницу по вертикали. Эту возможность лучше
отключить для пользователей мобильных устройств, добавив к списку touch-action:
pan-x
. Однако если список покрывает всю видимую область просмотра, а на странице есть что-то ещё, эти элементы станут недоступны. То есть такой вариант надо использовать с осторожностью.

В результате наших преобразований соответствующая часть CSS теперь будет иметь следующий вид:

            .list {
  display: flex;
  overflow-x: scroll;
  padding: 20px;

  /* Предотвращает случайное использование "Назад"*/
  overscroll-behavior: contain;

  /* Скрывает полосу прокрутки */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Плавная прокрутка на iOS */
  -webkit-overflow-scrolling: touch;

  /* Отключает вертикальный скроллинг для тач-устройств */
  touch-action: pan-x;
}

/* Скрывает полосу прокрутки  */
.list::-webkit-scrollbar {
  display: none;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) {
  margin-right: 10px;
}

/* фикс для отступов в конце списка */
.item:last-child {
  position: relative;
}

.item:last-child::after {
  position: absolute;
  left: 100%;
  height: 1px;
  width: 20px;
  display: block;
  content: "";
}

        

А результат выглядит вот так (там же полный код примера):


2. Горизонтальный список со снэппингом

Для описания этого типа списков отталкиваемся от списка со свободной прокруткой. Сначала сообщаем списку, когда нужно останавливаться:

            .list {
  scroll-snap-type: x mandatory;
}
        

Элементам списка добавляем опцию scroll-snap-align: start. Чтобы дать подсказку, следующий элемент может немного «выглядывать» из-за края экрана. Для этого достаточно установить отступ прокрутки: scroll-padding-inline-start:
20px
. Если хочется свайпнуть
одним движением несколько элементов,
можно добавить scroll-snap-stop: always к элементам списка, но не все браузеры пока
такое поддерживают.

Код полностью также выложен на Codesandbox:


3. Галерея в стиле Instagram

Если мы сделаем наш
предыдущий список таким же широким, как область прокрутки, и удалим отступы, он будет выглядеть и вести себя примерно так же, как галерея
Instagram. За исключением маленьких точек. Без точек будет выглядеть так:


Но как мы поясняли в теоретической части, эти маленькие точки – полезный графический приём. Получается, что нам нужно следить за тем, какой из элементов списка сейчас виден. Наиболее простой способ – использовать IntersectionObserver.

Составим список
точек, каждая из которых соответствует одной из картинок. Когда она пролистывается в списке, мы получаем индекс элемента и устанавливаем
индикаторную точку с соответствующим индексом в активное положение.

Вот как это будет
выглядеть:

            // ссылки на элементы DOM
const list = document.querySelector('.list');
const items = Array.from(document.querySelectorAll('.item'));
const indicators = Array.from(document.querySelectorAll('.indicator'));

// создание наблюдателя
const observer = new IntersectionObserver(onIntersectionObserved, {
  root: list,
  threshold: 0.6
});

// наблюдаем за каждым элементом
items.forEach(item => {
  observer.observe(item);
});

// когда observer обнаруживает изменение записи
// (пункт, входящий в список)
// и эта запись пересекается,
// получаем индекс пересекающегося элемента
// устанавливаем нужный индикатор в активное положение
function onIntersectionObserved(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const intersectingIndex = items.indexOf(entry.target);
      activateIndicator(intersectingIndex);
    }
  });
}

function activateIndicator(index) {
  indicators.forEach((indicator, i) => {
    indicator.classList.toggle('active', i === index);
  });
}
        

Полный результат также доступен в сэндбоксе:


Примечание
Мы установили пороговое значение 0.6. То есть если 60% элемента находится в поле зрения, то мы считаем его «пересекающимся». Если установим значение равным 1, то пересекаемым будет считать только полностью видимый элемент.

Заключение

Мы разобрали несколько
вариаций построения горизонтальных списков изображений. У приведённых решений есть и свои недостатки, и преимущества. Поскольку используется нативная
прокрутка, нет возможности настроить способ движения, мы не можем
контролировать «липкость» ползунка (в этом случае правильнее было бы использовать решение на
JavaScript). Кроме того, в нашем решении мы использовали не самые стандартные подходы в CSS. Однако объём кода невелик, и он работает даже в старых браузерах.

11
Май
2020

📸 Как сделать галерею в стиле Instagram

Галереи картинок с горизонтальной прокруткой выглядят современно и узнаваемо. Рассказываем, как с помощью CSS и JS перенести знакомую пользователям механику в ваши веб-приложения.

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

Теория: горизонтальные списки и галерея

Приведённая ниже анимация иллюстрирует горизонтальный список со свободным скроллингом. Структуру можно свободно прокручивать влево и вправо. Список можно слегка оттягивать на крайних элементах. Это даёт подсказку пользователю о границах объекта.

Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)
Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)

Горизонтальный список со снэппингом – почти то же самое, но текущий элемент списка при тапе заменяется на следующий.

Горизонтальный список со снэппингом (snapping horizontal lists)
Горизонтальный список со снэппингом (snapping horizontal lists)

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

Третий рассматриваемый элемент – галерея. Она похожа на snapping-списки, но каждый элемент занимает всё поле, как в обычном посте Instagram из нескольких фотографий. Под картинками расположен ряд точек – по одной для каждого изображения. Пользователю понятно, что есть другие изображения и какое из них видно в данный момент.

Пример галереи
Пример галереи

Практическая часть

1. Горизонтальные списки со свободной прокруткой

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

            .list {
  display: flex;
  padding: 20px;
  overflow-x: scroll;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) { margin-right: 10px; }
        

Интерактивное представление и полный код доступны в в
сэндбоксе:


Работает, но
можно улучшить.

Поведение при прокрутке влево. В некоторых браузерах свайп влево повторяет действие кнопки «Назад». Такое поведение можно предотвратить, установив для overscroll-behavior значение contain.

Полосу прокрутки можно скрыть, установив overflow-x в auto. Однако при скроллинге полоса вновь появится. Чтобы скрыть полностью, можно установить scrollbar-width в none. На момент написания статьи это работало только в Firefox. Для других браузеров придется добавить трюкачества в CSS:

            .list { -ms-overflow-style: none; }
.list::-webkit-scrollbar { display: none; }
        

Плавная прокрутка. В iOS не хватает
стандартной плавной прокрутки (momentum scrolling). Потребуется добавить -webkit-overflow-scrolling:
touch;
.

Предотвращение
вертикальной прокрутки
. Мы можем прокручивать
страницу по вертикали. Эту возможность лучше
отключить для пользователей мобильных устройств, добавив к списку touch-action:
pan-x
. Однако если список покрывает всю видимую область просмотра, а на странице есть что-то ещё, эти элементы станут недоступны. То есть такой вариант надо использовать с осторожностью.

В результате наших преобразований соответствующая часть CSS теперь будет иметь следующий вид:

            .list {
  display: flex;
  overflow-x: scroll;
  padding: 20px;

  /* Предотвращает случайное использование "Назад"*/
  overscroll-behavior: contain;

  /* Скрывает полосу прокрутки */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Плавная прокрутка на iOS */
  -webkit-overflow-scrolling: touch;

  /* Отключает вертикальный скроллинг для тач-устройств */
  touch-action: pan-x;
}

/* Скрывает полосу прокрутки  */
.list::-webkit-scrollbar {
  display: none;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) {
  margin-right: 10px;
}

/* фикс для отступов в конце списка */
.item:last-child {
  position: relative;
}

.item:last-child::after {
  position: absolute;
  left: 100%;
  height: 1px;
  width: 20px;
  display: block;
  content: "";
}

        

А результат выглядит вот так (там же полный код примера):


2. Горизонтальный список со снэппингом

Для описания этого типа списков отталкиваемся от списка со свободной прокруткой. Сначала сообщаем списку, когда нужно останавливаться:

            .list {
  scroll-snap-type: x mandatory;
}
        

Элементам списка добавляем опцию scroll-snap-align: start. Чтобы дать подсказку, следующий элемент может немного «выглядывать» из-за края экрана. Для этого достаточно установить отступ прокрутки: scroll-padding-inline-start:
20px
. Если хочется свайпнуть
одним движением несколько элементов,
можно добавить scroll-snap-stop: always к элементам списка, но не все браузеры пока
такое поддерживают.

Код полностью также выложен на Codesandbox:


3. Галерея в стиле Instagram

Если мы сделаем наш
предыдущий список таким же широким, как область прокрутки, и удалим отступы, он будет выглядеть и вести себя примерно так же, как галерея
Instagram. За исключением маленьких точек. Без точек будет выглядеть так:


Но как мы поясняли в теоретической части, эти маленькие точки – полезный графический приём. Получается, что нам нужно следить за тем, какой из элементов списка сейчас виден. Наиболее простой способ – использовать IntersectionObserver.

Составим список
точек, каждая из которых соответствует одной из картинок. Когда она пролистывается в списке, мы получаем индекс элемента и устанавливаем
индикаторную точку с соответствующим индексом в активное положение.

Вот как это будет
выглядеть:

            // ссылки на элементы DOM
const list = document.querySelector('.list');
const items = Array.from(document.querySelectorAll('.item'));
const indicators = Array.from(document.querySelectorAll('.indicator'));

// создание наблюдателя
const observer = new IntersectionObserver(onIntersectionObserved, {
  root: list,
  threshold: 0.6
});

// наблюдаем за каждым элементом
items.forEach(item => {
  observer.observe(item);
});

// когда observer обнаруживает изменение записи
// (пункт, входящий в список)
// и эта запись пересекается,
// получаем индекс пересекающегося элемента
// устанавливаем нужный индикатор в активное положение
function onIntersectionObserved(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const intersectingIndex = items.indexOf(entry.target);
      activateIndicator(intersectingIndex);
    }
  });
}

function activateIndicator(index) {
  indicators.forEach((indicator, i) => {
    indicator.classList.toggle('active', i === index);
  });
}
        

Полный результат также доступен в сэндбоксе:


Примечание
Мы установили пороговое значение 0.6. То есть если 60% элемента находится в поле зрения, то мы считаем его «пересекающимся». Если установим значение равным 1, то пересекаемым будет считать только полностью видимый элемент.

Заключение

Мы разобрали несколько
вариаций построения горизонтальных списков изображений. У приведённых решений есть и свои недостатки, и преимущества. Поскольку используется нативная
прокрутка, нет возможности настроить способ движения, мы не можем
контролировать «липкость» ползунка (в этом случае правильнее было бы использовать решение на
JavaScript). Кроме того, в нашем решении мы использовали не самые стандартные подходы в CSS. Однако объём кода невелик, и он работает даже в старых браузерах.

21
Апр
2020

В чем разница между интернет-магазином по сравнению с обычным сайтом?

Рассмотрим в статье в чем же разница между интернет-магазином и обычным сайтом. В внешнем мире интернет-магазин практически всегда  противопоставляется сайту. Но на самом деле различие у них только одно. И никакого взаимоисключения в нём не содержится. Конечно, наличие каталога товаров — это базовое различие. Но оно не единственное, разработка интернет магазина наиболее сложна чем обычного сайта. Следующие различия, которые мы рассмотрим, позволят нам… Читать далее »

Запись В чем разница между интернет-магазином по сравнению с обычным сайтом? впервые появилась Индиго.

13
Апр
2020

Docker: Удалить Контейнер — Удалить Все Контейнеры

Чтобы удалить конкретный Docker-контейнер в первую очередь необходимо узнать его CONTAINER ID или NAME сделав листинг всех Docker-контейнеров. Когда контейнер для удаления определен он может быть удален с помощью команды docker rm. Также из командной строки можно удалить только остановленные (неиспользуемые) Docker-контейнеры или, если необходимо, вы можете принудительно удалить все Docker-контейнеры Дельный Совет: Очистите Docker-хост удалив ненужные Docker-образы! Процесс аналогичен удалению Docker-контейнеров! Читать далее → Список Docker-контейнеров… Читать далее »

Запись Docker: Удалить Контейнер — Удалить Все Контейнеры впервые появилась Индиго.

27
Мар
2020

Как сделать кликабельный телефон на сайте

С каждым годом стремительно растет количество интернет-пользователей, использующих мобильные платформы. И чтобы не потерять своего клиента, необходимо предоставить посетителю возможность позвонить прямо сейчас. Современные мобильные браузеры могут автоматически делать номер телефона на сайте кликабельным, но это работает не всегда (из-за разных написаний номера телефона). Наверное, многие из вас сталкивались с такой ситуацией, когда, заказывая какой-либо товар в интернет-магазине, приходилось уточнять… Читать далее »

Запись Как сделать кликабельный телефон на сайте впервые появилась Индиго.

13
Фев
2020

«Каждый охотник желает знать»: теория цвета для веба и приложений

Рассказываем, как правильно подбирать и комбинировать цвета. Цветовой круг, модель HSL, цветовые схемы и психология, советы и правила. Показываем на примерах, как применять в CSS.

12
Фев
2020

Секреты правильной растяжки: адаптивные графики в вебе без клиентского JavaScript

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

Существует кучах библиотек для создания графиков и диаграмм в вебе. Можно даже имитировать отрисовку диаграмм «от руки». Обычно данные для графиков запрашиваются из сети или сама картинка рендерится на canvas – кажется, что без JavaScript не обойтись.

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

Ещё одна проблема – адаптивность. Графики необходимо перерисовывать при ресайзе, иначе они «поломаются». Это создаёт лишнюю работу и для браузера, и для программиста. Особенно если используются низкоуровневые библиотеки вроде D3.

Возьмём для примера SVG-графики из статьи New York Times и подумаем, можем ли построить их без JS.


Все методы, используемые в статье, собраны в экспериментальный компонент Pancake для фреймворка Svelte.

Проблема

Создать линейный график на SVG не так уж и сложно. Допустим, есть вот такие данные:

            const data = [
  { x: 0,  y: 0 },
  { x: 1,  y: 1 },
  { x: 2,  y: 4 },
  { x: 3,  y: 9 },
  { x: 4,  y: 16 },
  { x: 5,  y: 25 },
  { x: 6,  y: 36 },
  { x: 7,  y: 49 },
  { x: 8,  y: 64 },
  { x: 9,  y: 81 },
  { x: 10, y: 100 }
];
        

Требуется вывести результат в область размером 300×100 пикселей.

Чтобы получить координаты опорных точек, нужно умножить x на 30, а y вычесть из 100:

            <polyline points="
  0,0
  30,99
  60,96
  90,91
  120,84
  150,75
  180,64
  210,51
  240,36
  270,19
  300,0
"></polyline>
        

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

            function scale(domain, range) {
  const m = (range[1] - range[0]) / (domain[1] - domain[0]);
  return num => range[0] + m * (num - domain[0]);
}

const x = scale([0, Math.max(...data.map(d => d.x))], [0, 300]);
const y = scale([0, Math.max(...data.map(d => d.y))], [100, 0]);

const points = data.map(d => `${x(d.x)},${y(d.y)}`).join(' ');

const chart = `
<svg width="300" height="100">
  <polyline points="${points}"></polyline>
</svg>
`;
        

Добавив в svg пару осей и немного стилей, получаем график:


Живой пример

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

Однако этот чарт не адаптивен, размеры всегда составляют 300x100px. А это не то, чего мы ожидаем.

Решение. Часть 1

У SVG-элемента есть атрибут viewBox, задающий систему координат изображения, независящую от реальных размеров. По умолчанию соотношение сторон, заданное в viewBox сохраняется. Подавляется такое поведение с помощью preserveAspectRatio="none".

Добавим простую систему координат:

            <svg viewBox="0 0 100 100" preserveAspectRatio="none">
        

Что ж, теперь наш график действительно стал гибким…


Живой пример

Очевидно, это не то, что нам нужно. Символы ужасно масштабируются, и в некоторых случаях числа невозможно считать. Кроме того, варьируется толщина линии.

Вторую проблему легко решить, используя малоизвестное CSS-свойство vector-effect: non-scaling-stroke для каждого элемента.


Живой пример

Но с масштабированием текста придётся разбираться другими методами.

Решение. Часть 2

Оси графика необязательно рисовать прямо в SVG. Мы можем использовать обычные HTML-элементы и расположить их с помощью CSS. Чтобы синхронизировать масштабирование HTML- и SVG-слоев, применяем процентную координатную сетку.

            <!-- x axis -->
<div class="x axis" style="top: 100%; width: 100%; border-top: 1px solid black;">
  <span style="left: 0">0</span>
  <span style="left: 20%">2</span>
  <span style="left: 40%">4</span>
  <span style="left: 60%">6</span>
  <span style="left: 80%">8</span>
  <span style="left: 100%">10</span>
</div>

<!-- y axis -->
<div class="y axis" style="height: 100%; border-left: 1px solid black;">
  <span style="top: 100%">0</span>
  <span style="top: 50%">50</span>
  <span style="top: 0%">100</span>
</div>

<style>
  .axis {
    position: absolute;
  }

  .axis span {
    position: absolute;
    line-height: 1;
  }

  .x.axis span {
    top: 0.5em;
    transform: translate(-50%,0);
  }

  .y.axis span {
    left: -0.5em;
    transform: translate(-100%,-50%);
  }
</style>
        

График больше не ломается!


Живой пример

Ещё одно преимущество HTML-элементов – они автоматически привязываются к ближайшему пикселю. Так что не появляется неприятный эффект нечёткости, который встречается в SVG.

Готовое решение

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

            <script>
  import * as Pancake from '@sveltejs/pancake';

  const points = [
    { x: 0,  y: 0 },
    { x: 1,  y: 1 },
    { x: 2,  y: 4 },
    { x: 3,  y: 9 },
    { x: 4,  y: 16 },
    { x: 5,  y: 25 },
    { x: 6,  y: 36 },
    { x: 7,  y: 49 },
    { x: 8,  y: 64 },
    { x: 9,  y: 81 },
    { x: 10, y: 100 }
  ];
</script>

<div class="chart">
  <Pancake.Chart x1={0} x2={10} y1={0} y2={100}>
    <Pancake.Box x2={10} y2={100}>
      <div class="axes"></div>
    </Pancake.Box>

    <Pancake.Grid vertical count={5} let:value>
      <span class="x label">{value}</span>
    </Pancake.Grid>

    <Pancake.Grid horizontal count={3} let:value>
      <span class="y label">{value}</span>
    </Pancake.Grid>

    <Pancake.Svg>
      <Pancake.SvgLine data={points} let:d>
        <path class="data" {d}/>
      </Pancake.SvgLine>
    </Pancake.Svg>
  </Pancake.Chart>
</div>

<style>
  .chart {
    height: 100%;
    padding: 3em 2em 2em 3em;
    box-sizing: border-box;
  }

  .axes {
    width: 100%;
    height: 100%;
    border-left: 1px solid black;
    border-bottom: 1px solid black;
  }

  .y.label {
    position: absolute;
    left: -2.5em;
    width: 2em;
    text-align: right;
    bottom: -0.5em;
  }

  .x.label {
    position: absolute;
    width: 4em;
    left: -2em;
    bottom: -22px;
    font-family: sans-serif;
    text-align: center;
  }

  path.data {
    stroke: red;
    stroke-linejoin: round;
    stroke-linecap: round;
    stroke-width: 2px;
    fill: none;
  }
</style>
        

Живой пример

Благодаря Svelte этот чарт отрендерить на сервере с помощью NodeJS или вставить в DOM на клиенте. Для больших и сложных интерактивных графиков может потребоваться одновременно и то, и другое. Основную разметку можно генерировать на сервере, а затем прогрессивно улучшать на клиенте, добавляя интерактивность. Без компонентных фреймворков типа Svelte это было бы непросто.

Обратите внимание, Pancake не создаёт элементы span и path внутри SVG-изображения. Компоненты прежде всего логические – вы детально контролируете представление.

Усложняем задачу

Мы можем сделать гораздо больше, чем простейшие линейные графики:


Например, очень интересны диаграммы рассеивания (scatterplots).

Живой пример

Мы не можем использовать для них элементы circle, так как они могут некорректно масштабироваться. Компонент Pancake.Scatterplot генерирует path, состоящий из несвязанных дуг с нулевым радиусом. Размер окружностей определяется шириной контура (stroke width).

Благодаря тому, что библиотека оформлена в виде компонента Svelte, мы легко можем добавить графикам немного интерактива.

Живой пример

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

            <Pancake.SvgScatterplot data={points} let:d>
  <path class="data" {d}/>
</Pancake.SvgScatterplot>

<Pancake.Quadtree data={points} let:closest>
  {#if closest}
    <Pancake.SvgPoint x={closest.x} y={closest.y} let:d>
      <path class="highlight" {d}/>
    </Pancake.SvgPoint>
  {/if}
</Pancake.Quadtree>
        

В будущем в библиотеке появится поддержка canvas (2D и WebGL). Конечно, графики на canvas не смогут работать без JavaScript, но при работе с большими наборами данных SVG может не справляться.

Предостережения

Помните, Pancake является экспериментальной библиотекой, у него ещё не очень много реальных кейсов.

Основное внимание уделено управлению системой координат для двумерных чартов. Для графиков, гистограмм и диаграмм рассеивания этого достаточно, но круговые диаграммы, к сожалению, не поддерживаются.

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

***

Название Pancake (Блин) связано с тем, что фактически чарты строятся путём наложения слоёв друг на друга. Проект во многом вдохновлялся фреймворком Layer Cake и библиотекой D3.

А какие инструменты вы применяете для построения графиков?

06
Фев
2020

Нет времени объяснять! Пишем таймер обратного отсчёта на чистом CSS

Ради фана реализуем динамический виджет таймера на одних стилях. Для порядка приводим ещё и решение на JavaScript (но это не главное).

CSS – мощный инструмент современного разработчика. Он многое умеет, в нём есть переменные, функции, наследование и ещё много крутых штук. Но всё-таки это не язык программирования – у него совсем другая сфера ответственности. Тем интереснее использовать CSS для решения задач программирования 🙂 Именно этим сегодня и займёмся.

Дисклеймер! Многие вещи в принципе невозможно сделать на CSS. Ещё больше вещей делать на CSS нерационально. Мы занимаемся этим только из болезненного любопытства и стремления познать все скрытые возможности инструмента.

Условие задачи

Нужно сделать таймер обратного отсчёта. Предъявляемые требования:

  • Таймер должен выводить миллисекунды от 99 до 0.
  • Когда остаётся меньше 10 миллисекунд, нужно выводить только одну цифру (от 9 до 0) и центрировать её.
  • Бонус #1: Цвет шрифта и фона можно настраивать в процессе работы (без кусочка JS не обойтись).
  • Бонус #2. После остановки таймера его можно перезапустить.
  • Код должен работать и на ПК, и на мобильных устройствах.

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

Да, не очень элегантно. Но сработает, вот увидите.

Что нам потребуется?

  1. CSS трансформации
  2. CSS анимации
  3. Flexbox-модель
  4. CSS переменные
  5. Различные селекторы

Вот что получится в итоге:

Реализация на JavaScript

Сразу посмотрим, как это можно было сделать на JS.

Простой и понятный код, состоящий из пары функций. Для обновления таймера подписываемся на момент перерисовки браузера с помощью метода window.requestAnimationFrame().

            let end;
const now = Date.now;
const timer = document.getElementById("timer");
const duration = 9900;

function displayCountdown() {
  const count = parseInt((end - now()) / 100);
  timer.textContent =
    count > 0 ? (window.requestAnimationFrame(displayCountdown), count) : 0;
}

function start() {
  end = now() + duration;
  window.requestAnimationFrame(displayCountdown);
}
        

Всего пара блоков в HTML:

            <div class="timer-container">
  <p class="timer" id="timer">99</p>
</div>
        

И элементарные стили для выравнивания:

            .timer-container {
  display: flex;
  height: 100vh; 
}

.timer {
  margin: auto;
}
        

Согласитесь, ровным счётом ничего интересного. Поэтому бросаем эту ерунду и идём писать по-настоящему крутой таймер.

Общий подход

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

            <div class="timer">
  <div class="digit seconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span>
    <span>0</span> 
  </div><div class="digit milliseconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span> 
    <span>0</span>
  </div>
</div>
        

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

CSS трансформации

Большинство CSS-свойств плохо подходят для анимирования, так как их изменение вызывает перерисовку страницы. Но есть два «безопасных» свойства, которыми мы можем воспользоваться: transform и opacity.

Подробнее об анимации в вебе можно прочитать в замечательном руководстве High Performance Animations.

Для оживления таймера возьмём надёжное свойство translateY. Оно обеспечит перемещение блока только по y-оси.

            .selector {
  transform: translateY(0);
}
        

Можно воспользоваться и полным свойством translate, но помните, что его первый аргумент соответствует x-координате. Если хотите перемещать элемент только по вертикали, то передайте первым параметром 0.

            .selector {
  transform: translate(3em);
}

/* то же самое */
.selector {
  transform: translate(3em, 0);
}
        

Чтобы лучше понять функции трансформации, загляните в спецификацию CSS Transforms Module Level 1. Там всё разобрано на подробных примерах, так что вы разберётесь, даже если не очень любите математику.

CSS animations

Следующий шаг – анимировать применение трансформаций. Для этого мы используем CSS-анимации.

Самое важное правило, которое вы должны знать, – это @keyframes. Оно позволяет разбить анимацию на кадры и описать каждый из них в отдельности.

            @keyframes seconds {
  0% { transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { 
    transform: translateY(-10em);
    width: 0;
  }
  100% { 
    transform: translateY(-10em);
    width: 0;
  }
}

@keyframes milliseconds {
  0% {transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { transform: translateY(-9em) }
  100% { transform: translateY(-9em) }
}
        

Здесь мы создали две анимации – по одной для каждого блока с цифрами.

Обратите внимание на два последних кадра в анимации первого блока. В этот момент там должна отображаться цифра 0, но она нам не нужна, поэтому скрываем её с помощью width: 0.

Чтобы применить анимации, используем краткий синтаксис свойства animation:

            .seconds {
  animation: seconds 10s 1 step-end forwards;
}

.milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
        

Если вы зайдете в панель инструментов разработчика и откроете вкладку с вычисленными значениями (computed), то увидите, что вместо одного свойства animation к элементу применились сразу несколько:


animation-name

Имя анимации, использующееся для её идентификации. Для него можно использовать латинские буквы, цифры, нижнее подчёркивание и дефисы. Первой должна идти буква. В начале не могут стоять зарезервированные слова none, unset, initial или inherit, а также сочетание --. Регистр символов имеет значение.

animation-duration

Продолжительность одного цикла анимации. Для первой колонки цифр анимация будет длиться 10 секунд (10s). Вторая колонка двигается в 10 раз быстрее (1s).

animation-iteration-count

Количество циклов анимации, которое должно выполниться до ее остановки. Первую колонку нужно прокрутить лишь один раз – от 9 до 0. Вторую – целых 10 раз, по одному на каждое положение первой колонки.

animation-timing-function

Это свойство описывает прогресс анимации в течение одного цикла. Если вы знакомы с кривыми Безье, то можете контролировать его до мельчайших подробностей с помощью функции cubic-bezier(). Для простых смертных есть несколько готовых значений animation-timing-function, обозначенных ключевыми словами (ease, ease-in и т.д)

Нам же больше подойдёт значение step-end. Это то же самое, что и steps(1, jump-end).

Функция steps() разбивает анимацию на равные «шаги», то есть величина изменяется не плавно, а прерывисто. Первый аргумент – количество шагов, второй – момент, когда начинается анимация. Ключевое слово jump-end означает, что анимация запускается в конце, а не начале каждого шага.

Чтобы лучше разобраться в этой функции, обратитесь к статье Дэна Уилсона Jumps: The New Steps() in Web Animation.

animation-fill-mode

Состояние целевого объекта до и после завершения анимации. Нам требуется, чтобы колонки останавливались на последней цифре (последний ключевой кадр), поэтому используем значение forwards.

Когда анимация остановится, первая цифра будет скрыта, а вторая колонка замрёт на позиции -9em.

Ещё больше о CSS анимациях вы можете узнать в спецификации CSS Animations Level 1.

Flexbox

Цифру 0 в первом разряде мы уже скрыли с помощью инструкции @keyframes, осталось только выровнять таймер по центру страницы:

            .timer-container {
  display: flex;
  height: 100vh; 
}

.timer {
  overflow: hidden;
  margin: auto;
  height: 1em;
  width: 2ch;
  text-align: center;
}

.digit {
  display: inline-block;
}

.digit span {
  display: block;
  width: 100%;
  height: 1em;
}
        

Для вертикального выравнивания используем автоматический расчёт маргинов у потомка флекс-контейнера. Другими словами, назначаем display: flex родительскому блоку, и margin: auto дочернему.

Горизонтальное выравнивание достигается обычным text-align: center.

Больше информации о Flex-модели – в спецификации CSS Flexible Box Layout Module Level 1.

Бонус #1: Динамическое изменение цвета

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

Используем для ее решения кастомные свойства CSS и инпут выбора цвета.

Положим цвета в переменные:

            :root {
  --fontColour: #000000;
  --bgColour: #ffffff;
}
        

И используем их в таймере:

            .timer {
  background-color: var(--bgColour, white);
}

.digit {
  color: var(--fontColour, black);
}
        

Добавим на страницу два инпута для изменения цветовой схемы:

            <aside>
  <label>
    <span>Font colour:</span>
    <input id="fontColour" type="color" value="#000000" />
  </label>
  <label>
    <span>Background colour:</span>
    <input id="bgColour" type="color" value="#ffffff" />
  </label>
</aside>
        

Теперь придётся написать пару строк JS-кода, чтобы динамически изменять переменные цветов при изменении инпутов:

            let root = document.documentElement;
const fontColourInput = document.getElementById('fontColour');
const bgColorInput = document.getElementById('bgColour');

fontColourInput.addEventListener('input', updateFontColour, false);
bgColorInput.addEventListener('input', updateBgColour, false);

function updateFontColour(event) {
  root.style.setProperty('--fontColour', event.target.value);
}

function updateBgColour(event) {
  root.style.setProperty('--bgColour', event.target.value);
}
        

Да, не всё можно решить на чистом CSS.

Бонус #2: Перезапуск таймера

Сейчас таймер отрабатывает только один раз. Чтобы запустить его сначала, необходимо перезагрузить страницу. Давайте добавим возможность перезапуска.

Возможно, вы подумали, что нам придётся снова смошенничать и добавить JavaScript? А вот и нет, мы справимся своими силами!

Воспользуемся чекбоксом, который обладает состоянием checked. Мы легко можем получить к нему доступ из CSS.

            .toggle span {
  font-size: 1.2em;
  padding: 0.5em;
  background-color: palegreen;
  cursor: pointer;
  border-radius: 4px;
}

input[type="checkbox"] {
  opacity: 0;
  position: absolute;
}

 input[type="checkbox"]:checked ~ aside .toggle span:first-of-type {
  display: none;
}

.toggle span:nth-of-type(2) {
  display: none;
}

input[type="checkbox"]:checked ~ aside .toggle span:nth-of-type(2) {
  display: inline;
}
        

Теперь анимация работает только при включенном чекбоксе, а при выключенном таймер сбрасывается и возвращается в исходное состояние.

            input[type="checkbox"]:checked ~ .timer .seconds {
  animation: seconds 10s 1 step-end forwards;
}

input[type="checkbox"]:checked ~ .timer .milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
        

Важно, чтобы чекбокс находился на одном уровне с таймером и стоял в разметке перед ним. Это даст нам возможность воспользоваться селектором “сиблингов” (~). А лейбл для него может находиться где угодно.

***

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

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

Если вы любите CSS, вот ещё кое-что:

В комментариях делитесь самыми нестандартными решениями ваших задач.

20
Янв
2020

Графический приём, благодаря которому пользователи будут регистрироваться чаще

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

При разработке приложения мы прикладываем все усилия к со…

17
Янв
2020

Веб-скрапинг по расписанию с Django и Heroku

оздаём Django-приложение, ежедневно проверяющее доску объявлений о работе. Парсим в BeautifulSoup, сохраняем в PostgreSQL, развёртываем на сервере Heroku.

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

31
Окт
2019

WebMoney Domains

В настоящее время существует много интересных доменных зон, некоторые из них были и раньше. У большинства сервисов позволяющих регистраторовать на такие зоны большие скидки и на какой-то момент можно поверить, чтобы были созданы дешёвые и доступные зоны для всех пользователей для любых нужд, но на самом деле это и близко не так. На днях столкнулся с интересным сервисом от webmoney… Читать далее »

Запись WebMoney Domains впервые появилась Индиго.

13
Сен
2019

Поддержка сайта

С момента появления таких сервисов как narod.ru, Google Sites или Front Page, которые позволяли «склепать» самостоятельно простой сайт с использованием элементарных шаблонов, многие создавали себе домашние странички, где рассказывали о себе, о своих услугах, хобби, достижениях. Однако, для рекламы коммерческих услуг такое решение не подходило и стали обращаться к профессионалам, которые предлагали комплекс услуг по созданию сайта. Так было до тех… Читать далее »

Запись Поддержка сайта впервые появилась Индиго.

05
Сен
2019

Что такое IT-аутсорсинг и способы организации

За последние несколько лет IT-аутсорсинг прочно вошел в экономическую жизнь общества и взаимоотношения в бизнесе. Использование сторонних сервисов для решения задач в самом широком спектре информационных технологий можно назвать трендом нашего времени Выгоды и преимущества IT-аутсорсинга Более высокое качество услуг при относительно низкой стоимости. Возможность в одной фирме решить все необходимые задачи по созданию и обслуживанию IT-инфраструктуры. Абонентские услуги в… Читать далее »

Запись Что такое IT-аутсорсинг и способы организации впервые появилась Индиго.

30
Авг
2019

Список лучших CSS фреймворков для фронтенд-разработки

Список лучших CSS фреймворков для фронтенд-разработкиМечтаете быстро создавать прототипы приложений? Упростите себе задачу с помощью этих CSS фреймворков. И не придётся беспокоится о стилях. Зачем использовать CSS фреймворки при написании пользовательского CSS-кода? Время – ограниченный ресурс, поэтому тратьте его на создание функциональности. Улучшите ли производительность и эстетику, когда создадите CSS с нуля? Абсолютно! Но когда нет творческих способностей в области […]

Запись Список лучших CSS фреймворков для фронтенд-разработки впервые появилась Библиотека программиста.

26
Авг
2019

Мастер отзывчивого дизайна за 5 минут: 4 секретные техники

Современный веб-сайт должен хорошо выглядеть в любых условиях. Разбираемся в основах отзывчивого дизайна и учим страницу подстраиваться. Что такое отзывчивый дизайн?  Это способность вашего сайта подстраиваться под изменение внешних условий, например, ширины браузера, без потери визуальной привлекательности. Смотрите небольшой ликбез по отзывчивости и адаптивности в вебе. Зачем нужен отзывчивый дизайн?  Чтобы пользователи могли с одинаковым […]

Запись Мастер отзывчивого дизайна за 5 минут: 4 секретные техники впервые появилась Библиотека программиста.