Чтение файла формата CSV в Golang

Чтение файла формата CSV в Golang

Формат CSV довольно популярен среди тех, кто работает с табличными приложениями, такими как Google Sheets или Microsoft Excel. Как работать с этим форматом в приложениях на языке Go? Сегодня мы разберёмся с этой задачей: загрузим CSV в структуру данных, а затем выгрузим данные в JSON.

В других языках программирования работа с данными, разделёнными запятыми (CSV), бывает очень сложной. Бывает, что для этого даже пишут целые библиотеки. В Go работа с данными в формате CSV возможна «из коробки» благодаря пакету encoding/csv.

Возьмём для примера следующий данные (файл people.csv):

Nic,Raboy,San Francisco,CA
Maria,Raboy,Dublin,CA
Steve,,,

Это просто небольшой список людей. Он может содержать много строк и колонок, но для нашего примера этого достаточно.

А теперь перейдём непосредственно к коду программы:

// main.go

package main

import (
"bufio"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"log"
"os"
)

type Person struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Address *Address `json:"address,omitempty"`
}

type Address struct {
City string `json:"city"`
State string `json:"state"`
}

func main() {
csvFile, _ := os.Open("people.csv")
reader := csv.NewReader(bufio.NewReader(csvFile))
var people []Person
for {
line, error := reader.Read()
if error == io.EOF {
break
} else if error != nil {
log.Fatal(error)
}
people = append(people, Person{
Firstname: line[0],
Lastname: line[1],
Address: &Address{
City: line[2],
State: line[3],
},
})
}
peopleJson, _ := json.Marshal(people)
fmt.Println(string(peopleJson))
}

Перед запуском приложения убедитесь, что оба файла — код в файле main.go и данные в CSV в файле people.csv — существуют и находятся рядом в любой папке вашего компьютера.

Рассмотрим подробнее структуру:

type Person struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Address *Address `json:"address,omitempty"`
}

type Address struct {
City string `json:"city"`
State string `json:"state"`
}

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

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

Источник: «Parse CSV Data Using The Go Programming Language»

Как отправлять СМС и звонить с Angular NativeScript

Как отправлять СМС и звонить с Angular NativeScript

Отправлять СМС из приложения NativeScript очень просто! И сегодня я покажу вам как это сделать.

Исходный код можно скачать здесь.

Убедитесь, что у вас установлен NodeJS и NativeScript CLI.

Вначале создадим пустой проект Angular NativeScript:

tns create HelloWorld --template nativescript-template-ng-tutorial
Эта команда создаст проект smsApp. Перейдите в созданную директорию проекта и установите плагин nativescript-phone:

tns plugin add nativescript-phone
Импортируем этот модуль в файле app.component.ts:

import * as phone from 'nativescript-phone';

Для использования ngModel в нашем проекте, нужно добавить NativeScriptFormsModule в файл app.module.ts:

import {NativeScriptFormsModule} from "nativescript-angular/forms"

@NgModule({
declarations: [AppComponent],
bootstrap: [AppComponent],
imports: [NativeScriptModule, NativeScriptFormsModule],
schemas: [NO_ERRORS_SCHEMA],
})

Добавим стили в app.css для кнопок:

@import "nativescript-theme-core/css/sky.css";

.phone-button {
background-color:#2ecc71;
color:white;
}

.text-button {
background-color:#3498db;
color:white;
}

Теперь нужно разработать интерфейс. Для простоты опишем его в шаблоне в файле app.component.ts

@Component({
selector: "my-app",
template: `












`
})

Для набора номера мы можем использовать функцию phone.dial(). Мы получим этот номер через привязку к ngModel в шаблоне. Вот как выглядит эта функция:

Как отправлять СМС и звонить с Angular NativeScript
Достаточно просто. Нужно просто передать номер строкой в метод. Так как мы передаём номер числом с цифровой клавиатуры, достаточно будет просто преобразовать его в строку:

export class AppComponent {
//Переменные
phoneNumber: number;
message: string;

//Для определения какой выбор сделал пользователь
phoneType: number;

//Установим выбор пользователя (смс или звонок)
setPhoneType(val) {
this.phoneType = val;
}

callNumber() {
phone.dial(String(this.phoneNumber), true)
}
}

Для передачи СМС есть соответствующий метод:

Как отправлять СМС и звонить с Angular NativeScript
Интересное здесь то, что мы можем отправить сообщение массиву номеров, поэтому мы можем создать несколько полей для ввода номеров. Я пока передам только один номер методу:

textNumber() {
/*
Здесь у нас массив номеров, поэтому легко можем добавить ещё один номер:
phone.sms(['1234','5678'...], message)
*/
phone.sms([String(this.phoneNumber)], this.message).then((result) => {
//Возвращает promise, делайте с ним что хотите
console.log(result);
})
}

Вот полный код урока:

import { Component } from "@angular/core";
import * as phone from 'nativescript-phone';
@Component({
selector: "my-app",
template: `












`
})
export class AppComponent {
//Переменные
phoneNumber: number;
message: string;

//Для определения какой выбор сделал пользователь
phoneType: number;

//Установим выбор пользователя (смс или звонок)
setPhoneType(val) {
this.phoneType = val;
}

callNumber() {
phone.dial(String(this.phoneNumber), true)
}

textNumber() {
/*
Здесь у нас массив номеров, поэтому легко можем добавить ещё один номер:
phone.sms(['1234','5678'...], message)
*/
phone.sms([String(this.phoneNumber)], this.message).then((result) => {
//Возвращает promise, делайте с ним что хотите
console.log(result);
})
}
}

Как отправлять СМС и звонить с Angular NativeScript

Источник: «How to Send an SMS and Make Phone Calls with Angular NativeScript»

Работа с XML в приложениях Go

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

Мы увидим, как читать XML данные, преобразовывать их в структуру. А также рассмотрим как конвертировать JSON в XML.

Очень круто, что в Go для работы с JSON и XML не требуются сторонние пакеты — всё для этого уже есть в самом языке.

Для примера возьмём такой XML:

Nic
Raboy

San Francisco
CA
Maria
Raboy

Наша цель — понять как нужно работать с массивом XML-нод и вложенными данными. Короче говоря, нужно научиться работать со сложными XML данными.

Создайте и откройте файл main.go, он будет содержать такой код:

package main

import (
"encoding/json"
"encoding/xml"
"fmt"
)

type Data struct {
XMLName xml.Name `xml:"data" json:"-"`
PersonList []Person `xml:"person" json:"people"`
}

type Person struct {
XMLName xml.Name `xml:"person" json:"-"`
Firstname string `xml:"firstname" json:"firstname"`
Lastname string `xml:"lastname" json:"lastname"`
Address *Address `xml:"address" json:"address,omitempty"`
}

type Address struct {
City string `xml:"city" json:"city,omitempty"`
State string `xml:"state" json:"state,omitempty"`
}

func main() {
rawXmlData := "NicRaboy

San FranciscoCA
MariaRaboy"
var data Data
xml.Unmarshal([]byte(rawXmlData), &data)
jsonData, _ := json.Marshal(data)
fmt.Println(string(jsonData))
}

