Full stack проект на Go за неделю. День 2: Разработка бэкенд-сервера

Full stack проект на Go за неделю. День 2: Разработка бэкенд-сервера

Это вторая часть материала. Первая часть.

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

Выполнение задач

При разработке серверного приложения на Go, имейте в виду, что оно выполняется постоянно (есть исключения, но о них не будем говорить сейчас).
В предыдущей части мы говорили о том, что необходимо загружать курсы валют один раз в час — это очень простая задача для Go. С помощью стандартной библиотеки это можно сделать так:

go func() {
for {
updateCurrencyRates()
time.Sleep(1 * time.Hour)
}
}()

Не будем пока вдаваться в детали, просто знайте, что метод updateCurrencyRates() будет вызываться каждый час. Вот так просто.

Выбор языка программирования

Буду краток. Уже долгое время я с удовольствием программирую на Go — особенно он хорош в написании сервисов типа этого, поэтому и остановимся на Go 🙂

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

  • PHP
  • Ruby (Ruby on Rails)
  • Node.js
  • Java
  • Scala
  • ASP.NET

… а на самом деле их намного больше. И у всех есть свою плюсы и минусы.

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

Разработка

Здесь мы опишем только часть реализованного функционала, весь код этой части можно найти здесь. И так как наш проект написан как стандартный проект Go, его можно установить простой командой:

go get github.com/goingfullstack/currencyconverter

Структура проекта:

.
├── main.go
└── server
├── currency.go
├── data.go
├── handlers.go
├── server.go
└── webhook.go

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

В модуле server.go мы задаём константами параметры запуска нашего сервиса — адрес и порт, при этом устанавливаем их по-умолчанию на адрес http://127.0.0.1:4000. Вот структура нашего сервера:

type Server struct {
host string
port int

hasCurrencies bool // true если валюты успешно получены и обработаны
lastUpdateTime time.Time // время, взятое из данных в файле ECB
currencies map[string]float64 // данные валюты

mutex *sync.Mutex // для блокировки при использовании веб-хуков
webhooks map[string]webhook // для хранения веб-хуков
}

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

func New() (s *Server, err error) {
// берём данные из переменных среды
host := getEnv(HostEnvironment, defaultHost)
portStr := getEnv(PortEnvironment, defaultPort)
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("Error parsing port number: {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s", portStr)
}

// initialize internal variables
return &Server{
host: host,
port: port,

hasCurrencies: false,

mutex: &sync.Mutex{},
webhooks: make(map[string]webhook),
}, nil
}

И самая важная функция запуска сервера Run — после запуска сервера возвращает ошибки из http.ListenAndServe:

func (s *Server) Run() (err error) {
log.Printf("Starting server on {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d\n", s.host, s.port)

// запускает подзадачу обновления валют
s.startCurrencyUpdating()
return http.ListenAndServe(fmt.Sprintf("{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d", s.host, s.port), s)
}

Рассмотрим пару важных методов работы с данными — startCurrencyUpdating() и parseCurrencyData() в файле data.go:

func (s *Server) startCurrencyUpdating() {
log.Println("Starting currency fetching...")
go func() {
for {
log.Println("Starting new currency fetch...")

// устанавливаем время паузы
napTime := successSleepTime

if data, err := fetchCurrencyData(); err == nil {
if ts, curr, err2 := parseCurrencyData(data); err2 == nil {
// всё хорошо - обновляем данные по валютам
// заблокируем, когда закончим
s.mutex.Lock()
s.hasCurrencies, s.lastUpdateTime, s.currencies = true, ts, curr
s.mutex.Unlock()

log.Println("Currencies updated.")

// вызываем веб-хуки
go s.callWebhooks()
} else {
// ошибка - запишем в лог и уменьшим время до след. запуска
log.Println("Error parsing currency data:", err)
napTime = errorSleepTime
}
} else {
// ошибка - запишем в лог и уменьшим время до след. запуска
log.Println("Error fetching currency data:", err)
napTime = errorSleepTime
}

// всё сделали - пауза
log.Println("Sleeping", napTime)
time.Sleep(napTime)
}
}()
}

Здесь мы запускаем подпрограмму Go — goroutine с передачей ей структуры сервера, которая в бесконечном цикле (с настроенными перерывами — time.Sleep(napTime) ) получает данные с ECB.

и
func parseCurrencyData(data []byte) (ts time.Time, currencies map[string]float64, err error) {
// читаем файл, возврат при ошибке
var c currencyEnvelope
err = xml.Unmarshal(data, &c)
if err != nil {
return time.Time{}, nil, err
}

// читаем ещё раз для получения штампа времени, возврат при ошибке
var t timeEnvelope
err = xml.Unmarshal(data, &t)
if err != nil {
return time.Time{}, nil, err
}

// разбираем время, возврат при ошибке
ts, err = time.Parse(currencyDateFormat, t.Time.Time)
if err != nil {
return time.Time{}, nil, err
}

currencies = make(map[string]float64)

// вручную вставляем EUR как "1"
currencies[eur] = 1

// добавляем все курсы
for _, currency := range c.Cube {
currencies[currency.Name] = currency.Rate
}

return ts, currencies, nil
}

Ещё немного поясню — здесь мы читаем «сырые» данные от ECB, возвращаем время обновления курсов и map с курсами.

И немного про обработчик URL нашего приложения. Рассмотрим метод ServeHTTP() в файле handlers.go — с которого всё начинается. Он разбирает все запросы к серверу и вызывает соответствующий метод для каждого запроса:

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("[{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s] {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s\n", r.Method, r.RequestURI)

// если нет валют, вернём ошибку
if !s.hasCurrencies {
log.Println("No currencies, returning error!")
http.Error(w, "No currencies", http.StatusServiceUnavailable)
return
}

// выбираем нужный обработчик, и возвращаем ошибку на незнакомый URI
switch r.RequestURI {
case "/currencies":
s.currenciesHandler(w, r)
case "/convert":
s.convertHandler(w, r)
case "/webhook":
s.webhookHandler(w, r)
default:
http.NotFound(w, r)
}
}

В главном файле проекта, main.go, мы импортируем наш серверный модуль и создаём объект сервера: s, err := server.New(), а затем, если нет ошибок, запускаем его: err = s.Run().

Что дальше?

Прочитайте код, загрузите его себе и запустите!

Продолжение

Full stack проект на Go за неделю

Full stack проект на Go за неделю
Этой серией постов мы охватим создание full stack проекта на языке Go (golang).

Наш проект: Конвертер валют

Из названия в принципе понятно, что будет делать приложение — конвертация валют. Реальное, полезное, удобное приложение — что ещё нужно для хорошего примера?

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

День первый. Определяемся с бэкендом и API

Сегодня мы должны определиться с тем, что мы хотим от нашего приложения и API, какие технологии используем для его построения и т.д. С этим фундаментом мы и начнём реализацию.

Главная идея сервиса — предоставить API для массовой конвертации сумм в разных валютах. Кроме того, мы создадим систему настройки веб-хуков для оповещений об изменении курса валют.
Для веб-сайта и мобильных приложений нам нужен способ получения всех курсов валют (во фронтенде будет происходить расчёт и конвертация) и способ получения истории изменения курса каждой валюты.

Получение валют

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

И Европейский Центральный Банк (ECB) как раз предоставляет курсы валют в формате XML со своих серверов. XML файл мы можем получить через HTTP и это полностью нас устраивает на данный момент. В ECB нет курсов всех валют мира, но сейчас для нас это также не имеет значения.

Их XML файл выглядит следующим образом:



Reference rates

European Central Bank





































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

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

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

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

API