Здесь мы создали три структуры для представления каждого слоя данных XML.

Корневая нода будет представлена структурой Data. В ней вложены несколько , которые будут представлены структурой Person. Также в ноде есть адрес, который будет представлен структурой Address.

Обратите внимание на аннотации у полей структур. Эти аннотации задают привязки тегов XML к полям структуры и также к полям JSON. Аннотации с дефисом игнорируются.

При запуске приложения будет прочитан переданный XML, обработан и выведен в JSON:

{
"people": [
{
"firstname": "Nic",
"lastname": "Raboy",
"address": {
"city": "San Francisco",
"state": "CA"
}
},
{
"firstname": "Maria",
"lastname": "Raboy"
}
]
}

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

Немного доработаем функцию main в файле main.go:

func main() {
rawJsonData := "{\"people\": [{\"firstname\": \"Nic\", \"lastname\": \"Raboy\"}]}"
var data Data
json.Unmarshal([]byte(rawJsonData), &data)
xmlData, _ := xml.Marshal(data)
fmt.Println(string(xmlData))
}

Готово!

Источник: Parse XML Data In A Golang Application

Создаём укорачивалку URL на Golang с Couchbase NoSQL

Создаём укорачивалку URL на Golang с Couchbase NoSQL
Разработка сервиса по укорачиванию ссылок (такого как TinyURL или Bitly) на Go, думаю, будет очень крутым примером для начинающих. Итак, приступим!

Подготовка

Для начала проверьте, что у вас установлен Go версии не ниже 1.7 и установите Couchbase Server 4.1+

Наше приложение будет использовать запросы N1QL — SQL запросы к базе данных Couchbase NoSQL.

Подготовка базы данных, создание модели данных

Для хранения информации о длинных и коротких URL нам нужна база данных. Для нашей несложной задачи лучшим вариантом будет выбор NoSQL базы, поэтому остановимся на БД с открытым исходным кодом Couchbase.

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

Для работы нам необходимо создать и настроить хранилище данных в Couchbase.


Так как мы будем использовать запросы N1QL, нам понадобится как минимум один индекс в хранилище. Его можно создать несколькими способами: с помощью Couchbase Query Workbench или через оболочку CBQ. Запрос создания индекса будет примерно таким:

CREATE PRIMARY INDEX ON `bucket-name` USING GSI;
Для больших приложений можно создать несколько индексных полей.


Перейдём к модели данных. Наше приложение будет принимать длинный URL и отдавать соответствующий короткий URL. Оба URL будут хранится в базе данных. Вот как примерно может выглядеть модель данных:

{
"id": "5Qp8oLmWX",
"longUrl": "https://www.thepolyglotdeveloper.com/2016/08/using-couchbase-server-golang-web-application/",
"shortUrl": "http://localhost:3000/5Qp8oLmWX"
}

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

Теперь начнём разработку приложения.

Создание RESTful приложения на Golang

Мы создадим RESTful API, но перед этим нужно определиться с логикой каждой конечной точки, а также позаботиться о надёжной работе приложения.

Создадим новый проект Go. Я назову его просто main.go и он будет расположен в $GOPATH/src/github.com/nraboy/shorturl. Добавьте следующий код в файл $GOPATH/src/github.com/nraboy/shorturl/main.go:

package main

import (
"net/http"
"github.com/couchbase/gocb"
"github.com/gorilla/mux"
)

var bucket *gocb.Bucket
var bucketName string

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) { }

func CreateEndpoint(w http.ResponseWriter, req *http.Request) { }

func RootEndpoint(w http.ResponseWriter, req *http.Request) { }

func main() {
router := mux.NewRouter()
cluster, _ := gocb.Connect("couchbase://localhost")
bucketName = "example"
bucket, _ = cluster.OpenBucket(bucketName, "")
router.HandleFunc("/{id}", RootEndpoint).Methods("GET")
router.HandleFunc("/expand/", ExpandEndpoint).Methods("GET")
router.HandleFunc("/create", CreateEndpoint).Methods("PUT")
log.Fatal(http.ListenAndServe(":12345", router))
}

Рассмотрим подробно что мы сделали здесь. Мы импортировали Couchbase Go SDK и утилиту Mux, с помощью которой так легко создавать RESTful API. Установить эти пакеты можно так:

go get github.com/couchbase/gocb
go get github.com/gorilla/mux

Затем нам нужны две переменных, которые будут доступны во всём файле main.go, в них мы будем хранить копию открытого хранилища и название этого хранилища.

В методе main мы настраиваем роутер, соединяемся с локальным кластером Couchbase и открываем наше хранилище. В нашем случае открывается хранилище example, которое уже есть в кластере.

Далее мы создаём три роута, представляющие конечные точки API. Роут /create принимает длинный URL и отдаёт короткий. /expand делает обратное преобразование. И, наконец, роут /root принимает хэш и перекидывает на нужную страницу.

Логика API

Перед тем как создать логику, определим модель данных, это будет структура данных Go:

type MyUrl struct {
ID string `json:"id,omitempty"`
LongUrl string `json:"longUrl,omitempty"`
ShortUrl string `json:"shortUrl,omitempty"`
}

В структуре MyUrl есть три поля, представляющие свойством JSON.

Добавим самую сложную конечную точку /create:

func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
var url MyUrl
_ = json.NewDecoder(req.Body).Decode(&url)
var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
if err != nil {
w.WriteHeader(401)
w.Write([]byte(err.Error()))
return
}
var row MyUrl
rows.One(&row)
if row == (MyUrl{}) {
hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})
url.ShortUrl = "http://localhost:12345/" + url.ID
bucket.Insert(url.ID, url, 0)
} else {
url = row
}
json.NewEncoder(w).Encode(url)
}

Роут /create будет доступен через запрос PUT. В этом запросе будет указан длинный URL в формате JSON. Для удобства мы будем хранить весь JSON объект в объекте MyUrl.
Также необходимо убедится, что мы храним только уникальные длинные URL, а это значит, что каждый короткий URL должен быть также уникальным. Поэтому вначале мы проверяем базу данных на существование такого URL:

var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)

Здесь мы используем параметризованный запрос N1QL для проверки. При ошибках в запросе мы выведем их на экран.

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

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

Перед использованием, установим его:

go get github.com/speps/go-hashids
Для получения уникального короткого URL мы будем делать хэш из текущего времени:

hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})

После получения уникального хэша сохраним его в MyUrl вместе с коротким URL. А длинный URL уже хранится в ней.

Перейдём к /expand, вот код для роута:

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) {
var n1qlParams []interface{}
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE shortUrl = $1")
params := req.URL.Query()
n1qlParams = append(n1qlParams, params.Get("shortUrl"))
rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
var row MyUrl
rows.One(&row)
json.NewEncoder(w).Encode(row)
}

Здесь почти такой же код, как и у /create, но есть отличия: вместо N1QL запроса, мы передаём короткий URL и передаём параметры в запросе вместо того, чтобы передавать полный запрос.