Здесь мы опишем, что хотим от API. В первую очередь, все конечные точки должны возвращать данные в формате JSON.

/currencies GET, POST

Эта конечная точка возвратит JSON-представление всех валют. При GET-запросе будут получены данные относительно базовой валюты (EUR), а запрос POST будет принимать параметр с наименованием базовой валюты, после чего будет проведён соответствующий расчёт.

Содержание POST-запроса с передачей базовой валюты Британского фунта будет таким:

{
"base_currency": "GBP"
}

Ответ сервера и на POST и на GET запросы будет следующим:

{
"currency_date": "2016-04-01",
"base_currency": "GBP",
"rates": [
{
"name": "USD",
"rate": 1,43097
},
{
"name": "DKK",
"rate": 9,32570
},
...
]
}

/convert POST

Эта конечная точка будет конвертировать некоторые суммы (возможно, всего одну) из одной валюты в другую. Запрос будет выглядеть примерно так:

{
"target_currency": "USD",
"base_currency": "GBP",
"amounts": [
14,
9,
4.3125,
5.5,
...
]
}

В ответе будет сохранён такой же порядок сумм:

{
"base_currency": "GBP",
"target_currency": "USD",
"currency_date": "2016-04-01",
"converted_amounts": [
9.783590,
6.289451,
3.013695,
3.843553,
...
]
}

/webhook POST, DELETE

С помощью этой конечной точки мы будем добавлять и удалять веб-хуки. При её вызове необходимо учитывать, что переданный URL способен принимать POST-запрос и отвечать в JSON.
POST-запрос будет примерно таким:

{
"base_currency": "USD",
"url": "http://some.exampleserver.foo/currency/webhook",
"token": "somemagickeyword"
}

При добавлении новых курсов, бэкенд выполнит POST-запрос на этот URL, в котором будет необходимый курс валюты и дополнительный заголовок Authorization с настроенным токеном (для безопасности, чтобы никто посторонний не мог делать POST-запросы на этот URL).

Что дальше?

Программирование!

Далее мы выберем среду разработки и стек для деплоя, затем примемся за разработку.

Второй день

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка: Разработка кросс-платформенного мобильного приложения на голом JSON

Предыдущие несколько месяцев я посвятил работе над совершенно новым способом разработки нативных приложений для iOS и Android и назвал его Jasonette.

С Jasonette можно описать всю логику приложения всего в одном файле JSON-разметки. А если всё ваше приложение состоит из JSON, его можно загружать так же, как любые другие данные.

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

В видео ниже кратко описан весь процесс (на англ.):

Jasonette состоит из многих компонентов. Там есть функции, шаблоны, стили и многое другое, и всё это в JSON разметке. Вы можете писать супер-изощрённые нативные приложения в стиле [simple_tooltip content=’Model-View-Controller (Модель-Представление-Контроллер) — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер — таким образом, что модификация каждого компонента может осуществляться независимо.’]MVC[/simple_tooltip].

Сегодня мы разберём только «Представления»:

  1. Как Jasonette выражает различные кросс-платформенные шаблоны UI в JSON.
  2. Как реализованы внутренние преобразования JSON-в-Native.

Базовая структура

Издалека можно подумать, что Jasonette работает подобно браузеру. Но вместо того, чтобы работать с HTML и отрисовывать web-view, Jasonette загружает JSON и «на лету» собирает нативное представление.

Разметка JSON здесь самая обычная, но при этом она строится на нескольких стандартах. Во-первых, структура начинается с ключа $jason, который имеет двух потомков: head и body. Выглядит она так:

{
  "$jason": {
    "head": {
      .. метаданные документа ...
    },
    "body": {
      .. содержимое, выводимое на экран ..
    }
  }
}

Философия проектирования

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

  1. Список с вертикальной прокруткой
  2. Список с горизонтальной прокруткой
  3. Абсолютное позиционирование
  4. Сетка

Взглянем на первые три, ибо они самые распространённые.

1. Секции — описание списка с прокруткой

Списки с прокруткой это самый популярный способ построения интерфейса приложений. В Jasonette мы называем их sections.

Они бывают двух видов: вертикальные и горизонтальные.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON


Реализация — Вертикальные секции

Под iOS Jasonette реализует их с помощью UITableView. Под Android — с RecyclerView.


{
  "body": {
    "sections": [{
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}

Под iOS эта JSON разметка создаст UITableView с тремя UITableViewCells, каждая из которых содержит UILabel, с соответствующими атрибутами.
Под Android будет создано RecyclerView с тремя элементами, каждый из которых это TextView, выводящий соответствующий элемент.
Всё это будет сконструировано программно, без какого-либо использования Storyboards (в iOS) или файлов макета XML (в Android).

Реализация — Горизонтальные секции

Синтаксически нет разницы с вертикальными секциями. Единственное, что мы изменили, это type в “horizontal”.


{
  "body": {
    "sections": [{
      "type": "horizontal",
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}
2. Элементы — Описываем макет каждого элементы прокрутки

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

Элемент может быть:

  • Единичным компонентом типа label (метка), image (картинка), button (кнопка), textarea (текстовое поле) и т.д.
  • Комбинацией любых этих элементов

К счастью, iOS и Android имеют похожие системы построения представлений, UIStackView и LinearLayout, соответственно. И эти системы в свою очередь похожи на CSS Flexbox, что облегчает нам работу. И вдобавок к этому, нативные системы представлений бесконечно компонуемы — как показано ниже, вы можете создать вертикальный макет, горизонтальный макет, а также скомпоновать и горизонтальный и вертикальный в одном, и так до бесконечности.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка кросс-платформенного мобильного приложения на голом JSON

Для создания вертикального макета, выставим type как vertical, затем настроим остальные компоненты:


{
  "items": [{
    "type": "vertical",
    "components": [
      {
        "type": "label",
        "text": "First"
      }, 
      {
        "type": "label",
        "text": "Second"
      }, 
      {
        "type": "label",
        "text": "Third"
      }
    ]
  }]
}

Здесь то же самое. Просто установим type в horizontal:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "label",
        "text": "Rick"
      }
    ]
  }]
}

Встроить один макет в другой так же просто:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "vertical",
        "components": [{
          "type": "label",
          "text": "User"
        }, {
          "type": "label",
          "text": "Rick"
        }]
      }
    ]
  }]
}

Чтобы не усложнять понимание, я пока не упоминал про стилизование элементов приложения, но это делается очень просто. Всё, что вам нужно для этого, это добавить объект style с описанием атрибутов font (шрифт), size (размер), width (ширина), height (высота), color (цвет), background (фон), corner_radius (угловой радиус), opacity (прозрачность) и т.п.

3. Слои — абсолютное позиционирование

Иногда вам может понадобиться разместить элементы в определённой части экрана без прокрутки и перемещений. Jasonette поддерживает такое размещение в layers.

На текущий момент в слое можно разместить только два типа дочерних объектов: image и label. Вы можете разместить их в любой части экрана. Ниже пример этого:

Разработка кросс-платформенного мобильного приложения на голом JSON

В этом примере у нас созданы две метки (для температуры и состояния погоды) и картинка (иконка камеры). Они размещены в своих координатах и не двигаются:


{
  "$jason": {
    "body": {
      "style": {
        "background": "camera"
      },
      "layers": [
        {
          "type": "label",
          "text": "22°C",
          "style": {
            "font": "HelveticaNeue-Light",
            "size": "20",
            "top": "50",
            "left": "50{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}-100",
            "width": "200",
            "align": "center"
          }
        },
        {
          "type": "label",
          "text": "few clouds",
          "style": {
            "font": "HelveticaNeue",
            "size": "15"
          }
        },
        {
          "type": "image",
          "url": "https://s3.amazonaws.com/.../camera{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}402x.png",
          "style": {
            "bottom": "100",
            "width": "30",
            "color": "#ffffff",
            "right": "30"
          }
        }
      ]
    }
  }
}

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

Вот ещё примеры того, что построено в Jasonette:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON
За пределами представлений

Прочитав всё это, вы можете подумать:

  • «Ух ты, круто! Я должен это попробовать!» или
  • «Да, я конечно могу сделать приложение на поиграться, но реальное — никогда!»

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

Вы можете соединить действия с элементами UI и они сработают, как только пользователь кликнет на них. Также вы можете вызывать действия одно за другим, основываясь на возвращаемых ими успехах/ошибках. А можно даже автоматически вызывать действия на основе происходящих событий с устройством или приложением.

Что ещё возможно?

Вам обязательно нужен сервер, хранящий JSON, а в остальном Jasonette совершенно автономна. И этот JSON может прилетать отовсюду: с локального устройства, с удаленных серверов, да хоть с raspberry pi!

  1. У вас есть веб-приложение? Тогда вы с лёгкостью построите мобильное приложение, вызывая серверный API
  2. Вам можно вообще не думать о сервере. Храните JSON файл на Pastebin или Github!
  3. Сконвертируйте любой веб-сайт в приложение. В Jasonette есть мощный парсер HTML-в-JSON на базе библиотеки cheerio, которая позволяет преобразовать любой HTML в объект JSON. Ну и, конечно, вы можете сами сформировать нужный JSON.

Ещё немного примеров:

Разработка кросс-платформенного мобильного приложения на голом JSON

Фото-приложение, делающее снимок камерой устройства и выкладывающая его в S3, затем она создаёт запись в ленте новостей на своём сервере:

Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение Eliza Chatbot для iOS и Android на базе Node.js:

Приложение для микро-блоггинга:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение, конвертирующее сайт HTML в JSON структуру, а затем в мобильное приложение:

Разработка кросс-платформенного мобильного приложения на голом JSON
Заключение

Jasonette это пока молодой проект. Версия для iOS вышла в конце 2016 г, а для Android ещё немного позже.

Но уже сейчас у неё есть огромное сообщество разработчиков и она активно развивается.

Звучит круто? Тогда Jasonette будет очень рад вам!

Источник

Разработка Web-приложений и микросервисов на Go с Gin, часть 2

Создаём веб-приложение с Go, Gin и React

Это продолжение материала, часть 1.

Выводим список топиков

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

Настройка роута

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

После всего этого файл routes.go будет таким:

// routes.go

package main

func initializeRoutes() {

  // определение роута главной страницы
  router.GET("/", showIndexPage)
}

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

Файл main.go должен быть сейчас таким:

// main.go

package main

import "github.com/gin-gonic/gin"

var router *gin.Engine

func main() {

  // роутер по-умолчанию в Gin
  router = gin.Default()

  // Обработаем шаблоны вначале, так что их не нужно будет перечитывать
  // ещё раз. Из-за этого вывод HTML-страниц такой быстрый.
  router.LoadHTMLGlob("templates/*")

  // Инициализируем роуты
  initializeRoutes()

  // Запускаем приложение
  router.Run()

}
Проектирование модели топика

Сделаем структуру топика простой, всего с тремя полями — Id, Title (название) и Content (содержание). Её можно описать так:

type article struct {
  ID      int    `json:"id"`
  Title   string `json:"title"`
  Content string `json:"content"`
}

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

var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

Мы вставим этот кусок кода в новый файл models.article.go. Сейчас нам понадобится функция, возвращающая список всех топиков. Мы её назовём getAllArticles() и положим её в этот же файл. И создадим тест для неё. Мы назовём этот тест TestGetAllArticles и вставим его в файл models.article_test.go.

Создадим тест (TestGetAllArticles) для функции getAllArticles(). В результате файл models.article_test.go будет таким:

// models.article_test.go

package main

import "testing"

// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
  alist := getAllArticles()

  // Check that the length of the list of articles returned is the
  // same as the length of the global variable holding the list
  if len(alist) != len(articleList) {
    t.Fail()
  }

  // Check that each member is identical
  for i, v := range alist {
    if v.Content != articleList[i].Content ||
      v.ID != articleList[i].ID ||
      v.Title != articleList[i].Title {

      t.Fail()
      break
    }
  }
}

В этом тесте используется функция getAllArticles() для получения списка всех топиков. Сперва этот тест проверяет, что эта функция получает список топиков и этот список идентичен списку, загруженному в глобальную переменную articleList. Затем он проходит в цикле по списку топиков для проверки уникальности каждого. Если хотя бы одна из этих проверок не удалась, тест возвращает неудачу.

После написания теста приступим к написанию кода модуля. Файл models.article.go будет содержать такой код:

// models.article.go

package main

type article struct {
  ID      int    `json:"id"`
  Title   string `json:"title"`
  Content string `json:"content"`
}

// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

// Return a list of all the articles
func getAllArticles() []article {
  return articleList
}
Создание шаблона представления

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

Предположим, что список топиков будет передан в шаблон в переменной payload. Тогда следующий снипет выведет список всех топиков:

{{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

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

Обновлённый index.html будет таким:

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <!--Loop over the `payload` variable, which is the list of articles-->
  {{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}
Определяем требования к обработчику роута с помощью юнит-теста

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

  1. Обработчик отвечает статус-кодом HTTP 200,
  2. Возвращаемый HTML содержит тег title с текстом «Home Page».

Код теста поместим в функцию TestShowIndexPageUnauthenticated в файл handlers.article_test.go. Вспомогательные функции, используемые в этом тесте, мы разместим в файле common_test.go.

Вот содержимое файла handlers.article_test.go:

// handlers.article_test.go

package main

import (
  "io/ioutil"
  "net/http"
  "net/http/httptest"
  "strings"
  "testing"
)

// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
  r := getRouter(true)

  r.GET("/", showIndexPage)

  // Create a request to send to the above route
  req, _ := http.NewRequest("GET", "/", nil)

  testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    // Test that the http status code is 200
    statusOK := w.Code == http.StatusOK

    // Test that the page title is "Home Page"
    // You can carry out a lot more detailed tests using libraries that can
    // parse and process HTML pages
    p, err := ioutil.ReadAll(w.Body)
    pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0

    return statusOK && pageOK
  })
}

Файл common_test.go:

package main

import (
  "net/http"
  "net/http/httptest"
  "os"
  "testing"

  "github.com/gin-gonic/gin"
)

var tmpArticleList []article

// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
  //Set Gin to Test Mode
  gin.SetMode(gin.TestMode)

  // Run the other tests
  os.Exit(m.Run())
}

// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
  r := gin.Default()
  if withTemplates {
    r.LoadHTMLGlob("templates/*")
  }
  return r
}

// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {

  // Create a response recorder
  w := httptest.NewRecorder()

  // Create the service and process the above request.
  r.ServeHTTP(w, req)

  if !f(w) {
    t.Fail()
  }
}

// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
  tmpArticleList = articleList
}

// This function is used to restore the main lists from the temporary one
func restoreLists() {
  articleList = tmpArticleList
}

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

Функция TestMain переводит Gin в тестовый режим и вызывает функции тестирования. Функция getRouter создаёт и возвращает роутер. Функция saveLists() помещает список топиков во временную переменную. Она используется в функции restoreLists() для восстановления списка топиков до первоначального состояния после выполнения юнит-теста.

И, наконец, функция testHTTPResponse выполняет переданную ей функцию для проверки — возвращает ли она булево значение true — показывая успешность теста, или нет. Эта функция помогает нам избежать дублирования кода, необходимого для тестирования ответа на HTTP-запрос.