Теперь остался роут root. Мы можем рассматривать все запросы к точке root как короткие URL:

func RootEndpoint(w http.ResponseWriter, req *http.Request) {
params := mux.Vars(req)
var url MyUrl
bucket.Get(params["id"], &url)
http.Redirect(w, req, url.LongUrl, 301)
}

После поиска по id, будет сделан 301 редирект на длинный URL.

Полный код проекта

package main

import (
"encoding/json"
"log"
"net/http"
"time"

"github.com/couchbase/gocb"
"github.com/gorilla/mux"
"github.com/speps/go-hashids"
)

type MyUrl struct {
ID string `json:"id,omitempty"`
LongUrl string `json:"longUrl,omitempty"`
ShortUrl string `json:"shortUrl,omitempty"`
}

var bucket *gocb.Bucket
var bucketName string

func ExpandEndpoint(w http.ResponseWriter, req *http.Request) {
var n1qlParams []interface{}
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE shortUrl = $1")
params := req.URL.Query()
n1qlParams = append(n1qlParams, params.Get("shortUrl"))
rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
var row MyUrl
rows.One(&row)
json.NewEncoder(w).Encode(row)
}

func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
var url MyUrl
_ = json.NewDecoder(req.Body).Decode(&url)
var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
if err != nil {
w.WriteHeader(401)
w.Write([]byte(err.Error()))
return
}
var row MyUrl
rows.One(&row)
if row == (MyUrl{}) {
hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})
url.ShortUrl = "http://localhost:12345/" + url.ID
bucket.Insert(url.ID, url, 0)
} else {
url = row
}
json.NewEncoder(w).Encode(url)
}

func RootEndpoint(w http.ResponseWriter, req *http.Request) {
params := mux.Vars(req)
var url MyUrl
bucket.Get(params["id"], &url)
http.Redirect(w, req, url.LongUrl, 301)
}

func main() {
router := mux.NewRouter()
cluster, _ := gocb.Connect("couchbase://localhost")
bucketName = "example"
bucket, _ = cluster.OpenBucket(bucketName, "")
router.HandleFunc("/{id}", RootEndpoint).Methods("GET")
router.HandleFunc("/expand/", ExpandEndpoint).Methods("GET")
router.HandleFunc("/create", CreateEndpoint).Methods("PUT")
log.Fatal(http.ListenAndServe(":12345", router))
}

После запуска приложения, оно будет принимать запросы на http://localhost:12345

Этот же урок в видео (на английском): https://youtu.be/OVBvOuxbpHA

По материалам: «Create A URL Shortener With Golang And Couchbase NoSQL» by Nic Raboy

Создание проекта на Angular 2 с Angular Material

Создание проекта на Angular 2 с Angular Material

В этой статье мы рассмотрим процесс создания проекта Angular2 с использованием компонентов Angular Material2.

Мы будем использовать следующие версии пакетов: Angular 2.4.6, Angular-CLI 1.0.0-beta.30 и Angular Material2 2.0.0-beta.1.

Начальная подготовка

Вначале установим библиотеку material:

npm install -S @angular/material
После установки импортируем модуль Material Module из неё в файл app.module.ts

import { MaterialModule } from ‘@angular/material';
и добавим в массив импорта

MaterialModule.forRoot()
Файл app.module.ts должен выглядеть так:

Создание проекта на Angular 2 с Angular Material

Установка HammerJS

Если вы планируете использовать в приложении слайдеры или переключатели, вам не обойтись без hammerjs. В противном случае, пропустите этот абзац.

npm install -S hammerjs
npm install --save-dev @types/hammerjs

Не забудьте импортировать hammerjs:

import 'hammerjs';
Получится примерно так:

Создание проекта на Angular 2 с Angular Material
Добавим hammerjs в файл tsconfig.json, в раздел типов:

"types": [
"hammerjs"
]

Создание проекта на Angular 2 с Angular Material

Добавим Roboto Font и Material Icons

Очень важно для стиля Material использовать специальный шрифт Roboto и иконки Material Icons, добавим их в styles.css:

@import '~https://fonts.googleapis.com/icon?family=Material+Icons';
@import '~https://fonts.googleapis.com/icon?family=Roboto';
body {
font-family: Roboto;
}

Сохраним файл и перезапустим локальный сервер — шрифты применятся. Вот пример до/после:

Создание проекта на Angular 2 с Angular Material
Создание проекта на Angular 2 с Angular Material

Темы в Angular Material2

Опции тем по-умолчанию доступны в папке /node_modules/angular/material/core/theming/prebuilt. Сейчас доступны следующие темы:

deep purple / amber,
indigo / pink,
pink / bluegrey,
purple / green

Создание проекта на Angular 2 с Angular Material
Первый цвет в названии темы это главный цвет, второй — цвет выделения. Добавим тему в styles.css:

@import '~@angular/material/core/theming/prebuilt/indigo-pink.css';
Создать свою тему вам поможет официальная документация.

А для тестирования своих тем или просмотра «вживую» официальных тем поможет сайт Material Palette.

Создание проекта на Angular 2 с Angular Material

Проверка тем

После добавления темы в приложение необходимо убедиться в её работоспособности. Добавим несколько кнопок в app.component.html и увидим тему в действии.


Создание проекта на Angular 2 с Angular Material
В результате у нас получится что-то подобное:

Создание проекта на Angular 2 с Angular Material
Если ваше приложение выглядит не так, попробуйте перезапустить сервер разработки — обычно это помогает 🙂

Исходный код этого примера можно взять здесь.

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

Источник: Setting up your first Angular (2) Project Using Angular Material (2)

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3

Это продолжение. Первая часть. Вторая часть.

Скорость работы

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

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3

Ух ты, 31 запрос и 20.8КБ передано за 546 мс. Это почти в пять раз медленне загрузки главной страницы Hacker News и вдвое больший объём данных при загрузке топиков. Это очень медленно. Даже приняв во внимание, что мы грузим главную страницу один раз и можем смириться с полусекундной задержкой, то загрузка комментариев к популярной новости займёт очень много времени!
Вы можете увидеть как я загружаю новость с 2000 комментариев тут. Если вам лень смотреть гифку, то вот статистика: там 741 запрос, 1,5 МБ и 90 сек для загрузки примерно 700 комментариев (я не стал ждать пока все комментарии загрузятся).

Вносим коррективы

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

Вот пример ответа по запросу списка популярных историй:

// https://node-hnapi.herokuapp.com/news?page=1

[
{
"id": 12469856,
"title": "Owl Lisp – A purely functional Scheme that compiles to C",
"points": 57,
"user": "rcarmo",
"time": 1473524669,
"time_ago": "2 hours ago",
"comments_count": 9,
"type": "link",
"url": "https://github.com/aoh/owl-lisp",
"domain": "github.com"
},
{
"id": 12469823,
"title": "How to Write Articles and Essays Quickly and Expertly",
"points": 52,
"user": "bemmu",
"time": 1473524142,
"time_ago": "2 hours ago",
"comments_count": 6,
"type": "link",
"url": "https://www.downes.ca/post/38526",
"domain": "downes.ca"
},
...
]