Для проверки HTTP-кода и возвращаемого HTML, сделаем следующее:

  1. Создадим новый роутер,
  2. Определим роуту тот же обработчик, что используется в главном приложении (showIndexPage),
  3. Создадим новый запрос для доступа к роуту,
  4. Создадим функцию, обрабатывающую ответ, для тестирования HTTP-кода и HTML, и
  5. Вызовем testHTTPResponse() из новой функции для завершения теста.
Создание обработчика роута

Мы будет создавать все обработчики роутов, относящихся к топикам, в файле handlers.article.go. Обработчик главной страницы, showIndexPage выполняет следующие задачи:

1. Получает список топиков

Это делается с помощью функции getAllArticles, созданной ранее:

articles := getAllArticles()

2. Обрабатывает шаблон index.html, передавая ему список топиков

Это делается с помощью кода ниже:

c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
        "title":   "Home Page",
        "payload": articles,
    },
)

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

Файл handlers.article.go должен быть таким:

// handlers.article.go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

Если сейчас собрать и запустить приложение, открыть в браузере http://localhost:8080, оно будет выглядеть так:

Разработка Web-приложений и микросервисов на Go с Gin, часть 2

Новые файлы, добавленные в этом разделе:

├── common_test.go
├── handlers.article.go
├── handlers.article_test.go
├── models.article.go
├── models.article_test.go
└── routes.go

Окончание

Web Scraping с Golang и goQuery

Web Scraping с Golang и goQuery

Web scraping это обработка HTML кода веб-страницы и выборка необходимых элементов из неё. На этом принципе построены поисковые системы, такие как Google. Они заходят на все страницы, которые найдут, и копируют их себе.

Проверьте, что у вас установлен go и настроена переменная $GOPATH.

Разбор страницы с goQuery

goQuery это почти то же самое, что jQuery, только для go. Она предоставляет удобный доступ к HTML-структуре страницы и позволяет оперировать её элементами и содержимым. Если сравнивать их функции, то здесь также, как в jQuery есть .Text() для строкового содержимого и .Attr() или .AttrOr() для значений атрибутов.

Чтобы начать работать с goQuery, наберите в консоли:

go get github.com/PuerkitoBio/goquery

Сбор ссылок на странице с golang и goQuery

Создадим тестовый проект:

# проверим, что $GOPATH настроена
echo $GOPATH
/home/jonathan/projects/go
# перейдём в папку `src`
cd $GOPATH/src
# создадим папку
mkdir tutorial-web-scraping
# перейдём в неё
cd tutorial-web-scraping

Теперь мы можем создать несколько примеров. Вообще не принято создавать несколько функций main() в одной директории, но мы сделаем для этого примера исключение — мы же новички, верно? 🙂

Список постов на странице блога

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

// файл: list_posts.go
package main

import (
// импортируем стандартные библиотеки
"fmt"
"log"

// импортируем сторонние библиотеки
"github.com/PuerkitoBio/goquery"
)

func postScrape() {
doc, err := goquery.NewDocument("http://jonathanmh.com")
if err != nil {
log.Fatal(err)
}

// используем CSS селекторы, найденные инспектором в браузере
// для каждого используем индекс и элемент
doc.Find("#main article .entry-title").Each(func(index int, item *goquery.Selection) {
title := item.Text()
linkTag := item.Find("a")
link, _ := linkTag.Attr("href")
fmt.Printf("Post #{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d: {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s - {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s\n", index, title, link)
})
}

func main() {
postScrape()
}

Вывод этой программы будет примерно таким:

$ go run list_posts.go
Post #0: How to use SSH keys for Authentication (for beginners) - http://jonathanmh.com/how-to-use-ssh-keys-for-authentication-for-beginners/
Post #1: Using Sourcegraph Checkup with local file system storage - http://jonathanmh.com/using-sourcegraph-checkup-local-file-system-storage/
Post #2: Copenhagen Pride 2016 Photos - http://jonathanmh.com/copenhagen-pride-2016-photos/
Post #3: Searching the Google Books API with PHP [Quickstart] - http://jonathanmh.com/searching-google-books-api-php-quickstart/
Post #4: How to get a high score on Pagespeed Insights (and make your site fast) - http://jonathanmh.com/get-high-score-pagespeed-insights-make-site-fast/
Post #5: NGINX / Apache: Block Requests to PHP file (xmlrpc.php) - http://jonathanmh.com/nginx-apache-block-requests-php-file-xmlrpc-php/
Post #6: Distortion Copenhagen 2016 – Nørrebro / Wednesday - http://jonathanmh.com/distortion-copenhagen-2016-norrebro-wednesday/
Post #7: I need feminism because: Metal T-shirts - http://jonathanmh.com/need-feminism-metal-t-shirts/
Post #8: On Being Powerless - http://jonathanmh.com/on-being-powerless/
Post #9: How to get a Job in Tech - http://jonathanmh.com/get-job-tech/

Соберём все ссылки со страницы

Собрать все ссылки со страницы не намного сложнее, просто нужно использовать более широкий селектор, body a и обойти все найденные ссылки. Получить содержимое тега <а> можно с помощью linkText := linkTag.Text().

// файл: get_all_links.go
package main

import (
"fmt"
"log"

"github.com/PuerkitoBio/goquery"
)

func linkScrape() {
doc, err := goquery.NewDocument("http://jonathanmh.com")
if err != nil {
log.Fatal(err)
}

doc.Find("body a").Each(func(index int, item *goquery.Selection) {
linkTag := item
link, _ := linkTag.Attr("href")
linkText := linkTag.Text()
fmt.Printf("Link #{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d: '{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s' - '{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s'\n", index, linkText, link)
})
}

func main() {
linkScrape()
}

Вывод будет примерно таким:

$ go run get_all_links.go
Link #0: 'Skip to content' - '#content'
Link #1: 'JonathanMH' - 'http://jonathanmh.com/'
Link #2: 'twitter' - 'https://twitter.com/JonathanMH_com'
Link #3: 'rss feed' - 'http://jonathanmh.com/feed/'
... (many more)
Link #172: 'Proudly powered by WordPress' - 'https://wordpress.org/'

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

Получаем название страницы и метаданные

// файл: metadata.go
package main

import (
"fmt"
"log"

"github.com/PuerkitoBio/goquery"
)

func metaScrape() {
doc, err := goquery.NewDocument("http://jonathanmh.com")
if err != nil {
log.Fatal(err)
}

var metaDescription string
var pageTitle string

pageTitle = doc.Find("title").Contents().Text()

doc.Find("meta").Each(func(index int, item *goquery.Selection) {
if( item.AttrOr("name","") == "description") {
metaDescription = item.AttrOr("content", "")
}
})
fmt.Printf("Page Title: '{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s'\n", pageTitle)
fmt.Printf("Meta Description: '{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s'\n", metaDescription)
}

func main() {
metaScrape()
}

И получим:

Page Title: 'JonathanMH - Just a guy, usually in Denmark, blogging about things he couldn't get to work right away and then made a blog post in case others get stuck too.'
Meta Description: 'JonathanMH Coder, Blogger, Videographer, Webguy, Just a guy, usually in Denmark, blogging about things he couldn't get to work right away and then made a blog post in case others get stuck too.'

В этом примере мы используем AtrrOr( value, fallback_value) для того, чтобы точно знать, что получим данные. Это намного проще написания дополнительных проверок содержимого.

Вы используете web scraping? Если у вас есть вопросы / дополнения по теме, напишите о них в комментариях!

По материалам: Web Scraping with Golang and goQuery