Мы видим, что здесь есть и такие атрибуты, как domain и time_ago, это очень круто. И это значит, что мы можем выкинуть файл domain.pipe.ts, созданный ранее, а также удалить библиотеку angular2-moment. И доработаем нашу службу:

// hackernews-api.service.ts

export class HackerNewsAPIService {
baseUrl: string;

constructor(private http: Http) {
this.baseUrl = 'https://node-hnapi.herokuapp.com';
}

fetchStories(storyType: string, page: number): Observable {
return this.http.get(`${this.baseUrl}/${storyType}?page=${page}`)
.map(response => response.json());
}
}

Так как API не загружает все 500 топиков, нам необходимо будет добавить номер страницы как аргумент. Обратите внимание также на то, как мы передаём storyType — это позволит нам показывать разные типы топиков, в зависимости от запроса пользователя.

Доработаем компонент stories. Можно начать лишь передав ‘news’ и номер страницы 1 в вызов службы для получения топа:

// stories.component.ts

export class StoriesComponent implements OnInit {
items;

constructor(private _hackerNewsAPIService: HackerNewsAPIService) {}

ngOnInit() {
this._hackerNewsAPIService.fetchStories('news', 1)
.subscribe(
items => this.items = items,
error => console.log('Error fetching stories'));
}
}

Соответствующая разметка:


И нам нужно ещё добавить индикатор загрузки.

Доработаем ItemComponent — в файле item.component.ts уже не нужен HackerNewsService:

// item.component.ts

export class ItemComponent implements OnInit {
@Input() item;

constructor() {}

ngOnInit() {

}
}

Разметка:


{{item.title}}

({{item.domain}})


{{item.points}} points by
{{item.user}}


{{item.time_ago}}
|


{{item.comments_count}}
comment
comments

discuss



И посмотрим что у нас получилось:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3
Всё работает намного быстрее! Исходный код этого этапа можно скачать здесь.

Роутинг

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

Начнём с того, что мы уже сделали:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3
Также обрисуем компоненты, показывающие переход к странице комментариев:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3
Чтобы пользователь мог переходить между этими страницами, нам нужен небольшой роутинг. Создадим компонент:

ng g component ItemComments
А теперь создадим файл app.routes.ts в папке app.

// app.routes.ts

import { Routes, RouterModule } from '@angular/router';

import { StoriesComponent } from './stories/stories.component';
import { ItemCommentsComponent } from './item-comments/item-comments.component';

const routes: Routes = [
{path: '', redirectTo: 'news/1', pathMatch : 'full'},
{path: 'news/:page', component: StoriesComponent, data: {storiesType: 'news'}},
{path: 'newest/:page', component: StoriesComponent, data: {storiesType: 'newest'}},
{path: 'show/:page', component: StoriesComponent, data: {storiesType: 'show'}},
{path: 'ask/:page', component: StoriesComponent, data: {storiesType: 'ask'}},
{path: 'jobs/:page', component: StoriesComponent, data: {storiesType: 'jobs'}},
{path: 'item/:id', component: ItemCommentsComponent}
];

export const routing = RouterModule.forRoot(routes);
Вот что мы сделали:

  1. Мы создали массив роутов, с указанием относительного пути и привязкой к конкретному компоненту
  2. Ссылки в шапке страницы будут указывать на разные пути: news, newest, show, ask и jobs. Все эти пути привязаны к StoriesComponent
  3. С корневого пути мы сделаем редирект на news, возвращающий топ историй
  4. При привязке StoriesComponent мы передаём storiesType как параметр свойства data.
  5. :page используем как токен, поэтому StoriesComponent может получать список топиков определённой страницы
  6. :id используется также, поэтому ItemCommentsComponent получает все комментарии к нужному топику

С роутингом можно сделать ещё много интересного, но этой основы нам пока достаточно. Откроем app.module.ts и зарегистрируем наш роутинг:

// app.module.ts

// ...
import { routing } from './app.routes';

@NgModule({
declarations: [
//...
],
imports: [
//...
routing
],
providers: [HackerNewsAPIService],
bootstrap: [AppComponent]
})
export class AppModule { }

Чтобы Angular знал, куда загружать нужный компонент, нам нужен RouterOutlet.



Навигация по топикам

Привяжем навигационные ссылки в HeaderComponent к соответствующим роутам:


Директива RouterLink ответственна за привязку определённого элемента к роуту. Теперь обновим StoriesComponent:

// stories.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute } from '@angular/router';

import { HackerNewsAPIService } from '../hackernews-api.service';

@Component({
selector: 'app-stories',
templateUrl: './stories.component.html',
styleUrls: ['./stories.component.scss']
})

export class StoriesComponent implements OnInit {
typeSub: any;
pageSub: any;
items;
storiesType;
pageNum: number;
listStart: number;

constructor(
private _hackerNewsAPIService: HackerNewsAPIService,
private route: ActivatedRoute
) {}

ngOnInit() {
this.typeSub = this.route
.data
.subscribe(data => this.storiesType = (data as any).storiesType);

this.pageSub = this.route.params.subscribe(params => {
this.pageNum = +params['page'] ? +params['page'] : 1;
this._hackerNewsAPIService.fetchStories(this.storiesType, this.pageNum)
.subscribe(
items => this.items = items,
error => console.log('Error fetching' + this.storiesType + 'stories'),
() => this.listStart = ((this.pageNum - 1) * 30) + 1);
});
}
}

Опишем вкратце, что мы сделали. Вначале мы импортировали ActivatedRoute — это служба, обеспечивающая доступ к информации в роуте.

import { ActivatedRoute } from '@angular/router';

@Component({
//...
})

export class StoriesComponent implements OnInit {
//..

constructor(
private route: ActivatedRoute
) {}
//...
}

Затем мы подписываемся на свойства данных роута и сохраняем storiesType в переменной компонента в хуке ngOnInit.

ngOnInit() {
this.typeSub = this.route
.data
.subscribe(data => this.storiesType = (data as any).storiesType);

// ...
}

И, наконец, мы подписываемся на параметры роута и получаем номер страницы. Затем получаем список топиков:

ngOnInit() {
// ...

this.pageSub = this.route.params.subscribe(params => {
this.pageNum = +params['page'] ? +params['page'] : 1;
this._hackerNewsAPIService.fetchStories(this.storiesType, this.pageNum)
.subscribe(
items => this.items = items,
error => console.log('Error fetching' + this.storiesType + 'stories'),
() => {
this.listStart = ((this.pageNum - 1) * 30) + 1;
window.scrollTo(0, 0);
});
});
}

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


Главная страница готова, у нас есть навигация и пагинация. А лучше сами проверьте как работает приложение.

Комментарии

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


{{item.title}}

({{item.domain}})


{{item.points}} points by
{{item.user}}


{{item.time_ago}}
|


{{item.comments_count}}
comment
comments

discuss



Запустите приложение и кликните на комментарии топика:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3
Красота. Роутинг к ItemCommentsComponent работает. Теперь создадим остальные компоненты.

ng g component CommentTree
ng g component Comment

Добавим новый GET-запрос в нашу службу для получения комментариев.

// hackernews.api.service.ts

//...

fetchComments(id: number): Observable {
return this.http.get(`${this.baseUrl}/item/${id}`)
.map(response => response.json());
}

И заполним наши компоненты

// item-comments.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { HackerNewsAPIService } from '../hackernews-api.service';

@Component({
selector: 'app-item-comments',
templateUrl: './item-comments.component.html',
styleUrls: ['./item-comments.component.scss']
})
export class ItemCommentsComponent implements OnInit {
sub: any;
item;

constructor(
private _hackerNewsAPIService: HackerNewsAPIService,
private route: ActivatedRoute
) {}

ngOnInit() {
this.sub = this.route.params.subscribe(params => {
let itemID = +params['id'];
this._hackerNewsAPIService.fetchComments(itemID).subscribe(data => {
this.item = data;
}, error => console.log('Could not load item' + itemID));
});
}
}

Также как в StoriesComponent, мы сделаем подписку к параметрам роута, получим id элемента и по нему получим нужные комментарии.


{{item.title}}

({{item.domain}})


{{item.points}} points by
{{item.user}}


{{item.time_ago}}
|


{{item.comments_count}}
comment
comments

discuss



В начале компонента мы выводим детали элемента, идущие за его описанием (item.content). Затем вводим объект комментариев (item.comments) в app-comment-tree, селектор для CommentTreeComponent. Стили для этого компонента можно скачать здесь.

Теперь доработаем CommentTreeComponent.

// comment-tree.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'app-comment-tree',
templateUrl: './comment-tree.component.html',
styleUrls: ['./comment-tree.component.scss']
})
export class CommentTreeComponent implements OnInit {
@Input() commentTree;

constructor() {}

ngOnInit() {

}
}


Мы выводим список комментариев директивой ngFor. Здесь можно скачать стили.

Доработаем CommentComponent, отвечающий за конкретный комментарий:

// comment.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'app-comment',
templateUrl: './comment.component.html',
styleUrls: ['./comment.component.scss']
})
export class CommentComponent implements OnInit {
@Input() comment;
collapse: boolean;

constructor() {}

ngOnInit() {
this.collapse = false;
}
}

[{{collapse ? '+' : '-'}}]
{{comment.user}}
{{comment.time_ago}}

[deleted] | Comment Deleted

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

Создание hacker news с angular 2 cli, rxjs и webpack, часть 3
Исходный код этого этапа можно взять здесь.

Профили пользователей

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

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

Вот готовый код этой части.

Заключение

Мы закончили. Для сборки приложения можно запустить

ng build --prod или
ng serve --prod

Источник: «building hacker news with angular 2 cli, rxjs and webpack»

Создание hacker news с angular 2 cli, rxjs и webpack, часть 2

Создание hacker news с angular 2 cli, rxjs и webpack, часть 2
Первая часть.

RxJS и Observables

В Angular 2 для общения с сервером мы используем библиотеку RxJS, которая возвращает Observable с данными, или асинхронный поток данных. Вероятно, вы уже знакомы с концепцией Promise-ов и как с их помощью можно асинхронно получать данные. Observable получают данные подобно promise-ам, но при этом позволяют следить за потоком данных и реагировать на различные события с ним.

Создание hacker news с angular 2 cli, rxjs и webpack, часть 2
Источник: Вступление в Реактивное Программирование, которое вы пропустили

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

Концепция использования Observable в приложениях известна как Реактивное Программирование.

Observable Data Service

Пришло время для получения реальных данных. Для этого нам нужно создать Observable Data Service и включить его в наши компоненты.

ng g service hackernews-api
Будет создан и настроен файл службы. А ещё нам следует разобраться с тем, как работает Hacker News API. Из документации понятно, что всё (опросы, комментарии, топики, вакансии) это элементы с различающимися id. И информация по конкретному элементу может быть получена по специальному адресу

// https://hacker-news.firebaseio.com/v0/item/2.json?print=pretty

{
"by" : "phyllis",
"descendants" : 0,
"id" : 2,
"kids" : [ 454411 ],
"score" : 16,
"time" : 1160418628,
"title" : "A Student's Guide to Startups",
"type" : "story",
"url" : "https://www.paulgraham.com/mit.html"
}

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

// https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty

[ 12426766, 12426315, 12424656, 12425725, 12426064, 12427341, 12425692, 12425776, 12425324, 12425750, 12425135, 12427073, 12425632, 12423733, 12425720, 12427135, 12425683, 12423794, 12424987, 12423809, 12424738, 12425119, 12426759, 12425711, 12422891, 12424731, 12423742, 12424131, 12424184, 12422833, 12424421, 12426729, 12423373, 12421687, 12427437 ...]
Таким образом, мы получаем список топиков в топе, а затем необходимо пройтись по каждому из них. Приступим.

Включим службу в метаданные provider нашего NgModule:

// app.module.ts

//...
import { HackerNewsAPIService } from './hackernews-api.service';

@NgModule({
declarations: [
...
],
imports: [
...
],
providers: [HackerNewsAPIService],
bootstrap: [AppComponent]
})
export class AppModule { }

Теперь добавим метод для запроса в неё:

// hackernews-api.service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Injectable()
export class HackerNewsAPIService {
baseUrl: string;

constructor(private http: Http) {
this.baseUrl = 'https://hacker-news.firebaseio.com/v0';
}

fetchStories(): Observable {
return this.http.get(`${this.baseUrl}/topstories.json`)
.map(response => response.json());
}
}

Как мы говорили ранее, вызов http.get возвращает Observable с данными. В fetchStories мы принимаем Observable, а затем map-им его в формат JSON.

// stories.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { HackerNewsAPIService } from '../hackernews-api.service';

@Component({
selector: 'app-stories',
templateUrl: './stories.component.html',
styleUrls: ['./stories.component.scss']
})

export class StoriesComponent implements OnInit {
items;

constructor(private _hackerNewsAPIService: HackerNewsAPIService) {}

ngOnInit() {
this._hackerNewsAPIService.fetchStories()
.subscribe(
items => this.items = items,
error => console.log('Error fetching stories'));
}
}

В хуке ngOnInit, который срабатывает при инициализации компонента, мы подписываемся (subscribe) на поток данных и присваиваем атрибуту items то, что нам будет возвращено. А в наше представление мы добавим SlicePipe для вывода только 30 элементов списка из 500, которые нам возвращает запрос.


Запустив приложение, увидим список элементов с их id:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 2
Итак, мы получаем идентификатор каждого item, теперь добавим подписку на детали каждого элемента и для этого напишем новый метод:

// hackernews-api.service.ts

//...

fetchItem(id: number): Observable {
return this.http.get(`${this.baseUrl}/item/${id}.json`)
.map(response => response.json());
}

Немного доработаем компонент item:

// item.component.ts

import { Component, Input, OnInit } from '@angular/core';