Оптимизация времени загрузки приложения NativeScript с «ленивой загрузкой» Angular 2

Оптимизация времени загрузки приложения NativeScript с "ленивой загрузкой" Angular 2

При разработке мобильного приложения вы всегда должны обращать внимание на производительность и всегда оптимизировать её. Сегодня мы покажем как оптимизировать время загрузки приложений с Angular с «ленивой загрузкой Angular».

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

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

Оптимизация времени загрузки приложения NativeScript с "ленивой загрузкой" Angular 2

Использование ленивой загрузки с NativeScript

Встроенный в Angular 2 модуль загрузки использует SystemJS. Но при разработке приложений NativeScript оптимальнее использовать свой загрузчик модулей.

Далее мы рассмотрим приложение lazyNinjas, в котором есть два модуля — HomeModule (без ленивой загрузки) и NinjasModule (с ленивой загрузкой). В репозитории этого приложения есть две ветки — callback-loading и custom-module-loader, и в следующих двух разделах мы рассмотрим оба этих подхода к разработке. Уделите минуту этому приложению, чтобы знать о чём пойдёт речь далее — скачайте его с github и запустите.

Обратный вызов в свойство `loadChildren`

Рассмотрим конфигурацию нашего роутера:

// app-routing.ts
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { routes as homeRoutes } from "./home/home.routes";

const routes = [
...homeRoutes,
{
path: "ninjas",
loadChildren: () => require("./ninjas/ninjas.module")["NinjasModule"]
}
];

export const routing = NativeScriptRouterModule.forRoot(routes);
Массив `routes` это реальная конфигурация роутера в нашем приложении. Сначала мы добавили роуты модуля HomeModule с помощью оператора ‘…’ . Затем зарегистрировали лениво-загруженный NinjasModule. Обратим внимание на значение, передаваемое нами в свойство `loadChildren`:

loadChildren: () => require("./ninjas/ninjas.module.js")["NinjasModule"]
Здесь мы передаём ему обратный вызов. Рассмотрим подробнее, что будет дальше.

Сначала в файле «./ninjas/ninjas.module.ts» мы описали NinjasModule с его роутами, затем импортировали их методом `forChild` из `NativeScriptRouterModule`. Вот как он выглядит:

import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";

import { NinjasComponent } from "./ninjas.component";
import { routes } from "./ninjas.routes";

@NgModule({
imports: [
NativeScriptRouterModule,
NativeScriptRouterModule.forChild(routes)
],
declarations: [NinjasComponent]
})
export class NinjasModule { }

Помните, что в экспортированном объекте есть всего один элемент — `NinjasModule`.

loadChildren: () => require("./ninjas/ninjas.module.js")["NinjasModule"]
Мы можем опустить расширение файла и записать это так:

loadChildren: () => require("./ninjas/ninjas.module")["NinjasModule"]
Эта версия приложения находится в ветке callback-loading.

Свой загрузчик модулей

Вместо передачи обратного вызова каждому свойству `loadChildren`, мы можем вынести механику загрузки в отдельный загрузчик. Затем мы можем его использовать вместо встроенного на базе SystemJS.

Посмотрим на NinjaModuleLoader:

// ninja-module-loader.ts
import {
Injectable,
Compiler,
NgModuleFactory,
NgModuleFactoryLoader
} from "@angular/core";

import { path, knownFolders } from "file-system";

const SEPARATOR = "#";

@Injectable()
export class NinjaModuleFactoryLoader implements NgModuleFactoryLoader {

constructor(private compiler: Compiler) {
}

load(path: string): Promise> {
let {modulePath, exportName} = this.splitPath(path);

let loadedModule = require(modulePath)[exportName];
if (!loadedModule) {
throw new Error(`Cannot find "${exportName}" in "${modulePath}"`);
}

return this.compiler.compileModuleAsync(loadedModule);
}

private splitPath(path: string): {modulePath: string, exportName: string} {
let [modulePath, exportName] = path.split(SEPARATOR);
modulePath = getAbsolutePath(modulePath);

if (typeof exportName === "undefined") {
exportName = "default";
}

return {modulePath, exportName};
}
}

function getAbsolutePath(relativePath: string) {
return path.normalize(path.join(knownFolders.currentApp().path, relativePath));
}

В этом загрузчике реализован `NgModuleFactoryLoader` с единственным методом — `load` с одним параметром — путём, который мы передаём свойству `loadChildren`.

Мы должны изменить свойство `loadChildren` в конфигурации роутера:

loadChildren: "./ninjas/ninjas.module#NinjasModule"
В частном методе `splitPath` в NinjasModuleLoader мы храним расположение файла модуля и экспортируемое имя модуля. Затем мы можем запросить нужный модуль так же, как в функции обратного вызова:

let loadedModule = require(modulePath)[exportName];
Вынос логики загрузки позволяет нам проверять существование модуля и выдавать соответствующее сообщение в противном случае.

if (!loadedModule) {
throw new Error(`Cannot find "${exportName}" in "${modulePath}"`);
}

Если модуль успешно загружен, асинхронно скомпилируем его компилятором Angular:

return this.compiler.compileModuleAsync(loadedModule);
Теперь нам нужно зарегистрировать наш загрузчик в главном модуле*.

// app.module.ts
import { NgModule, NgModuleFactoryLoader } from "@angular/core";
...
import { NinjaModuleLoader } from "./ninja-module-loader";

@NgModule({
...
providers: [
{ provide: NgModuleFactoryLoader, useClass: NinjaModuleLoader }
]
})
export class AppModule { }

Финальную версию приложения вы можете взять в ветке custom-module-loader.

* У вас может быть разный загрузчик модулей в каждом NgModule!

Реальные приложения NativeScript с модулем ленивой загрузки вы можете взять в наших примерах SDK. В них более 100 различных компонент, каждый со своим роутом и ~15 функциональными модулями. В таблице ниже можно увидеть время первого старта приложения с и без ленивой загрузки.

Оптимизация времени загрузки приложения NativeScript с "ленивой загрузкой" Angular 2

По материалам Optimizing app loading time with Angular 2 Lazy Loading

Разработка Web-приложений и микросервисов на Go с Gin, часть 1

Разработка Web-приложений и микросервисов на Go с Gin, часть 1
Введение

Сегодня мы покажем, как создавать веб-приложения и микросервисы в Go с помощью фреймворка Gin. Gin это фреймворк, позволяющий уменьшить объём кода, необходимого для построения таких приложений. Он поощряет создание многократно-используемого и расширяемого кода.

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

Подготовка

Перед началом работы убедитесь, что у вас установлены Go и утилита curl. Если curl не установлена и вы не хотите работать с ней, используйте любую другую утилиту тестирования API.

Что такое Gin?

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

Почему именно Gin?

Одно из лучших качеств Go — его встроенная библиотека net/http, позволяющая с лёгкостью создавать HTTP сервер. Однако, она не настолько гибкая, как бы хотелось, и количество кода, требуемое при работе с ней, довольно большое.

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

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

Проектирование приложения

Посмотрим, как Gin обрабатывает запросы:

Request -> Route Parser -> [Optional Middleware] -> Route Handler -> [Optional Middleware] -> Response
Когда приходит запрос, Gin сначала проверяет, есть ли подходящий роут (маршрут). Если соответствующий роут найден, Gin запускает обработчик этого роута и промежуточные звенья в заданном порядке. Мы увидим как это происходит, когда перейдём к коду в следующем разделе.

Функционал приложения