import { HackerNewsAPIService } from '../hackernews-api.service';

@Component({
selector: 'item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.scss']
})
export class ItemComponent implements OnInit {
@Input() itemID: number;
item;

constructor(private _hackerNewsAPIService: HackerNewsAPIService) {}

ngOnInit() {
this._hackerNewsAPIService.fetchItem(this.itemID).subscribe(data => {
this.item = data;
}, error => console.log('Could not load item' + this.itemID));
}
}


{{item.title}}

{{item.url | domain}}

{{item.score}} points by
{{item.by}}
{{ (item.time | amFromUnix) | amTimeAgo }}


{{item.descendants}}
comment
comments

discuss


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

Код приложения на текущем этапе можно скачать здесь. Перезапустите приложение и увидим такую картину:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 2

Окончание.

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Если вы когда-либо создавали приложения с Angular 2, то знаете, что первоначальная настройка проекта отнимает немало времени. К счастью, команда Angular создала Angular CLI — утилиту командной строки, облегчающую эту задачу.

В этой статье мы построим клиента Hacker News, используя Angular CLI, RxJS Observables и Webpack как загрузчик модулей.

Готовое приложение.
Исходный код.

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Мы постепенно, шаг за шагом, пройдём весь процесс построения приложения и я постараюсь подробно объяснять важные моменты по ходу дела, а также сделанные мной ошибки и способы их решения.

Вот краткий список того, что нам предстоит сделать:

  1. Мы начнём с построения каркаса приложения, главной страницы Hacker News
  2. Затем подключим Observable Data Service для асинхронной загрузки данных
  3. Добавим роутинг с Angular Component Router для построения навигации между страницами и видами топиков
  4. И, наконец, добавим роуты чтобы пользователь мог перейти к комментариям к топику и в профили пользователей.
Приступим

Убедитесь, что у вас установлен Node и npm, затем установите CLI в терминале:

npm install -g @angular/cli
Создадим и запустим приложение:

ng new angular2-hn
cd angular2-hn
ng serve

Откройте браузер по адресу https://localhost:4200/

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Круто, да?

Настроим Sass как препроцессор CSS:

ng set defaults.styleExt scss
Создадим первый компонент HeaderComponent

ng generate component Header
Будет создана папка header, содержащая такие файлы:

  • header.component.scss
  • header.component.html
  • header.component.ts
  • header.component.spec.ts

Посмотрим на файл app.module.ts и увидим, что наш компонент уже задекларирован:

// app.module.ts

// ...
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';

@NgModule({
declarations: [
AppComponent,
HeaderComponent
],
//...

А взглянув в файл header.component.ts, вы увидите, что селектор компонентов — app-header. Добавим его в наш корневой компонент, app.component.ts.


Запустим приложение. Компонент header загрузился нормально:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Супер. Теперь добавим кое-какую разметку и стили.


Стили из файла app.component.scss можете скачать здесь. Перейдём к header.


Стили этого компонента можно скачать здесь. Запустим приложение:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1

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

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Но у нас с края появился непредвиденный отступ. Это из-за того, что элемент body имеет отступ по-умолчанию (через margin):

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Но если открыть app.component.scss, там указано правило margin: 0 для экранов меньше 768px:

$mobile-only: "only screen and (max-width : 768px)";

body {
margin-bottom: 0;

@media #{$mobile-only} {
margin: 0;
}
}

Так почему же оно так работает? Это из-за способа, которым Angular инкапсулирует CSS-стили компонента. Не будем вдаваться в детали, но есть три способа, которые может использовать Angular для этого:

  • None: Angular ничего не предпринимает — ни инкапсуляции, ни Shadow DOM, просто обычная загрузка стилей.
  • Emulated: Angular эмулирует поведение Shadow DOM. Это способ по-умолчанию.
  • Native: Angular использует нативный Shadow DOM браузера (только в браузерах, имеющих соответствующую поддержку).

В корневом компоненте мы добавляем стили элементу body, но оно не работает, потому что мы не указали Angular не применять никаких действий к представлению компонента:

// app.component.ts

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'app-root',
encapsulation: ViewEncapsulation.None,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})

export class AppComponent {
}

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

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1

Несколько компонентов

Добавим ещё пару компонентов, Stories и Footer. Stories представляют топики в Hacker News, и мы начнём с каркаса, добавив в него упорядоченный список.

ng g component Stories
// stories.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-stories',
templateUrl: './stories.component.html',
styleUrls: ['./stories.component.scss']
})

export class StoriesComponent implements OnInit {
items: number[];

constructor() {
this.items = Array(30);
}

ngOnInit() {
}
}

  1. Story #{{i}}


Стили для Stories можно взять здесь. Подвал страницы очень простой (стили для него скачайте тут).

ng g component Footer


Обновим корневой компонент, чтобы увидеть добавленные компоненты:




Вот так теперь выглядит приложение:

Создание hacker news с angular 2 cli, rxjs и webpack, часть 1
Так как каждый топик, или элемент, будет иметь собственные атрибуты, имеет смысл создать отдельный компонент для этого.

ng g component Item
Когда у нас будут реальные данные, нужно будет передавать идентификатор элемента из компонента топиков его дочернему элементу. Тем временем, сделаем передачу позиции списка как itemID:


// item.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.scss']
})
export class ItemComponent implements OnInit {
@Input() itemID: number;

constructor() { }

ngOnInit() {
}

}

Story #{{itemID}}


Перезапустите приложение, всё должно работать так же, а это значит, что параметр позиции передаётся успешно с @Input.

Итак, у нас получился отличный каркас главной страницы. Здесь исходный код примера на текущем этапе.

Продолжение.

Генерируем Identicon в Go

Генерируем Identicon в Go
В этой статье мы разработаем простой генератор идентиконов на языке Go. Исходный код вы можете взять здесь.

Но что такое идентикон? Думаю, все видели стандартную аватарку при регистрации на Github. Вот пример:
Генерируем Identicon в Go
Вы видите визуальное представление значения хэш-функции. То есть с помощью специального алгоритма (md5 или sha256 и т.п.) захэшировано какое-то слово, при этом вывод этого алгоритма используется для генерации картинки. Поэтому идентикон будет всегда один и тот же при одинаковых входных данных.

Но хватит болтать, пора программировать!

Начнём с описания структуры идентикона:

type Identicon struct {
name string
hash [16]byte
color [3]byte
}

Структура имеет три поля:

  • name: Переданное имя для генерации идентикона.
  • hash: 16 байтовый массив, содержащий хэшированное значение имени, мы используем [16]byte из-за того, что наши хэширующие функции возвращают байтовый массив с 16 значениями.
  • color: 3 байтовый массив, содержащий цвета Red,Green,Blue

Итак, мы описали базовый контейнер для хранения значений. Остаётся слушать пользовательский ввод и хэшировать введённые значения.
Мы используем метод Sum из пакета «crypto/md5», этот метод возвращает [16]byte контрольную сумму от переданного параметра:

func hashInput(input []byte) Identicon {
// генерируем контрольную сумму из ввода
checkSum := md5.Sum(input)
// возвращаем identicon
return Identicon{
name: string(input),
hash: checkSum,
}
}

Мы можем использовать метод так:

// конвертируем строку в массив байтов
data := []byte("bart")
// вызываем метод hashinput для этого массива байтов
hashInput(data)
// на выходе: [245 65 70 163 252 130 171 23 229 38 86 149 178 63 100 107]

Теперь необходимо задать цвет идентикона, ведь мы хотим, чтобы он был всегда одинаковым при одном и том же вводе. Трюк в том, что мы возьмём первые 3 байта контрольной суммы и, соответственно, цвет всегда будет одинаковым. Первый будет отвечать за Красный (Red), второй за Зелёный (Green), а третий за Синий (Blue). Так у нас получится валидное значение RGB и мы сможем его использовать вместе с библиотекой color в go.

func pickColor(identicon Identicon) Identicon {
// сначала создадим массив байтов, размером 3 элемента
rgb := [3]byte{}
// затем скопируем первые 3 элемента хэша в массив rgb
copy(rgb[:], identicon.hash[:3])
// затем присвоим его полю color
identicon.color = rgb
// вернём изменённый identicon
return identicon
}

Сейчас в нашем идентиконе есть имя, цвет и хэш. И нюанс в том, что его левая сторона зеркально копирует правую. Нам нужно создать сетку размером 5х5 и если мы возьмём наш массив байтов [245 65 70 163 252 130 171 23 229 38 86 149 178 63 100 107] для примера, то после создания сетки получим

[ 245, 65, 70, 65, 245,
163, 252, 130, 252, 163,
171, 23, 229, 23, 171,
38, 86, 149, 86, 38,
178, 63, 100, 63, 178 ]

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

type Identicon struct {
name string
hash [16]byte
color [3]byte
grid []byte // новое поле для сетки
}