Наше приложение — это простой менеджер топиков. Оно должно:

  • позволять пользователям регистрироваться с логином и паролем (для неавторизованных пользователей),
  • позволять пользователям авторизоваться (для неавторизованных пользователей),
  • позволять пользователям завершать сеанс (для авторизованных пользователей),
  • позволять пользователям создавать топики (для авторизованных пользователей),
  • Выводить список всех топиков на главной странице (для всех пользователей), и
  • Выводить топик на его собственной странице (для всех пользователей).

Вдобавок к этому мы сделаем, чтобы список топиков и отдельные топики были доступны в форматах HTML, JSON и XML.

Это позволит нам проиллюстрировать, как можно использовать Gin для проектирования веб-приложений, API серверов и микросервисов.

Для этого мы используем следующий функционал Gin:

  • Routing — для обработки различных URL адресов,
  • Custom rendering — для обработки формата ответа, и
  • Middleware — для реализации авторизации.

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

Routing

Роутинг (маршрутизация) это одна из важнейших функций, имеющихся во всех современных веб-фреймворках. Любая веб-страница или вызов API доступен по URL. Фреймворки используют роуты для обработки запросов к этим URL-адресам. Если URL такой: httр://www.example.com/some/random/route, то роут будет: /some/random/route.

У Gin очень быстрый роутер, удобный в конфигурировании и работе. Вместе с обработкой определенных URL-адресов, роутер в Gin может обрабатывать шаблоны адресов и группы URL.

В нашем приложении мы будем:

  • Хранить главную страницу в роуте / (запрос HTTP GET),
  • Группировать роуты, относящиеся к пользователям, в роуте /u ,
    • Хранить страницу авторизации в /u/login (запрос HTTP GET),
    • Передавать данные авторизации в /u/login (запрос HTTP POST),
    • Завершение сеанса в /u/logout (запрос HTTP GET),
    • Хранить страницу регистрации в /u/register (запрос HTTP GET),
    • Передавать регистрационную информацию в /u/register (запрос HTTP POST) ,
  • Группировать роуты, относящиеся к топикам, в роуте /article,
    • Хранить страницу создания топика в /article/create (запрос HTTP GET),
    • Передавать утверждённый топик в /article/create (запрос HTTP POST), и
    • Хранить страницу топика в /article/view/:article_id (запрос HTTP GET). Обратите внимание на часть :article_id в этом роуте. Двоеточие : в начале указывает на то, что это динамический роут. Это значит, что :article_id может содержать любое значение и Gin сделает это значение доступным в обработчике запроса.
Rendering

Веб-приложение может вывести ответ в различных форматах, таких как HTML, текст, JSON, XML или другие форматы. API и микросервисы обычно отдают данные в формате JSON, но здесь также нет ограничений.

В следующем разделе мы увидим, как можно обработать разные типы ответов без дублирования функционала. По-умолчанию мы будем отвечать на запрос шаблоном HTML. Однако, мы создадим ещё два вида запроса, которые будут отвечать в формате JSON или XML.

Middleware

В контексте веб-приложений на Go, middleware это часть кода, которую можно выполнить на любом этапе обработки HTTP-запроса. Обычно их используют для инкапсуляции типового функционала, который вам нужно вызывать из различных роутов. Мы можем использовать middleware перед и/или после обработанного HTTP-запроса. К типовым примерам применения middleware относятся авторизация, валидация и т.п.

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

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

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

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

Установка зависимостей

Наше приложение будет использовать только одну внешнюю зависимость — сам фреймворк Gin. Установим актуальную версию такой командой:

go get -u github.com/gin-gonic/gin

Создание многократно-используемых шаблонов

Наше приложение будет отображать веб-страницу, используя её шаблон. Однако, в ней будет несколько частей, таких как шапка (header), меню, боковая панель и подвал (footer), которые будут представлены на всех страницах. В Go можно создавать шаблонные снипеты, которые можно будет загружать в любые шаблоны.

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

Сначала создайте шаблон меню в файле templates/menu.html как описано ниже:

<!--menu.html-->
<nav class="navbar navbar-default">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">
        Home
      </a>
    </div>
  </div>
</nav>

Пока в нашем меню есть только одна ссылка на главную страницу. Позже мы добавим остальные ссылки по мере роста функционала приложения. Шаблон шапки будет в файле templates/header.html:





<!--header.html-->

<!doctype html>

<html>

  <head>

    <!--Use the `title` variable to set the title of the page-->

    <title>{{ .title }}</title>

    <meta name="viewport" content="width=device-width, initial-scale=1">

    <meta charset="UTF-8">

    <!--Use bootstrap to make the application look nice-->

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">

    <script async src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>

  </head>

  <body class="container">

    <!--Embed the menu.html template at this location-->

    {{ template "menu.html" . }}


Как вы видите, мы используем здесь фреймворк с открытым исходным кодом Bootstrap. Большая часть файла это стандартный HTML. Однако, посмотрим внимательно на пару строк. В строке сдинамически задаётся заголовок страницы с помощью переменной .title, которая должна быть определена в приложении. А в строке {{ template «menu.html» . }} мы загружаем шаблон меню из файла menu.html. Вот так в Go можно вызывать один шаблон из другого.

Шаблон подвала содержит только статический HTML. Шаблон главной страницы вызывает шапку и подвал и выводит сообщение Hello Gin:

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <h1>Hello Gin!</h1>

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

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

Завершение и проверка установки

Создав шаблоны, теперь самое время создать главный файл приложения. Мы создадим файл main.go, в нём будет простое веб-приложение, загружающее главную страницу. С Gin это делается в четыре шага:

1. Создаём роутер

Роутер в Gin создаётся так:

router := gin.Default()

2. Загружаем шаблоны

После создания роутера, загрузим все шаблоны:

router.LoadHTMLGlob("templates/*")
Это загрузит все шаблоны из папки templates. Загрузив один раз шаблоны, больше не будет необходимости перечитывать их, что делает веб-приложения с Gin очень быстрыми.

3. Задаём обработчик роутов

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

router.GET("/", func(c *gin.Context) {

  // Call the HTML method of the Context to render a template
  c.HTML(
      // Set the HTTP status to 200 (OK)
      http.StatusOK,
      // Use the index.html template
      "index.html",
      // Pass the data that the page uses (in this case, 'title')
      gin.H{
          "title": "Home Page",
      },
  )

})

С помощью метода router.GET мы задаём обработчик роута для GET-запросов. Он принимает в качестве параметров сам роут (/) и один или несколько обработчиков, которые всего лишь функции.

Обработчик роута имеет указатель на Контекст (gin.Context) в параметрах. В этом контексте содержится вся информация о запросе, которая может понадобится обработчику в дальнейшем. К примеру, в нём есть информация о заголовках, cookies и т.д. В Контексте также есть методы для вывода ответа в форматах HTML, тексте, JSON и XML. В нашем случае мы взяли метод context.HTML для обработки HTML шаблона (index.html). Вызов этого метода включает дополнительные данные, в которых значение title установлено Home Page. Это значение, которое может быть обработано в HTML шаблоне. Мы используем это значение в теге в шаблоне шапки.

4. Запуск приложения Для запуска приложения воспользуемся методом Run нашего роутера:

router.Run()

Приложение запустится на localhost и 8080 порте, по-умолчанию.

Финальный файл main.go будет таким:

// main.go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

var router *gin.Engine

func main() {

  // Set the router as the default one provided by Gin
  router = gin.Default()

  // Process the templates at the start so that they don't have to be loaded
  // from the disk again. This makes serving HTML pages very fast.
  router.LoadHTMLGlob("templates/*")

  // Define the route for the index page and display the index.html template
  // To start with, we'll use an inline route handler. Later on, we'll create
  // standalone functions that will be used as route handlers.
  router.GET("/", func(c *gin.Context) {

    // Call the HTML method of the Context to render a template
    c.HTML(
      // Set the HTTP status to 200 (OK)
      http.StatusOK,
      // Use the index.html template
      "index.html",
      // Pass the data that the page uses (in this case, 'title')
      gin.H{
        "title": "Home Page",
      },
    )

  })

  // Start serving the application
  router.Run()

}

Для запуска приложения из командной строки, перейдите в папку приложения и выполните команду:

go build -o app

Будет собрано приложение и создан исполняемый файл с именем app, который можно запустить так:

./app
Разработка Web-приложений и микросервисов на Go с Gin, часть 1

Если всё прошло успешно, вы должны увидеть приложение по адресу http://localhost:8080 и оно будет выглядеть примерно так:

На этом этапе иерархия папок приложения будет такой:

├── main.go
└── templates
    ├── footer.html
    ├── header.html
    ├── index.html
    └── menu.html

Продолжение

Добавляем иконки Font Awesome в приложение NativeScript

Добавляем иконки Font Awesome в приложение NativeScript

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

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

Представим, что у вас есть приложение, в котором используются 20 разных иконок. Android работает на различных экранах с большим количеством разрешений, требуя разный размер иконок для каждого из них. К примеру, каждая иконка должна иметь разные размеры для mdpi, hdpi, xhdpi, xxhdpi, и xxxhdpi экранов. В принципе, не так сложно нагенерировать иконки для всех этих размеров, но они довольно сильно скажутся на размере приложения в будущем.

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

Итак, приступим.

Начнём с чистого проекта NativeScript для Android и iOS. В консоли выполните следующие команды:

tns create ExampleProject
cd ExampleProject
tns platform add ios
tns platform add android

Теперь скачаем свежий релиз Font Awesome. Распакуем архив и найдём файл TTF. Скорее всего, он будет называться fonts/fontawesome-webfont.ttf. Скопируем этот файл в нашу папку так: app/fonts/fontawesome-webfont.ttf.

Приложение NativeScript автоматически подхватит шрифты из папки fonts.

Теперь откроем файл app/app.css и создадим класс с новым свойством font-family. Вот так должно работать:

.font-awesome {
font-family: "FontAwesome";
}

Не важно, как будет называться наш класс, главное — указать свойство font-family. Имя шрифта я нашёл в официальной документации к Font Awesome.

Откроем файл app/main-page.xml и немного его изменим:



Миллион запросов в секунду с Python

Возможно ли такое? До недавнего времени, это было невозможно. Крупные компании пытаются мигрировать на другие языки программирования для улучшения производительности и сокращения расходов на серверы, но в действительности этого делать не нужно. Python это хороший инструмент и в сообществе вокруг него в последнее время происходит много всего, касающегося улучшения производительности. В CPython 3.6 улучшена общая производительность интерпретатора благодаря новой реализации словарей, CPython 3.7 будет ещё производительнее благодаря введению более быстрых вызовов и кэшированию в словарях. Для некоторых задач вы можете использовать PyPy с его just-in-time компиляцией. С недавних пор он может запускать тестовый набор NumPy и кардинально улучшил общую совместимость с расширениями на C. Немного позже ожидается, что PyPy достигнет соответствия с Python 3.5.

Всё это вдохновило меня на инновацию в одной из сфер, где Python широко используется, в разработке web и micro-сервисов.

Встречайте Japronto!

Japronto это совершенно новый микро-фреймворк, созданный с учётом ваших нужд. Его главные цели это скорость, расширяемость и лёгкость. Он умеет синхронное и асинхронное программирование с asyncio, и он чертовски быстр. Даже быстрее NodeJS и Go.

Миллион запросов в секунду с Python
микро-фреймворки на Python (синие), Тёмная сторона силы (зелёные) и Japronto (фиолетовый)

Этот микро-бенчмарк был сделан с помощью минимальной программы типа “Hello world!”, но он хорошо демонстрирует серверную нагрузку некоторых решений. Эти результаты были получены на экземпляре AWS c4.2xlarge с 8 VCPU, запущенной в Сан-Пауло с ресурсами по-умолчанию, виртуализацией HVM и магнитным устройством хранения. На машине была установлена Ubuntu 16.04.1 LTS (Xenial Xerus) с ядром Linux 4.4.0–53-generic x86_64. В отчёте ОС было: Xeon® CPU E5–2666 v3 @ 2.90GHz CPU. Я использовал Python 3.6, скомпилированный из исходного кода. Чтобы быть справедливым ко всем участникам сравнения (включая Go), был запущен только один рабочий процесс. Для тестирования серверной загрузки была использована утилита wrk с 1 потоком, 100 соединениями и 24 одновременными (в конвейере) запросами на каждое соединение (всего 2400 запросов).

Миллион запросов в секунду с Python
HTTP pipelining (картинка из Wikipedia)

HTTP pipelining (переводится как Конвейерная обработка HTTP) — технология, которая позволяет передавать на сервер сразу несколько запросов в одном соединении, не ожидая соответствующих ответов. Конвейерная обработка одна из важнейших оптимизаций в данном тесте, так как используется Japronto при выполнении запросов.

Большинство серверов выполняет запросы от клиентов HTTP pipelining таким же образом, как от клиентов не имеющих конвейерной обработки. Они не пытаются оптимизировать их. (На самом деле Sanic и Meinheld также молча сбрасывают запросы от клиентов HTTP pipelining, что является нарушением HTTP 1.1 протокола.)

Говоря простым языком, HTTP-pipelining представляет собой метод, в котором клиент не должен ждать ответа перед отправкой новых запросов, в рамках одного TCP соединения. Для обеспечения целостности соединения, сервер отправляет обратно несколько ответов в порядке, в котором были получены запросы.

Подробности механизма оптимизации

Если клиент отправляет большое количество мелких GET запросов, объединенных конвейерным методом, существует высокая вероятность того, что они будут прибывать, на сервер, в одном TCP пакете (благодаря

алгоритму Нэйгла

) и будут считаны одним системным вызовом.

Выполнение системного вызова и перемещения данных из пространства ядра в

пользовательское пространство

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

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

scatter/gather IO

, ядро может сильно помочь в части склеивания ответов, но Japronto еще не умеет использовать этот метод.

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

Миллион запросов в секунду с PythonJapronto позволяет обработать в среднем 1,214,440 запросов в секунду , эти данные рассчитаны методом интерполяции, исходя из того что 50{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} значений располагается ниже этого уровня.

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

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

Japronto опирается на отличную C библиотеку picohttpparser для разбора строки состояния, заголовков и тела HTTP сообщения. Picohttpparser напрямую использует инструкции по обработке текста в современных процессорах с расширениями SSE4.2 (доступно в любом x86_64 процессоре), для быстрого определения границ HTTP токенов. Ввод/вывод обрабатывается потрясающим uvloop — альтернативная реализации цикла событий для asyncio, написанная Юрием Селивановым на Cython и построеная на базе libuv. Работает примерно в 2 раза быстрее Node.js и практически не уступает программам на Go. На низком уровне, это мост к системному вызову epoll, обеспечивающий асинхронные уведомления о готовности чтения-записи.

Миллион запросов в секунду с Python
Picohttpparser полагается на SSE4.2 и CMPESTRI x86_64

Python является языком, имеющим специальный процесс сбора мусора, периодически освобождающий память, удаляя объекты, которые уже не будут востребованы приложениями. Внутренняя реализация Japronto пытается избегать циклических ссылок и делать как можно меньше выделений/освобождений памяти. Это реализовано путем предварительного выделения некоторых объектов в так называемые арены. Также производится попытка повторного использования объектов Python для будущих запросов, если они больше не имеют циклических ссылок, вместо выбрасывания их.

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