func buildGrid(identicon Identicon) Identicon {
// Создадим пустую сетку
grid := []byte{}
// Обходим в цикле весь хэш идентикона с шагом 3,
// соответственно мы исключим ситуацию обращения за границами массива
// и получим 5 чанков по 3 элемента
for i := 0; i < len(identicon.hash) && i+3 <= len(identicon.hash)-1; i += 3 { // создадим пустой chunk chunk := make([]byte, 5) // Скопируем элементы из старого массива в новый copy(chunk, identicon.hash[i:i+3]) chunk[3] = chunk[1] // зеркалируем второй элемент chunk[4] = chunk[0] // зеркалируем первый элемент grid = append(grid, chunk...) // добавляем chunk в сетку } identicon.grid = grid // заполняем поле grid в идентиконе return identicon // возвращаем изменённый identicon }

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

type GridPoint struct {
value byte
index int
}

Добавим её в структуру идентикона для удобства.

type Identicon struct {
name string
hash [16]byte
color [3]byte
grid []byte
gridPoints []GridPoint // Отфильтрованные ячейки сетки
}

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

func filterOddSquares(identicon Identicon) Identicon {
grid := []GridPoint{} // создадим пустую сетку, будем заполнять её в цикле
for i, code := range identicon.grid { // идём в цикле по нашей сетке
if code{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2 == 0 { // проверяем - нечётное ли число
// создадим новую Gridpoint, куда положим значение и индекс элементов
point := GridPoint{
value: code,
index: i,
}
// добавим элемент к новой сетке
grid = append(grid, point)
}
}
// присвоим значение
identicon.gridPoints = grid
return identicon // возвращаем изменённый идентикон
}

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

Создадим структуру для хранения размеров.

type Point struct {
x, y int
}

type DrawingPoint struct {
topLeft Point
bottomRight Point
}

Но как нам связать ячейки сетки с пикселями на пиксельной карте? Вначале определимся с размером изображения - оно будет размером 250 на 250 пикселей. Так как сетка у нас 5х5, то 1 ячейка сетки будет размером 50 на 50 пикселей, ибо 250/5 = 50.
Теперь рассчитаем границы по таким формулам:

  • горизонтальная: (x {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} 5) * 50
  • вертикальная: (x / 50) * 50

где x = индекс ячейки в сетке

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

type Identicon struct {
name string
hash [16]byte
color [3]byte
grid []byte
gridPoints []GridPoint
pixelMap []DrawingPoint // пиксельная карта для рисования
}

И сохраним карту в структуре идентикона:

func buildPixelMap(identicon Identicon) Identicon {
drawingPoints := []DrawingPoint{}

pixelFunc := func(p GridPoint) DrawingPoint {
horizontal := (p.index {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} 5) * 50
vertical := (p.index / 5) * 50
topLeft := Point{horizontal, vertical}
bottomRight := Point{horizontal + 50, vertical + 50}

return DrawingPoint{
topLeft,
bottomRight,
}
}

for _, gridPoint := range identicon.gridPoints {
drawingPoints = append(drawingPoints, pixelFunc(gridPoint))
}
identicon.pixelMap = drawingPoints
return identicon
}

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

func rect(img *image.RGBA, col color.Color, x1, y1, x2, y2 float64) {
gc := draw2dimg.NewGraphicContext(img) // подготовим новый контекст изображения
gc.SetFillColor(col) // зададим цвет
gc.MoveTo(x1, y1) // перенесёмся в левый верхний край картинки
// нарисуем габаритные линии
gc.LineTo(x1, y1)
gc.LineTo(x1, y2)
gc.MoveTo(x2, y1) // передвинемся вправый край изображения
// нарисуем габаритные линии
gc.LineTo(x2, y1)
gc.LineTo(x2, y2)
// сделаем ширину линии в ноль
gc.SetLineWidth(0)
// Заполним ячейку
gc.FillStroke()
}

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

func drawRectangle(identicon Identicon) error {
// создадим пустую картинку размером 250x250
var img = image.NewRGBA(image.Rect(0, 0, 250, 250))
// получим цвет из соответствующего поля идентикона
col := color.RGBA{identicon.color[0], identicon.color[1], identicon.color[2], 255}

// обойдём в цикле pixelmap вызывая функцию rect с передачей ей картинки, цвета и размеров
for _, pixel := range identicon.pixelMap {
rect(
img,
col,
float64(pixel.topLeft.x),
float64(pixel.topLeft.y),
float64(pixel.bottomRight.x),
float64(pixel.bottomRight.y)
)
}
// Сохраним изображение на диск
return draw2dimg.SaveToPngFile(identicon.name+".png", img)
}

Готово! Генератор идентикона сделан, осталось одна вещь. Вы видели, что большинство наших функций изменяют идентикон и возвращают идентикон. Это хорошая работа для конвейерной функции. Сделаем её набросок:

type Apply func(Identicon) Identicon
И определим функцию, принимающую x количество типов Apply и применяющих их к идентикону:

func pipe(identicon Identicon, funcs ...Apply) Identicon {
for _, applyer := range funcs {
identicon = applyer(identicon)
}
return identicon
}

Теперь пропишем весь функционал в методе main() и создадим флаг определения ввода имени в функцию:

func main() {
var (
name = flag.String("name", "", "Set the name where you want to generate an Identicon for")
)
flag.Parse()

if *name == "" {
flag.Usage()
os.Exit(0)
}

data := []byte(*name)
identicon := hashInput(data)

// Передадим идентикон, вызывая методы для его обработки
identicon = pipe(identicon, pickColor, buildGrid, filterOddSquares, buildPixelMap)

// Передадим идентикон в функцию drawRectangle
if err := drawRectangle(identicon); err != nil {
log.Fatalln(err)
}
}

Выполнив этот код с именем "bart" мы получим:
Генерируем Identicon в Go
Поздравляем, вы создали генератор идентиконов!

Источник: "Tutorial: Identicon generator in Go" by Bart Fokker

Создание списка задач с Vue.js

Создание списка задач с Vue.js

Сегодня мы создадим приложение — список задач с Vue.js, а также рассмотрим и другие удобные инструменты для создания современных веб-приложений.

Убедитесь, что у вас установлена Vue CLI ! Если нет, то установите её командой
$ npm install --global vue-cli

С Vue CLI идут несколько шаблонов готовых приложений:

webpack - Полнофункциональная установка Webpack + Vue-loader с hot reload, linting, testing & CSS extraction.

webpack-simple — Упрощённая установка Webpack + Vue-loader.

browserify — Полнофункциональная установка Browserify + vueify с hot-reload, linting & unit testing.

browserify-simple — Упрощённая установка Browserify + vueify.

simple - Простенькая установка Vue всего в одном файле HTML
Создадим приложение:

$ vue init webpack todo-app
Вас спросят название для проекта, описание, автора и сборку Vue. Мы будем работать не устанавливая Vue-router.

Установим зависимости:

$ cd my-project
$ npm install

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

$ npm run dev
Откроется браузер на странице http://localhost:8080

Создание списка задач с Vue.js

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

Структура компонента

Каждое приложение Vue имеет компонент верхнего уровня, у нас таким компонентом будет TodoList.

У нас уже есть готовый компонент Hello (сгенерированный Vue CLI) в папке src/App.vue, осталось создать недостающие компоненты нижнего уровня.
Создадим простенький компонент TodoList.vue:

  • Todo A
  • Todo B
  • Todo C


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

Импортируем наш компонент в главный экземпляр Vue, чтобы можно было с ним работать. Откройте файл src/App.vue и отредактируйте его:


Для обработки компонента необходимо его включить в HTML: // Обработка компонента TodoList
// он будет здесь:


Теперь наше приложение выглядит так:

Создание списка задач с Vue.js

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

Добавим данных в наш компонент:

export default {
name: 'app',
components: {
TodoList,
},
// data function avails data to the template
data() {
return {
todos: [{
title: 'Todo A',
project: 'Project A',
done: false,
}, {
title: 'Todo B',
project: 'Project B',
done: true,
}, {
title: 'Todo C',
project: 'Project C',
done: false,
}, {
title: 'Todo D',
project: 'Project D',
done: false,
}],
};
},
};

Нам нужно передать данные из главного компонента в TodoList и для этого мы будем использовать директиву v-bind. Она принимает аргумент (он отделяется двоеточием от имени директивы), в нашем случае это будет todos; это показывает директиве v-bind связать элемент todos с тем, что идёт в нём.


Теперь нужно доработать компонент TodoList для доступа к этим данным. Добавим свойство к классу компонента:

export default {
props: ['todos'],
}

Обработка данных

Пройдёмся в цикле по списку задач в шаблоне TodoList и выведем количество завершённых и незавершённых задач: // JavaScript expressions in Vue are enclosed in double curly brackets.

Completed Tasks: {{todos.filter(todo => {return todo.done === true}).length}}

Pending Tasks: {{todos.filter(todo => {return todo.done === false}).length}} {{ todo.title }} {{ todo.project }}

Completed Complete

Редактирование задач

Разделим шаблон задачи для улучшения читабельности кода. Создайте новый компонент Todo.vue в папке src/components и перенесите в него шаблон задачи: {{ todo.title }} {{ todo.project }}

Completed Complete


Переработаем компонент TodoList для работы с компонентом Todo:

Completed Tasks: {{todos.filter(todo => {return todo.done === true}).length}}

Pending Tasks: {{todos.filter(todo => {return todo.done === false}).length}}

// we are now passing the data to the todo component to render the todo list


Добавим свойство isEditing в класс компонента Todo, это свойство покажет редактируется ли в данный момент задача или нет. Повесим обработчик события на элемент Edit и будем показывать форму редактирования, при этом изменим значение свойства isEditing в true. Осталось добавить форму и установить начальное значение свойства. Наш шаблон теперь выглядит так: // Todo shown when we are not in editing mode. {{ todo.title }} {{ todo.project }}

// form is visible when we are in editing mode Title
Project

Close X
Completed Pending


У нас есть метод showForm, открывающий форму правки, теперь добавим метод hideForm для её закрытия при нажатии на кнопку отмены:


Данные в форме уже привязаны к списку задач и их редактирование автоматически сохраняет обновлённые данные. По нажатию кнопки Close увидим обновлённую задачу:

Создание списка задач с Vue.js
Удаление задачи

Добавим иконку удаления задачи:





/* add the trash icon in below the edit icon in the template */




Добавим метод в класс компонента, вызываемый по событию delete-todo, и передающий компоненту текущую задачу к удалению.


// Todo component
methods: {
deleteTodo(todo) {
this.$emit('delete-todo', todo);
},
},

Теперь нужно добавить обработчик события удаления в родительский компонент (TodoList):

// TodoList component
methods: {
deleteTodo(todo) {
const todoIndex = this.todos.indexOf(todo);
this.todos.splice(todoIndex, 1);
},
},

Передаём метод deleteTodo в компонент Todo:

// шаблон TodoList

Добавление новой задачи

Для создания новой задачи сделаем новый компонент CreateTodo в папке src/components. Он выведет кнопку со знаком плюс, она при нажатии откроет форму добавления задачи:

Title
Project

Create

Cancel


Добавим новый компонент в главный компонент:

// главный компонент в App.vue
components: {
TodoList,
CreateTodo,
},

А также добавим метод для создания новой задачи:

// в App.vue
methods: {
addTodo(title) {
this.todos.push({
title,
done: false,
});
},
},

Вызов компонента CreateTodo из шаблона App.vue:

Создание списка задач с Vue.js
Выполнение задачи

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

// компонент Todo
methods: {
completeTodo(todo) {
this.$emit('complete-todo', todo);
},
}

Обработчик события будет добавлен в TodoList:

methods: {
completeTodo(todo) {
const todoIndex = this.todos.indexOf(todo);
this.todos[todoIndex].done = true;
},
},

И добавим вызов метода в шаблоне:

Готово! Ниже по ссылкам можно увидеть полный код приложения или пощупать демонстрационную версию.

Полный код примера. Демо-версия.

По материалам «Build a To-Do App with Vue.js 2» by Jeremy Kithome