Открытые источники, которые можно использовать

Я работал на Japronto непрерывно в течение последних 3-х месяцев — как в выходные дни, так и рабочие дни. Это стало возможным только из-за перерыва в моей основной работе и я смог вложить все свои усилия в развитие этого проекта.

В настоящее время в Japronto реализован довольно солидный набор функций:
  • HTTP 1.x реализация с поддержкой Chunked загрузки
  • полная поддержка HTTP pipelining
  • Keep-Alive соединения с настраиваемым установщиком соединений
  • Подержка синхронного и асинхронного ввода/вывода
  • модель Master-multiworker на основе разветвления
  • Поддержка перезагрузки кода при изменениях
  • Простая маршрутизация

Если вы хотите помочь мне, пожалуйста, свяжитесь со мной в Twitter. Проект расположен по адресу https://github.com/squeaky-pl/japronto .

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

Послесловие

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

Я хочу поблагодарить Python сообщество за их неоценимый вклад в разработку направленную на улучшение производительности. В частности Виктора Стиннера @VictorStinner, INADA Naoki @methane, Юрия Селиванова @1st1 и всю команду PyPy.

Ради любви к Python.

Источник на английском языке: Paweł Piotr Przeradowski: Million requests per second with Python

Навигация в приложении NativeScript с Angular 2 Router

Навигация в приложении NativeScript с Angular 2 Router

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

Если вы следили за разработкой Angular 2, начиная с беты, то видели, что компоненты навигации кардинально менялись почти в каждом релизе. А те, кто наблюдал за NativeScript и Angular 2, также знают, что компания Telerik использовала все имеющиеся возможности Angular 2 в их текущем состоянии. Именно поэтому навигация в приложениях NativeScript Angular 2 менялась так часто за прошедший год. Однако, Angular 2 теперь официально вышел и Angular 2 Router не будет больше так сильно меняться.

Мы рассмотрим несложную навигацию между двумя компонентами Angular 2 в мобильном приложении на NativeScript под Android и iOS, используя стабильный Angular 2 Router.

Как всегда, мы начнём с чистого проекта NativeScript. Выполните в консоли следующие команды:

tns create MyProject --ng
cd MyProject
tns platform add ios
tns platform add android

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

mkdir -p components/page1
mkdir -p components/page2
touch components/page1/page1.ts
touch components/page1/page1.html
touch components/page2/page2.ts
touch components/page2/page2.html

Если вам не удалось выполнить команды mkdir и touch или вам просто лень это делать, то просто создайте эти папки и файлы вручную.

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

Откроем файл app/components/page2/page2.ts и вставим следующий код:

import {Component} from "@angular/core";

@Component({
selector: "page2",
templateUrl: "./components/page2/page2.html",
})
export class Page2Component {

public constructor() {}

}
В этом TypeScript коде нет ничего особенного, мы просто привязываем к нему соответствующий файл HTML. Не забудьте создать класс Page2Component.

После этого откроем файл app/components/page2/page2.html и добавим такую разметку:






Здесь мы создаём верхнюю панель с кнопкой «Назад» и пустой макет. Пока оставим название страницы в панели такой, этого более чем достаточно для понимания, на какой мы находимся странице.

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

Откроем файл app/components/page1/page1.ts и добавим следующий TypeScript код:

import {Component} from "@angular/core";
import {Router} from "@angular/router";

@Component({
selector: "page1",
templateUrl: "./components/page1/page1.html",
})
export class Page1Component {

public constructor(private router: Router) {

}

public onTap() {
this.router.navigate(["page2"]);
}

}
Вообще этот код сильно похож на тот, что мы прописали во второй странице. Однако, здесь мы добавили метод onTap и импортировали компонент Angular 2 Router. После добавления роутера в конструктор, мы можем использовать его для навигации к любому доступному роуту.

У нас пока нет роутов, но мы вполне можем предположить, что page2 соответствует нашей второй странице. Через минуту мы это исправим.

Откроем файл app/components/page1/page1.html и добавим в него следующую разметку HTML:






Обратили внимание на навигационную кнопку, вызывающую метод onTap при нажатии? Так будет работать навигация.

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

Создадим файл app/app.routing.ts в папке проекта. В этот файл добавим такой код:

import { Page1Component } from "./components/page1/page1";
import { Page2Component } from "./components/page2/page2";

export const appRoutes: any = [
{ path: "", component: Page1Component },
{ path: "page2", component: Page2Component }
];

export const appComponents: any = [
Page1Component,
Page2Component
];

Мы импортировали все имеющиеся страницы в первых строках файла. Зададим доступные роуты в массиве appRoutes, где path это значение, используемое в навигации TypeScript, а component это компонент, соответствующий заданному path.

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

На следующем шаге мы включим информацию о роутах во всемогущий блок @NgModule, находящийся в файле app/main.ts. Этот файл будет примерно такой:

// this import should be first in order to load some required settings (like globals and reflect-metadata)
import { platformNativeScriptDynamic, NativeScriptModule } from "nativescript-angular/platform";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { appComponents, appRoutes } from "./app.routing";

@NgModule({
declarations: [AppComponent, ...appComponents],
bootstrap: [AppComponent],
imports: [
NativeScriptModule,
NativeScriptRouterModule,
NativeScriptRouterModule.forRoot(appRoutes)
],
})
class AppComponentModule {}

platformNativeScriptDynamic().bootstrapModule(AppComponentModule);
Мы импортировали NativeScriptRouterModule и константы, заданные перед этим в файле app/app.routing.ts.

В блоке @NgModule мы определили все компоненты из массива appComponents в свойстве declarations. Импортировали NativeScriptRouterModule и доступные роуты в свойстве imports.

Всё почти готово!

Ещё хорошо бы сразу определить, откуда эти роуты будут видны. Отроем файл app/app.component.html и заменим всю HTML разметку следующей:


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

Передача параметров между роутами

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

Немного изменим наш код, чтобы можно было так и сделать.

Откроем файл app/app.routing.ts и изменим следующую строку:

{ path: "page2/:name", component: Page2Component }
Здесь мы добавили /:name в path. Теперь мы можем передать переменную, представляющую name.

Чтобы передать данные из первой страницы, откроем файл app/components/page1/page1.ts и изменим команду навигации вот так:

this.router.navigate(["page2", "Nic Raboy"]);
Здесь мы передаём строку с моим именем в навигационном запросе. На второй странице нам нужно ожидать эту переменную и получить её.

В файле app/components/page2/page2.ts заменим код на следующий:

import {Component} from "@angular/core";
import {ActivatedRoute} from "@angular/router";

@Component({
selector: "page2",
templateUrl: "./components/page2/page2.html",
})
export class Page2Component {

public fullName: string;

public constructor(private route: ActivatedRoute) {
this.route.params.subscribe((params) => {
this.fullName = params["name"];
});
}

}
Для получения данных с главной страницы, компонент ActivatedRoute должен быть импортирован. В методе constructor мы подписались на параметры навигации и присвоили их переменной fullName. После этого мы можем вывести значение этой переменной в UI.

В файле app/components/page2/page2.html изменим HTML разметку на такую:







На экран будет выведено “Hello Nic Raboy”, когда вы перейдёте на эту страницу.

Заключение

Вот так просто можно использовать стабильную версию Angular 2 Router для построения навигации между страницами в приложении NativeScript под Android и iOS. Мы построили не просто навигацию, но ещё и предусмотрели возможность передать параметр с данными между страницами.

Видеоверсия этой статью (на англ.):.

Nic Raboy Navigating A NativeScript App With The Angular 2 Router