Как сделать «Избранное» средствами Laravel и Vue.js

Как сделать "Избранное" средствами Laravel и Vue.js

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

Мы рассмотрим как реализовать такой функционал с Vue.js в приложении Laravel. Это приложение будет типа персонального блога, в нём будут пользователи и статьи. Пользователи смогут создавать статьи и добавлять их в избранное. А также пользователи смогут видеть полный список избранных статей.

В приложении будут модель User (Пользователь) и модель Post (Статья), в нём будет система авторизации, которая позволит добавлять статьи в Избранное (и удалять из него) только авторизованным пользователям. Мы сделаем динамическую пометку в избранное средствами VueJs и Axios, т.е. без перезагрузки страницы.

Перед началом работы посмотрите как это будет выглядеть:

Как сделать "Избранное" средствами Laravel и Vue.js

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

Мы начнём с создания нового проекта Laravel:

laravel new laravel-vue-favorite
В результате будет создан проект с именем «laravel-vue-favorite»

С Laravel идут дополнительные библиотеки, такие как Bootstrap, VueJs и Axios, однако нужно их установить с помощью npm:

npm install
Также эта команда установит Laravel Mix, с помощью которого мы будем компилировать и собирать наши CSS и JavaScript.

Модели, миграции

Итак, нам необходима модель User (она идёт с Laravel), модель Post и модель Favorite, а также файлы миграции для них.

php artisan make:model Post -m
php artisan make:model Favorite -m

Эти команды создадут нужные нам модели вместе с миграциями. Откроем файл миграции таблицы posts и обновим метод up():

/**
* Define posts table schema
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}

Таблица posts будет содержать id, user_id (ID пользователя, создавшего статью), title, body и колонки с отметками времени.

Теперь откроем файл миграций таблицы favorites и обновим up():

/**
* Define favorites table schema
*/
public function up()
{
Schema::create('favorites', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('post_id')->unsigned();
$table->timestamps();
});
}

Таблица favorites будет сводной. В ней будет две колонки:
user_id, в которой будет храниться ID пользователя, добавившего статью в избранное и post_id, в которой будет ID отмеченной статьи.

Файл миграции таблицы users оставим без изменений.

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

DB_DATABASE=laravue
DB_USERNAME=root
DB_PASSWORD=root

Здесь нужно указать свои параметры базы.

Теперь запустим миграции:

php artisan migrate

Для тестирования работы приложения нам необходимо внести в него какие-либо данные. Для их генерации воспользуемся Laravel Model Factories, они же в свою очередь используют Faker PHP library.

Сгенерируем данные по Пользователям и Статьям. Добавим немного кода в конец файла database/factories/ModelFactory.php:

// database/factories/ModelFactory.php

$factory->define(App\Post::class, function (Faker\Generator $faker) {
// Get a random user
$user = \App\User::inRandomOrder()->first();

// generate fake data for post
return [
'user_id' => $user->id,
'title' => $faker->sentence,
'body' => $faker->text,
];
});

После этого перейдём к созданию классов заполнения базы:

php artisan make:seeder UsersTableSeeder
php artisan make:seeder PostsTableSeeder

Теперь откроем database/seeds/UsersTableSeeder.php и обновим метод run():

// database/seeds/UsersTableSeeder.php

/**
* Run the database seeds to create users.
*
* @return void
*/
public function run()
{
factory(App\User::class, 5)->create();
}

В результате будут созданы 5 различных пользователей. Сделаем то же самое со статьями. Откроем database/seeds/PostsTableSeeder.php и обновим run():

// database/seeds/PostsTableSeeder.php

/**
* Run the database seeds to create posts.
*
* @return void
*/
public function run()
{
factory(App\Post::class, 10)->create();
}

В результате будут созданы 10 различных статей по окончании работы скрипта.

Перед запуском генераторов данных обновим файл database/seeds/DatabaseSeeder.php:

// database/seeds/DatabaseSeeder.php

/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$this->call(UsersTableSeeder::class);
$this->call(PostsTableSeeder::class);
}

И запустим генерацию данных:

php artisan db:seed
Готово.

Авторизация

К счастью, в Laravel уже есть встроенная система авторизации. Нужно только запустить соответствующего мастера:

php artisan make:auth
Будут созданы необходимые роуты и представления для авторизации и нам остаётся только зарегистрироваться в системе.

Роуты

Откроем файл routes/web.php и изменим маршруты:

// routes/web.php

Auth::routes();

Route::get('/', 'PostsController@index');

Route::post('favorite/{post}', 'PostsController@favoritePost');
Route::post('unfavorite/{post}', 'PostsController@unFavoritePost');

Route::get('my_favorites', 'UsersController@myFavorites')->middleware('auth');
После регистрации или авторизации пользователя Laravel перенаправит его на роут /home по-умолчанию. Мы удалили роут /home, созданный при запуске make:auth и поэтому нужно обновить свойство redirectTo в обоих файлах app/Http/Controllers/Auth/LoginController.php и app/Http/Controllers/Auth/RegisterController.php на:

protected $redirectTo = '/';

Отношения Пользователь-Избранное

Пользователь может добавить много статей в избранное, а статья может быть добавлена в избранное многими пользователями, поэтому отношения между этими таблицами будут многие-ко-многим. Для этого откроем модель User и добавим favorites():

// app/User.php

/**
* Get all of favorite posts for the user.
*/
public function favorites()
{
return $this->belongsToMany(Post::class, 'favorites', 'user_id', 'post_id')->withTimeStamps();
}

Laravel считает сводной таблицей post_user, но так как мы её переименовали (в favorites), нам нужно передать дополнительные параметры. Вторым параметром идёт имя сводной таблицы. Третьим — внешний ключ (user_id) модели, с которой создаётся связь (User), а четвёртым — внешний ключ (post_id) модели, к которой мы присоединяем (Post).

Обратите внимание, мы связали withTimeStamps() с belongsToMany(). Это позволит колонкам сводной таблицы с отметками времени (create_at и updated_at) автоматически сработать при добавлении или обновлении строки.

Контроллер статей

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

php artisan make:controller PostsController
Откроем app/Http/Controllers/PostsController.php и добавим в конец такой код:

// app/Http/Controllers/PostsController.php

// remember to use the Post model
use App\Post;

/**
* Display a paginated list of posts.
*
* @return Response
*/
public function index()
{
$posts = Post::paginate(5);

return view('posts.index', compact('posts'));
}

Метод index() вернёт все статьи и разделит их по 5 на странице, а затем обработает файл представления (который мы создадим) со всеми статьями.

Добавим в файл resources/views/layouts/app.blade.php (пониже ) такой код:</p> <p><code>// resources/views/layouts/app.blade.php</p> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" /></code><br /> А затем это перед элементом списка Logout:</p> <p><code>// resources/views/layouts/app.blade.php</p> <li> <a href="{{ url('my_favorites') }}">My Favorites</a> </li> <p></code><br /> Теперь создадим представление index. Создайте папку posts внутри views и в ней файл index.blade.php. Получится resources/views/posts/index.blade.php:</p> <p><code>// resources/views/posts/index.blade.php</p> <p>@extends('layouts.app')</p> <p>@section('content')</p> <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="page-header"> <h3>All Posts</h3> </p></div> <p> @forelse ($posts as $post)</p> <div class="panel panel-default"> <div class="panel-heading"> {{ $post->title }} </div> <div class="panel-body"> {{ $post->body }} </div> </p></div> <p> @empty</p> <p>No post created.</p> <p> @endforelse</p> <p> {{ $posts->links() }} </p></div> </p></div> </div> <p>@endsection</code><br /> Откройте браузер и увидите такую страничку:</p> <p><img title="Как сделать "Избранное" средствами Laravel и Vue.js" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/07/f7e0763e8d.png" class="image-center" alt="Как сделать "Избранное" средствами Laravel и Vue.js" /></p> <p>Вернёмся к PostsController и добавим недостающие методы:</p> <p><code>// app/Http/Controllers/PostsController.php</p> <p>// remember to use<br /> use Illuminate\Support\Facades\Auth;</p> <p>/**<br /> * Favorite a particular post<br /> *<br /> * @param Post $post<br /> * @return Response<br /> */<br /> public function favoritePost(Post $post)<br /> {<br /> Auth::user()->favorites()->attach($post->id);</p> <p> return back();<br /> }</p> <p>/**<br /> * Unfavorite a particular post<br /> *<br /> * @param Post $post<br /> * @return Response<br /> */<br /> public function unFavoritePost(Post $post)<br /> {<br /> Auth::user()->favorites()->detach($post->id);</p> <p> return back();<br /> }</code></p> <h5>Добавляем VueJs</h5> <p>С помощью Vue мы сделаем кнопку Избранного. По нажатию на неё, статья добавиться в избранное (или удалится из избранного) без перезагрузки страницы, используя AJAX. Здесь нам пригодится Axios — основанный на Promise HTTP-клиент для браузера и node.js.</p> <p>Создадим компонент Vue, для этого создайте файл Favorite.vue в папке resources/assets/js/components с таким содержимым:</p> <p><code>// resources/assets/js/components/Favorite.vue</p> <p><template><br /> <span><br /> <a href="#" v-if="isFavorited" @click.prevent="unFavorite(post)"><br /> <i class="fa fa-heart"></i><br /> </a><br /> <a href="#" v-else @click.prevent="favorite(post)"><br /> <i class="fa fa-heart-o"></i><br /> </a><br /> </span><br /> </template></p> <p><script> export default { props: ['post', 'favorited'],</p> <p> data: function() { return { isFavorited: '', } },</p> <p> mounted() { this.isFavorited = this.isFavorite ? true : false; },</p> <p> computed: { isFavorite() { return this.favorited; }, },</p> <p> methods: { favorite(post) { axios.post('/favorite/'+post) .then(response => this.isFavorited = true) .catch(response => console.log(response.data)); },</p> <p> unFavorite(post) { axios.post('/unfavorite/'+post) .then(response => this.isFavorited = false) .catch(response => console.log(response.data)); } } } </script></code><br /> В компоненте Favorite есть два раздела: template и script. В template мы задали разметку, которая будет обработана при вызове компонента. Мы используем обработку с условием, она нам должна вывести нужную кнопку. То есть если isFavorited равно true, кнопка должна быть отмечена как избранная и при нажатии вызвать unFavorite(). В противном случае она выглядит как не помеченная и вызывает при клике метод favorite().</p> <p>В разделе script мы задали свойства компонента — post (тут будет ID статьи) и favorited (true или false в зависимости от того, в каком он состоянии для текущего авторизованного пользователя). Также мы задали переменную isFavorited для условной обработки шаблона.</p> <p>При подключении компонента мы задаём значение isFavorited равным рассчитанному свойству isFavorite. То есть свойство isFavorite вернёт значение свойства favorited, которое может быть true или false.</p> <p>Также мы создали два метода: favorite() и unFavorite(), оба принимающие post как аргумент. С помощью Axios мы делаем POST-запрос к заданным ранее роутам. В favorite(), при успешном POST-запросе, мы задаём isFavorited в true, иначе выводим ошибку в консоль. То же самое и для unFavorite() — только меняем isFavorited на false.</p> <h5>Регистрация компонента</h5> <p>Перед использованием компонента необходимо зарегистрировать его в экземпляре Vue. Открыв resources/assets/js/app.js вы увидите, что Laravel зарегистрировал компонент Example. Заменим его на наш Favorite:</p> <p><code>// resources/assets/js/app.js</p> <p>Vue.component('favorite', require('./components/Favorite.vue'));</code><br /> Теперь скомпилируем и соберём наши стили и скрипты:</p> <p><code>npm run dev</code></p> <p>Теперь можно использовать наш компонент. Откроем resources/views/posts/index.blade.php и добавим в конец (после закрывающего div в panel-body) такой код:</p> <p><code>// resources/views/posts/index.blade.php</p> <p>@if (Auth::check())</p> <div class="panel-footer"> <favorite :post={{ $post->id }}<br /> :favorited={{ $post->favorited() ? 'true' : 'false' }}<br /> ></favorite> </div> <p>@endif</code><br /> Теперь кнопка добавления в избранное будет показана только авторизованным пользователям. А чтобы узнать, добавлена ли статья в избранное, нужен метод favorited(), который мы и создадим. Откройте app/Post.php и добавьте код:</p> <p><code>// app/Post.php</p> <p>// remember to use<br /> use App\Favorite;<br /> use Illuminate\Support\Facades\Auth;</p> <p>/**<br /> * Determine whether a post has been marked as favorite by a user.<br /> *<br /> * @return boolean<br /> */<br /> public function favorited()<br /> {<br /> return (bool) Favorite::where('user_id', Auth::id())<br /> ->where('post_id', $this->id)<br /> ->first();<br /> }</code><br /> Здесь мы получаем и приводим к булеву первую строку запроса, где user_id это идентификатор текущего пользователя, а post_id это идентификатор статьи, с которой мы работаем сейчас.</p> <p>Теперь наше приложение будет выглядеть так:</p> <p><img title="Как сделать "Избранное" средствами Laravel и Vue.js" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/07/4889091451.png" class="image-center" alt="Как сделать "Избранное" средствами Laravel и Vue.js" /></p> <h5>Вывод избранного</h5> <p>Для вывода избранных статей мы создали ранее роут my_favorites, доступный только авторизованным пользователям. </p> <p>Создадим UsersController, обрабатывающий этот роут:</p> <p><code>php artisan make:controller UsersController</code><br /> Теперь откроем app/Http/Controllers/UsersController.php и добавим код:</p> <p><code>// app/Http/Controllers/UsersController.php</p> <p>// remember to use<br /> use Illuminate\Support\Facades\Auth;</p> <p>/**<br /> * Get all favorite posts by user<br /> *<br /> * @return Response<br /> */<br /> public function myFavorites()<br /> {<br /> $myFavorites = Auth::user()->favorites;</p> <p> return view('users.my_favorites', compact('myFavorites'));<br /> }</code><br /> Теперь создадим представление. Создайте папку users в resources/views, а в ней файл my_favorites.blade.php:</p> <p><code>// resources/views/users/my_favorites.blade.php</p> <p>@extends('layouts.app')</p> <p>@section('content')</p> <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="page-header"> <h3>My Favorites</h3> </p></div> <p> @forelse ($myFavorites as $myFavorite)</p> <div class="panel panel-default"> <div class="panel-heading"> {{ $myFavorite->title }} </div> <div class="panel-body"> {{ $myFavorite->body }} </div> <p> @if (Auth::check())</p> <div class="panel-footer"> <favorite :post={{ $myFavorite->id }}<br /> :favorited={{ $myFavorite->favorited() ? 'true' : 'false' }}<br /> ></favorite> </div> <p> @endif </p></div> <p> @empty</p> <p>You have no favorite posts.</p> <p> @endforelse </p></div> </p></div> </div> <p>@endsection</code></p> <p><img title="Как сделать "Избранное" средствами Laravel и Vue.js" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/08/6ee53a54ea.png" class="image-center" alt="Как сделать "Избранное" средствами Laravel и Vue.js" /></p> <p>Готово! </p> <p><a href="https://github.com/ammezie/laravel-vue-favorite" target="_blank">Полный код примера.</a></p> <p>По материалам «Implement a Favoriting Feature Using Laravel and Vue.js» by Chimezie Enyinnaya</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-gravatar"><img title="Как сделать "Избранное" средствами Laravel и Vue.js" alt="Как сделать "Избранное" средствами Laravel и Vue.js" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div><div class="saboxplugin-desc"><div itemprop="description"><p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div></div><div class="clearfix"></div><div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/kak-sdelat-izbrannoe-sredstvami-laravel-i-vue_js.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-360" class="post-360 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/sborka-prilozhenij-nativescript-v-buddybuild.html" rel="bookmark">Сборка приложений NativeScript в BuddyBuild</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/sborka-prilozhenij-nativescript-v-buddybuild.html" title="00:14" rel="bookmark"><time class="updated" datetime="2019-01-04T17:27:40+03:00" itemprop="dateModified">04.01.2019</time><time class="entry-date published" datetime="2017-03-07T00:14:29+03:00" itemprop="datePublished">07.03.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Сборка приложений NativeScript в BuddyBuild" decoding="async" itemprop="image" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/06/3abc06426d.png" class="image-center" alt="Сборка приложений NativeScript в BuddyBuild" /></p> <p>Когда вы разрабатываете мобильное приложение командой, очень часто бывает необходимость в общем сервере сборки. Такой сервер может пересобирать ваше приложение после каждого коммита в исходный код и даже сообщать тестерам и пользователям бета-версии о новых билдах. Есть немало способов создать такой сервер непрерывного развертывания, однако мы предпочитаем отдать этот процесс на сторону и использовать облачные сервисы там, где необходимо.</p> <p>Представляем вам <a href="https://www.buddybuild.com/" target="_blank">BuddyBuild</a> — облачный сервис для сборки приложений под Android и iOS.<br /> <cut><br /> Начать работать с ним очень просто:</p> <ol> <li>Авторизуемся через GitHub, GitLab или другой подобный сервис</li> <li>Выбираем репозиторий с проектом, который нужно будет собирать в BuddyBuild</li> <li>Дальше BuddyBuild скачает исходный код и соберёт приложения для Android и iOS</li> </ol> <p>Готовые приложения можно будет скачать и запустить на локальных устройствах, но можно настроить BuddyBuild на автоматическое распространение сборок среди бета-тестеров (аналогично функционалу Telerik AppManager или Microsoft <a href="https://hockeyapp.net/" target="_blank" class="broken_link">HockeyApp</a>, оба сервиса работают с NativeScript).</p> <p>BuddyBuild пока не работает с проектами NativeScript прямо «из коробки», но у него имеется очень гибкая настраиваемая система сборки. Каждый билд получает свой чистый экземпляр сервера, поэтому у вас есть возможность настроить окружение так, как необходимо.</p> <p>Есть три главных этапа, на которых вы можете запустить командный сценарий для настройки сборочного окружения:</p> <ol> <li>Post Clone (ПостКлонирование — после завершения импорта свежей версии вашего проекта из репозитория)</li> <li>Pre Build (ПредСборка — прямо перед началом сборки приложения)</li> <li>Post Build (ПостСборка — по окончании сборки)</li> </ol> <p>Для приложений NativeScript нам нужно сделать два дополнительных действия до начала сборки:</p> <ol> <li>Установить NativeScript CLI</li> <li>Подготовить приложение к нативной сборке</li> </ol> <p>Мы сделаем это, добавив этап “Post Clone” к нашему проекту, чтобы установился CLI из npm и запустилась команда tns prepare для генерации файлов для XCode или Gradle.</p> <p>В корне проекта создайте новый файл buddybuild_postclone.sh. BuddyBuild найдёт этот файл и выполнит его содержимое в нужное время. Добавьте в этот файл следующее:</p> <p><code>#!/usr/bin/env bash</p> <p># Install NativeScript CLI<br /> echo "******** Install NativeScript CLI *************"<br /> npm install -g nativescript</p> <p># NativeScript Ready<br /> echo "******** NativeScript CLI Ready *************"<br /> tns --version</p> <p># Prepare NativeScript Project<br /> echo "******** Prepare NativeScript iOS Project *************"<br /> tns prepare ios</code><br /> Этот код настраивает окружение для сборки приложения под iOS. Если вам нужна поддержка обеих платформ, воспользуйтесь документацией по написанию скрипта сборки.</p> <p>Комментарии в скрипте (после echo) добавлены для облегчения отладки. Вы получите после сборки полный вывод и в нём можно будет найти возможные ошибки.<br /> <img title="Сборка приложений NativeScript в BuddyBuild" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/07/db8300c5cd.png" class="image-center" alt="Сборка приложений NativeScript в BuddyBuild" /></p> <p>Ещё скрины:</p> <p><img title="Сборка приложений NativeScript в BuddyBuild" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/03/07/0032364b30.png" class="image-center" alt="Сборка приложений NativeScript в BuddyBuild" /></p> <h5>Ложка дёгтя</h5> <p>С небольшими проектами сборка в BuddyBuild очень проста — так, как мы её и описали. Но для проектов, включающих кучу плагинов всё может быть не так радужно — сборка таких приложений иногда просто не срабатывала, возможно, это поправят в скором времени.</p> <p>Но несмотря на это, создание сервера сборки и непрерывного развёртывания никогда ранее не было настолько простым!</p> <p>Возможно, вы пользуетесь своими инструментами для сборки, поделитесь своим опытом в комментариях 🙂</p> <p>По материалам <a href="https://www.nativescript.org/blog/making-nativescript-work-with-buddybuild" target="_blank">«Making NativeScript Work with BuddyBuild»</a> by Todd Anglin</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-gravatar"><img title="Сборка приложений NativeScript в BuddyBuild" alt="Сборка приложений NativeScript в BuddyBuild" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div><div class="saboxplugin-desc"><div itemprop="description"><p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div></div><div class="clearfix"></div><div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/sborka-prilozhenij-nativescript-v-buddybuild.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-350" class="post-350 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/otslezhivaem-mestopolozhenie-ustrojstva-v-mobilnom-prilozhenii-nativescript-angular.html" rel="bookmark">Отслеживаем местоположение устройства в мобильном приложении NativeScript Angular</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/otslezhivaem-mestopolozhenie-ustrojstva-v-mobilnom-prilozhenii-nativescript-angular.html" title="12:11" rel="bookmark"><time class="updated" datetime="2018-10-18T11:44:48+03:00" itemprop="dateModified">18.10.2018</time><time class="entry-date published" datetime="2017-03-04T12:11:05+03:00" itemprop="datePublished">04.03.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Отслеживаем местоположение устройства в мобильном приложении NativeScript Angular" decoding="async" itemprop="image" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/10/feab4c.png" class="image-center" alt="Отслеживаем местоположение устройства в мобильном приложении NativeScript Angular" /></p> <p>Тема работы с GPS при создании мобильных приложений NativeScript часто встречается у начинающих разработчиков. И сегодня мы создадим такое приложение на NativeScript с Angular под Android и iOS.<br /> <cut></p> <blockquote><p>Имейте в виду, что работа с таким приложением в эмуляторе абсолютно бессмысленна, хотя поддержка смены координат и есть в некоторых эмуляторах. Не потому, что плагин не идеальный, а потому, что эмуляторы не приспособлены для работы со сменой координат.<br /> Используйте, по возможности, реальное устройство.</p></blockquote> <h5>Создаём новый проект</h5> <p>Для начала создадим новый проект NativeScript с Angular, TypeScript и HTML.<br /> Выполните в консоли следующие команды:</p> <p><code>tns create GeoProject --ng<br /> cd GeoProject<br /> tns platform add ios<br /> tns platform add android</code><br /> Ключ <strong>—ng</strong> нужен для создания проекта именно с Angular. Также помните, что создать проект для iOS можно только на Mac с установленным Xcode.</p> <p>Теперь установим плагин для геолокации:</p> <p><code>tns plugin add nativescript-geolocation</code><br /> Документация по плагину доступна <a href="https://docs.nativescript.org/hardware/location" target="_blank" class="broken_link">здесь</a>.</p> <h5>Разработка логики на TypeScript и интерфейса на HTML</h5> <p>Мы планируем разработать несложное приложение: на главном экране будет два текстовых поля, для вывода широты и долготы, и три кнопки. По нажатию на первую кнопку будет запрашиваться местоположение устройства и выводиться на экран. Две другие кнопки будут включать и выключать режим отслеживания, при котором текст на экране будет автоматически изменяться при смене координат устройства.</p> <p>Откройте файл app/app.component.ts и добавьте туда следующий код:</p> <p><code>import { Component, NgZone } from "@angular/core";<br /> import * as Geolocation from "nativescript-geolocation";</p> <p>@Component({<br /> selector: "my-app",<br /> templateUrl: "app.component.html",<br /> })<br /> export class AppComponent {</p> <p> public latitude: number;<br /> public longitude: number;<br /> private watchId: number;</p> <p> public constructor(private zone: NgZone) {<br /> this.latitude = 0;<br /> this.longitude = 0;<br /> }</p> <p> private getDeviceLocation(): Promise<any> {<br /> return new Promise((resolve, reject) => {<br /> Geolocation.enableLocationRequest().then(() => {<br /> Geolocation.getCurrentLocation({timeout: 10000}).then(location => {<br /> resolve(location);<br /> }).catch(error => {<br /> reject(error);<br /> });<br /> });<br /> });<br /> }</p> <p> public updateLocation() {<br /> this.getDeviceLocation().then(result => {<br /> this.latitude = result.latitude;<br /> this.longitude = result.longitude;<br /> }, error => {<br /> console.error(error);<br /> });<br /> }</p> <p> public startWatchingLocation() {<br /> this.watchId = Geolocation.watchLocation(location => {<br /> if(location) {<br /> this.zone.run(() => {<br /> this.latitude = location.latitude;<br /> this.longitude = location.longitude;<br /> });<br /> }<br /> }, error => {<br /> console.dump(error);<br /> }, { updateDistance: 1, minimumUpdateTime: 1000 });<br /> }</p> <p> public stopWatchingLocation() {<br /> if(this.watchId) {<br /> Geolocation.clearWatch(this.watchId);<br /> this.watchId = null;<br /> }<br /> }</p> <p>}</code><br /> Рассмотрим подробнее этот код. В методе constructor класса AppComponent инициализируются переменные и подключается служба NgZone из Angular. Нам она нужна для использования слушателей (listeners) и обновления интерфейса с их помощью.</p> <p>В документации NativeScript говорится об использовании функции isEnabled компонента Geolocation для определения работоспособности копонентов геолокации устройства. Я не использую эту функцию, потому что она иногда возвращает странный результат. Вместо этого мы воспользуемся этим:</p> <p><code>private getDeviceLocation(): Promise<any> {<br /> return new Promise((resolve, reject) => {<br /> Geolocation.enableLocationRequest().then(() => {<br /> Geolocation.getCurrentLocation({timeout: 10000}).then(location => {<br /> resolve(location);<br /> }).catch(error => {<br /> reject(error);<br /> });<br /> });<br /> });<br /> }</code><br /> Здесь мы вызываем метод enableLocationRequest, который проверит разрешение на доступ к компонентам и если такое разрешение будет получено, нам будет возвращены текущие координаты. Так как это происходит не сразу, мы и используем promise.</p> <p>Функция getDeviceLocation не привязана к интерфейсу и она частная. А для обновления интерфейса мы будем использовать метод updateLocation. </p> <p>А что нужно сделать, чтобы отслеживать изменения координат?<br /> Для этого и нужны методы startWatchingLocation и stopWatchingLocation. </p> <p><code>public startWatchingLocation() {<br /> this.watchId = Geolocation.watchLocation(location => {<br /> if(location) {<br /> this.zone.run(() => {<br /> this.latitude = location.latitude;<br /> this.longitude = location.longitude;<br /> });<br /> }<br /> }, error => {<br /> console.dump(error);<br /> }, { updateDistance: 1, minimumUpdateTime: 1000 });<br /> }</code><br /> Здесь создаём слушателя и обновляем интерфейс с Angular. Слушатель возвращает id, который мы будем использовать, когда захотим его остановить.</p> <p>Свойство updateDistance показываем, что мы хотим обновлять показания координат только при изменениях более, чем на 1 метр, но свойство minimumUpdateTime указывает, что это обновление происходит только раз в секунду. Поиграйтесь с этими значениями, но помните — они влияют на срок работы от батареи!</p> <p>Перейдём к интерфейсу. Откройте файл app/app.component.html и добавьте туда следующую разметку:</p> <p><code><ActionBar title="{N} Geolocation Example"></ActionBar><br /> <GridLayout><br /> <StackLayout><br /> <Label text="Latitude: {{ latitude }}"></Label><br /> <Label text="Longitude: {{ longitude }}"></Label><br /> <Button class="btn btn-primary btn-active" text="Update" (tap)="updateLocation()"></Button><br /> <Button class="btn btn-primary btn-active" text="Watch Location" (tap)="startWatchingLocation()"></Button><br /> <Button class="btn btn-primary btn-active" text="Stop Watching" (tap)="stopWatchingLocation()"></Button><br /> </StackLayout><br /> </GridLayout></code><br /> Здесь у нас есть верхняя панель и три кнопки для управления геолокацией, которые привязаны к логике в TypeScript коде.</p> <p>В этом простом примере мы рассмотрели только первые шаги по работе с геолокацией в NativeScript. Надеюсь, он вам поможет в работе.</p> <p>По материалам <a href="https://www.thepolyglotdeveloper.com/2017/03/device-geolocation-nativescript-angular-application/" target="_blank">«Track The Device Geolocation In A NativeScript Angular Mobile Application»</a> by Nic Raboy</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-gravatar"><img title="Отслеживаем местоположение устройства в мобильном приложении NativeScript Angular" alt="Отслеживаем местоположение устройства в мобильном приложении NativeScript Angular" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div><div class="saboxplugin-desc"><div itemprop="description"><p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div></div><div class="clearfix"></div><div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/otslezhivaem-mestopolozhenie-ustrojstva-v-mobilnom-prilozhenii-nativescript-angular.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-345" class="post-345 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/rabota-s-radsidedrawer-i-radlistview-v-mobilnom-prilozhenii-nativescript-angular-2.html" rel="bookmark">Работа с RadSideDrawer и RadListView в мобильном приложении NativeScript Angular 2</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/rabota-s-radsidedrawer-i-radlistview-v-mobilnom-prilozhenii-nativescript-angular-2.html" title="01:23" rel="bookmark"><time class="updated" datetime="2019-01-04T17:27:07+03:00" itemprop="dateModified">04.01.2019</time><time class="entry-date published" datetime="2017-03-03T01:23:36+03:00" itemprop="datePublished">03.03.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Работа с RadSideDrawer и RadListView в мобильном приложении NativeScript Angular 2" decoding="async" itemprop="image" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/10/feab4c.png" class="image-center" alt="Работа с RadSideDrawer и RadListView в мобильном приложении NativeScript Angular 2" /></p> <p>Сегодня мы рассмотрим пример включения бокового меню в приложение NativeScript под Android и iOS, построенное на Angular 2. Ну и чтобы <s>два раза не вставать</s> было интереснее, добавим в него мощный компонент построения списков RadListView.<br /> <cut><br /> Компоненты RadSideDrawer и RadListView доступны только в пакете NativeScript UI, который включает как бесплатную, так и платную версии. Всё, что мы рассмотрим в этом материале, доступно в бесплатной версии.</p> <blockquote><p>Для дальнейшей работы убедитесь в том, что у вас установлен NativeScript не ниже версии 2.5 и Android SDK и / или Xcode.<br /> И если кто не в курсе, приложения под iOS можно разрабатывать только имея Mac.</p></blockquote> <p>Создадим чистый проект. Откройте командную строку и введите поочерёдно следующие команды:</p> <p><code>tns create AwesomeProject --ng<br /> cd AwesomeProject<br /> tns platform add ios<br /> tns platform add android</code><br /> Более подробно об установке и настройке NativeScript читайте в <a href="https://docs.nativescript.org/start/quick-setup" target="_blank" class="broken_link">официальной документации</a>.</p> <p>В нашем приложении будут использованы всплывающие сообщения. Установим плагин для этого:</p> <p><code>tns plugin add nativescript-toast</code><br /> Вот как будет выглядеть наше приложение в конце:</p> <p>Приложение не будет особо блистать функционалом: в центре будет список элементов, а боковое меню будет выдвигаться по свайпу. В нём будут несколько элементов, по нажатию на которые появится всплывающее сообщение. А в главном списке элементов будут реализованы функции горизонтального свайпа и «потянуть для обновления».</p> <p>Теперь установим компоненты RadSideDrawer и RadListView. Выполните команду в терминале:</p> <p><code>tns plugin add nativescript-telerik-ui</code><br /> Более подробно об этом компоненте <a href="https://www.npmjs.com/package/nativescript-telerik-ui" target="_blank">здесь</a>.</p> <p>После установки необходимо включить компоненты в приложение. Откроем файл app/app.module.ts и добавим в него такие строки:</p> <p><code>import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";<br /> import { NativeScriptModule } from "nativescript-angular/nativescript.module";<br /> import { SIDEDRAWER_DIRECTIVES } from "nativescript-telerik-ui/sidedrawer/angular";<br /> import { LISTVIEW_DIRECTIVES } from 'nativescript-telerik-ui/listview/angular';</p> <p>import { AppComponent } from "./app.component";</p> <p>@NgModule({<br /> declarations: [<br /> AppComponent,<br /> LISTVIEW_DIRECTIVES,<br /> SIDEDRAWER_DIRECTIVES<br /> ],<br /> bootstrap: [AppComponent],<br /> imports: [NativeScriptModule],<br /> schemas: [NO_ERRORS_SCHEMA]<br /> })<br /> export class AppModule { }</code><br /> Обратите внимание, мы импортировали две директивы NativeScript UI и добавили их в массив declarations блока @NgModule. Такие же действия потребуются в случае установки других компонентов пакета NativeScript UI.</p> <h5>Разработка бокового меню</h5> <p>Откроем файл app/app.component.ts и добавим в него такой код:</p> <p><code>import { Component, ViewChild, OnInit } from "@angular/core";<br /> import { ListViewEventData, RadListView } from "nativescript-telerik-ui/listview";<br /> import { RadSideDrawerComponent, SideDrawerType } from "nativescript-telerik-ui/sidedrawer/angular";<br /> import { View } from 'ui/core/view';<br /> import * as Utils from "utils/utils";<br /> import * as FrameModule from "ui/frame";<br /> import * as Toast from 'nativescript-toast';</p> <p>@Component({<br /> selector: "my-app",<br /> templateUrl: "app.component.html",<br /> })<br /> export class AppComponent implements OnInit {</p> <p> public emails: Array<string>;<br /> public selected: number;<br /> private drawer: SideDrawerType;</p> <p> @ViewChild(RadSideDrawerComponent)<br /> public drawerComponent: RadSideDrawerComponent;</p> <p> public constructor() {<br /> this.emails = [<br /> "Welcome to The Polyglot Developer Newsletter!",<br /> "Raspberry Pi Zero's Available!",<br /> ];<br /> }</p> <p> public ngOnInit() {<br /> this.drawer = this.drawerComponent.sideDrawer;<br /> }</p> <p> public onPullToRefreshInitiated(args: any) { }</p> <p> public onSwipeCellStarted(args: ListViewEventData) { }</p> <p> public onDelete() { }</p> <p> public onArchive() { }</p> <p> public onMenuTapped(value: any) {<br /> Toast.makeText(value + " menu item selected").show();<br /> this.drawer.closeDrawer();<br /> }</p> <p>}</code><br /> Здесь мы сделали следующее: в классе AppComponent ввели несколько общих и частных переменных. В переменной emails будет храниться список строк, которые мы выведем в RadListView. Переменная selected сообщит нам, какой элемент выбран в RadListView, чтобы мы могли запустить соответствующий метод. Переменная drawer будет ссылаться на компонент RadSideDrawer и мы сможем выполнять такие действия как открытие/закрытие меню. </p> <p>По причине простоты реализации, мы инициируем строки, представляющие электронные адреса, в методе constructor. После вызова constructor будет вызван ngOnInit, где мы зададим @ViewChild равный переменной drawer.</p> <p>Мы ещё не касались интерфейса, но при клике на элемент в боковом меню нам нужно будет выполнить метод. Метод onMenuTapped возьмёт любое значение и выведет во всплывающем сообщении. После показа сообщения, боковое меню закроется.<br /> А теперь давайте построим интерфейс!</p> <p>Откроем файл app/app.component.html и включим в него следующую разметку:</p> <p><code><ActionBar title="{N} UI Example"></ActionBar><br /> <RadSideDrawer><br /> <StackLayout tkDrawerContent class="sideStackLayout"><br /> <StackLayout class="sideTitleStackLayout"><br /> <Label text="Menu"></Label><br /> </StackLayout><br /> <ScrollView><br /> <StackLayout class="sideStackLayout"><br /> <Label text="Primary" class="sideLabel" (tap)="onMenuTapped('Primary')"></Label><br /> <Label text="Social" class="sideLabel" (tap)="onMenuTapped('Social')"></Label><br /> <Label text="Promotions" class="sideLabel" (tap)="onMenuTapped('Promotions')"></Label><br /> <Label text="Labels" class="sideLabel" (tap)="onMenuTapped('Labels')"></Label><br /> <Label text="Important" class="sideLabel" (tap)="onMenuTapped('Important')"></Label><br /> <Label text="Starred" class="sideLabel" (tap)="onMenuTapped('Starred')"></Label><br /> <Label text="Sent Mail" class="sideLabel" (tap)="onMenuTapped('Sent Mail')"></Label><br /> <Label text="Drafts" class="sideLabel" (tap)="onMenuTapped('Drafts')"></Label><br /> </StackLayout><br /> </ScrollView><br /> </StackLayout><br /> <StackLayout tkMainContent class="mainContent"><br /> <GridLayout></GridLayout><br /> </StackLayout><br /> </RadSideDrawer></code><br /> Не касаясь пока тегов class, у нас здесь есть <ActionBar> и <RadSideDrawer>. У <RadSideDrawer> есть два очень важных компонента, заданных директивами tkDrawerContent и tkMainContent. Так мы отделяем часть меню от части главного окна приложения.</p> <p>В tkDrawerContent (это наше боковое меню) у нас есть список компонентов <Label>. При клике на них будет вызван метод onMenuTapped, показано сообщение и меню закроется.</p> <p>Довольно просто, не так ли?</p> <p>Перейдём к главному представлению в секции tkMainContent.</p> <h5>Добавление многофункционального списка</h5> <p>Для начала вернёмся в логику на TypeScript, в которой мы оставили недописанными пару методов.<br /> Откроем файл app/app.component.ts и добавим код:</p> <p><code>public onPullToRefreshInitiated(args: any) {<br /> var radListView = args.object;<br /> setTimeout(() => {<br /> this.emails.push("NativeScript for the Angular Developer");<br /> radListView.notifyPullToRefreshFinished();<br /> }, 500);<br /> }</code><br /> Здесь в методе onPullToRefreshInitiated мы получаем ссылку на наш RadListView и настраиваем таймаут в полсекунды. Наши данные статичны и в таймауте нет необходимости, но они легко могут приходить и из базы данных, поэтому при срабатывании таймера мы увидим что мы хотели реализовать.</p> <p>Реализовать свайпы в RadListView можно несколькими способами. Предположим, что мы хотим добавить кнопки действий. То есть, сдвинув элемент, мы увидим кнопки, на которые можно нажать.</p> <p><code>public onSwipeCellStarted(args: ListViewEventData) {<br /> var swipeLimits = args.data.swipeLimits;<br /> swipeLimits.threshold = 60 * Utils.layout.getDisplayDensity();<br /> swipeLimits.left = 120 * Utils.layout.getDisplayDensity();<br /> swipeLimits.right = 120 * Utils.layout.getDisplayDensity();<br /> this.selected = args.itemIndex;<br /> }</code><br /> Здесь мы задали расстояние, на которое можно сдвинуть элемент в любую сторону, относительно размера экрана. </p> <p>И у нас элемент списка не имеет жёсткой привязки к кнопкам в свайпе, поэтому необходимо получить информацию о том, какой элемент был сдвинут. Это понадобится для реализации клика на кнопки свайпа.</p> <p><code>public onDelete() {<br /> let radListView = <RadListView> FrameModule.topmost().currentPage.getViewById("radlistview");<br /> Toast.makeText("Deleted").show();<br /> this.emails.splice(this.selected, 1);<br /> radListView.notifySwipeToExecuteFinished();<br /> }</code><br /> В методе onDelete мы получаем RadListView из интерфейса. Мы знаем позицию элемента, с которым работаем, поэтому можем её использовать для удаления элемента и возвратить свайп обратно. То же самое применимо к методу onArchive:</p> <p><code>public onArchive() {<br /> let radListView = <RadListView> FrameModule.topmost().currentPage.getViewById("radlistview");<br /> Toast.makeText("Archived").show();<br /> this.emails.splice(this.selected, 1);<br /> radListView.notifySwipeToExecuteFinished();<br /> }</code></p> <p>Перейдём снова к интерфейсу. Откроем app/app.component.html и посмотрим на полный пример:</p> <p><code><ActionBar title="{N} UI Example"></ActionBar><br /> <RadSideDrawer tkExampleTitle tkToggleNavButton><br /> <StackLayout tkDrawerContent class="sideStackLayout"><br /> <StackLayout class="sideTitleStackLayout"><br /> <Label text="Menu"></Label><br /> </StackLayout><br /> <ScrollView><br /> <StackLayout class="sideStackLayout"><br /> <Label text="Primary" class="sideLabel" (tap)="onMenuTapped('Primary')"></Label><br /> <Label text="Social" class="sideLabel" (tap)="onMenuTapped('Social')"></Label><br /> <Label text="Promotions" class="sideLabel" (tap)="onMenuTapped('Promotions')"></Label><br /> <Label text="Labels" class="sideLabel" (tap)="onMenuTapped('Labels')"></Label><br /> <Label text="Important" class="sideLabel" (tap)="onMenuTapped('Important')"></Label><br /> <Label text="Starred" class="sideLabel" (tap)="onMenuTapped('Starred')"></Label><br /> <Label text="Sent Mail" class="sideLabel" (tap)="onMenuTapped('Sent Mail')"></Label><br /> <Label text="Drafts" class="sideLabel" (tap)="onMenuTapped('Drafts')"></Label><br /> </StackLayout><br /> </ScrollView><br /> </StackLayout><br /> <StackLayout tkMainContent class="mainContent"><br /> <GridLayout><br /> <RadListView id="radlistview" [items]="emails" itemSwipe="true" pullToRefresh="true" (pullToRefreshInitiated)="onPullToRefreshInitiated($event)" (itemSwipeProgressStarted)="onSwipeCellStarted($event)"><br /> <Template tkListItemTemplate let-email="item"><br /> <StackLayout class="listItemStackLayout"><br /> <Label text="{{ email }}"></Label><br /> </StackLayout><br /> </Template><br /> <GridLayout *tkListItemSwipeTemplate columns="auto, *, auto" class="listItemSwipeGridLayout"><br /> <StackLayout class="archiveViewStackLayout" col="0" (tap)="onArchive()"><br /> <Label text="ARCHIVE" verticalAlignment="center" horizontalAlignment="center"></Label><br /> </StackLayout><br /> <StackLayout class="deleteViewStackLayout" col="2" (tap)="onDelete()"><br /> <Label text="DELETE" verticalAlignment="center" horizontalAlignment="center"></Label><br /> </StackLayout><br /> </GridLayout><br /> </RadListView><br /> </GridLayout><br /> </StackLayout><br /> </RadSideDrawer></code></p> <p>Внутри RadListView у нас есть атрибут id и методы onDelete и onArchive могут получить ссылку на нужный компонент. Список строится из нашего массива в переменной emails и здесь же перечислены привязки к методам логики в коде TypeScript.</p> <p>Строки в пременной списка представлены компонентом <Label> в шаблоне списка. </p> <p><code><GridLayout *tkListItemSwipeTemplate columns="auto, *, auto" class="listItemSwipeGridLayout"><br /> <StackLayout class="archiveViewStackLayout" col="0" (tap)="onArchive()"><br /> <Label text="ARCHIVE" verticalAlignment="center" horizontalAlignment="center"></Label><br /> </StackLayout><br /> <StackLayout class="deleteViewStackLayout" col="2" (tap)="onDelete()"><br /> <Label text="DELETE" verticalAlignment="center" horizontalAlignment="center"></Label><br /> </StackLayout><br /> </GridLayout></code><br /> tkListItemSwipeTemplate показывает, что это представление будет показано при свайпе. Это трёхколоночный макет сетки с кнопками по углам.</p> <p>А теперь вернёмся к тегам class, упомянутым нами ранее, и рассмотрим стилизацию приложения.</p> <h5>Улучшения интерфейса с помощью CSS</h5> <p>Для простоты мы будем использовать только глобальные стили. Откройте файл app/app.css и добавьте следующее:</p> <p><code>.sideStackLayout {<br /> background-color: #555555;<br /> color: #FFFFFF;<br /> }</p> <p>.sideTitleStackLayout {<br /> padding: 16;<br /> font-weight: bold;<br /> background-color: #333333;<br /> }</p> <p>.sideLabel {<br /> padding: 16;<br /> }</p> <p>.listItemStackLayout {<br /> padding: 16;<br /> background-color: #FFFFFF;<br /> }</p> <p>.archiveViewStackLayout {<br /> padding: 16;<br /> background-color: #387EF5;<br /> color: #FFFFFF;<br /> }</p> <p>.deleteViewStackLayout {<br /> padding: 16;<br /> background-color: #EF473A;<br /> color: #FFFFFF;<br /> }</p> <p>@import 'nativescript-theme-core/css/core.light.css';</code><br /> Здесь нет ничего, кроме цвета фона и отступов, но наш интерфейс заметно преобразился.</p> <p>Выполните команду tns run [platform] и будет собрано и запущено приложение для выбранной платформы.</p> <p>По материалам <a href="https://www.thepolyglotdeveloper.com/2017/03/use-side-drawer-feature-rich-list-view-nativescript-angular-app/" target="_blank">«Use A Side Drawer And Feature Rich List View In A NativeScript Angular App»</a> by Nic Raboy</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-gravatar"><img title="Работа с RadSideDrawer и RadListView в мобильном приложении NativeScript Angular 2" alt="Работа с RadSideDrawer и RadListView в мобильном приложении NativeScript Angular 2" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div><div class="saboxplugin-desc"><div itemprop="description"><p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div></div><div class="clearfix"></div><div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/rabota-s-radsidedrawer-i-radlistview-v-mobilnom-prilozhenii-nativescript-angular-2.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-332" class="post-332 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/kak-rabotat-s-modalnymi-dialogami-v-mobilnom-prilozhenii-nativescript-angular-2.html" rel="bookmark">Как работать с модальными диалогами в мобильном приложении NativeScript Angular 2</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/kak-rabotat-s-modalnymi-dialogami-v-mobilnom-prilozhenii-nativescript-angular-2.html" title="12:43" rel="bookmark"><time class="updated" datetime="2018-12-10T13:18:20+03:00" itemprop="dateModified">10.12.2018</time><time class="entry-date published" datetime="2017-03-01T12:43:04+03:00" itemprop="datePublished">01.03.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Как работать с модальными диалогами в мобильном приложении NativeScript Angular 2" decoding="async" itemprop="image" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/10/feab4c.png" class="image-center" alt="Как работать с модальными диалогами в мобильном приложении NativeScript Angular 2" /></p> <p>Сегодня мы поговорим о модальных окнах в NativeScript. Многие не любят их по причине непонимания механизма передачи данных между окнами, и совершенно напрасно, это очень просто!<br /> <cut><br /> Как обычно, начнём с создания нового приложения (перед этим убедитесь, что у вас установлен NativeScript и Android SDK или Xcode в MacOS):</p> <p><code>tns create ModalProject --ng<br /> cd ModalProject<br /> tns platform add android<br /> tns platform add ios</code><br /> В конце у нас должно получиться такое приложение:</p> <p>У нас будет кнопка, по нажатию на которую появится список действий. При выборе действия, экран возвращается на первую страницу.</p> <p>Если у вас Mac или Linux, выполните команды:</p> <p><code>touch app/app.modal.ts<br /> touch app/app.modal.html</code><br /> Если у вас Windows, то нужно вручную создать эти файлы.</p> <p>Откройте файл app/app.modal.ts и вставьте в него следующий код:</p> <p><code>import { Component } from "@angular/core";<br /> import { ModalDialogParams } from "nativescript-angular/directives/dialogs";</p> <p>@Component({<br /> selector: "my-modal",<br /> templateUrl: "app.modal.html",<br /> })<br /> export class ModalComponent {</p> <p> public frameworks: Array<string>;</p> <p> public constructor(private params: ModalDialogParams) {<br /> this.frameworks = [<br /> "NativeScript",<br /> "Xamarin",<br /> "Onsen UI",<br /> "Ionic Framework",<br /> "React Native"<br /> ];<br /> }</p> <p> public close(res: string) {<br /> this.params.closeCallback(res);<br /> }</p> <p>}</code><br /> В классе ModalComponent есть публичный массив, который будет привязан к интерфейсу модального окна. При закрытии модального окна у нас есть возможность передать какие-то данные родительскому окну. Это делается с помощью метода closeCallback службы ModalDialogParams.</p> <p>Перейдём к интерфейсу. Откройте файл app/app.modal.html и вставьте следующую разметку:</p> <p><code><GridLayout backgroundColor="white"><br /> <ListView [items]="frameworks" class="list-group"><br /> <Template let-framework="item"><br /> <StackLayout class="list-group-item" (tap)="close(framework)"><br /> <Label text="{{ framework }}" class="label"></Label><br /> </StackLayout><br /> </Template><br /> </ListView><br /> </GridLayout></code><br /> У нас есть список, каждый элемент которого загружается из массива frameworks. При нажатии на элемент, вызывается метод close с передачей методу выбранного элемента. </p> <p>Для работы модального окна нужно его включить в блок @NgModule файла app/app.module.ts. Откроем его и вставим следующий код:</p> <p><code>import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";<br /> import { NativeScriptModule } from "nativescript-angular/platform";<br /> import { ModalDialogService } from "nativescript-angular/modal-dialog";</p> <p>import { AppComponent } from "./app.component";<br /> import { ModalComponent } from "./app.modal";</p> <p>@NgModule({<br /> declarations: [AppComponent, ModalComponent],<br /> entryComponents: [ModalComponent],<br /> bootstrap: [AppComponent],<br /> imports: [<br /> NativeScriptModule<br /> ],<br /> providers: [ModalDialogService],<br /> schemas: [NO_ERRORS_SCHEMA]<br /> })<br /> export class AppModule { }</code><br /> Здесь мы импортировали ModalDialogService и включили его в массивы declarations и entryComponents блока @NgModule.</p> <p>Теперь мы можем вызывать модальное окно из приложения.</p> <p>Откроем файл app/app.component.ts и включим следующий код:</p> <p><code>import { Component, ViewContainerRef } from "@angular/core";<br /> import { ModalDialogService } from "nativescript-angular/directives/dialogs";<br /> import { ModalComponent } from "./app.modal";</p> <p>@Component({<br /> selector: "my-app",<br /> templateUrl: "app.component.html",<br /> })<br /> export class AppComponent {</p> <p> public constructor(private modal: ModalDialogService, private vcRef: ViewContainerRef) { }</p> <p> public showModal() {<br /> let options = {<br /> context: {},<br /> fullscreen: true,<br /> viewContainerRef: this.vcRef<br /> };<br /> this.modal.showModal(ModalComponent, options).then(res => {<br /> console.log(res);<br /> });<br /> }</p> <p>}</code><br /> Здесь в методе constructor класса AppComponent у нас находятся методы для работы с модальными окнами. И для открытия модального окна нужно будет вызвать метод showModal.<br /> А при закрытии модального окна, переданные из него данные будут выведены в консоль.</p> <p>Снова откроем файл app/app.component.html и добавим:</p> <p><code><ActionBar title="{N} Modal Example"></ActionBar><br /> <StackLayout horizontalAlignment="center" verticalAlignment="center"><br /> <Button text="Show Modal" (tap)="showModal()" class="btn btn-primary"></Button><br /> </StackLayout></code><br /> Мы добавили навигационную панель и кнопку по центру окна. При нажатии на кнопку будет вызван метод showModal.</p> <p>Видео-версия этой статьи (на английском):<br /> <iframe loading="lazy" width="576" height="486" src="https://www.youtube.com/embed/KmROQAjiekk?ecver=1" frameborder="0" allowfullscreen></iframe></p> <p>Автор: Nic Raboy, <a href="https://www.thepolyglotdeveloper.com/2017/01/using-modal-dialogs-nativescript-angular-mobile-application/" target="_blank">«Using Modal Dialogs In A NativeScript Angular Mobile Application»</a></p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-gravatar"><img title="Как работать с модальными диалогами в мобильном приложении NativeScript Angular 2" alt="Как работать с модальными диалогами в мобильном приложении NativeScript Angular 2" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div><div class="saboxplugin-desc"><div itemprop="description"><p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div></div><div class="clearfix"></div><div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/kak-rabotat-s-modalnymi-dialogami-v-mobilnom-prilozhenii-nativescript-angular-2.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-326" class="post-326 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/sozdanie-odnostranichnogo-veb-prilozhenija-na-go-echo-i-vue.html" rel="bookmark">Создание одностраничного веб-приложения на Go, Echo и Vue</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/sozdanie-odnostranichnogo-veb-prilozhenija-na-go-echo-i-vue.html" title="00:03" rel="bookmark"><time class="updated" datetime="2019-01-04T17:27:36+03:00" itemprop="dateModified">04.01.2019</time><time class="entry-date published" datetime="2017-02-28T00:03:36+03:00" itemprop="datePublished">28.02.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" itemprop="image" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/298348afc3.jpg" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /></p> <p>Сегодня мы создадим несложное приложение для ведения списка задач. В нём можно будет указывать наименование задачи, выводить созданные задачи на экран и удалять задачи.</p> <p>Бэкенд приложения будет написан на языке Go. Для лучшего понимания материала необходимы хотя бы минимальные знания синтаксиса и установленный Go.<br /> <cut><br /> Чтобы ускорить создание приложения, мы возьмём микро-фреймворк <a href="https://echo.labstack.com/" target="_blank">Echo</a>. А задачи мы будем хранить в базе SQLite.</p> <p>Фронтенд мы создадим на HTML5 с популярным Javascript фреймворком VueJS.</p> <h5>Роуты и база данных</h5> <p>Вначале установим недостающие библиотеки:</p> <p><code>$ go get github.com/labstack/echo<br /> $ go get github.com/mattn/go-sqlite3</code><br /> И создадим директорию для нашего проекта:</p> <p><code>$ cd $GOPATH/src<br /> $ mkdir go-echo-vue && cd go-echo-vue</code><br /> Начнём с создания роутов. Создайте файл <strong>todo.go</strong> в корне созданной папки с таким содержимым:</p> <p><code>// todo.go<br /> package main</p> <p>import (<br /> "github.com/labstack/echo"<br /> "github.com/labstack/echo/engine/standard"<br /> )</p> <p>func main() {<br /> // Create a new instance of Echo<br /> e := echo.New()</p> <p> e.GET("/tasks", func(c echo.Context) error { return c.JSON(200, "GET Tasks") })<br /> e.PUT("/tasks", func(c echo.Context) error { return c.JSON(200, "PUT Tasks") })<br /> e.DELETE("/tasks/:id", func(c echo.Context) error { return c.JSON(200, "DELETE Task "+c.Param("id")) })</p> <p> // Start as a web server<br /> e.Run(standard.New(":8000"))<br /> }<br /> </code><br /> Здесь мы импортировали фреймворк Echo и создали обязательный метод <strong>main()</strong>. Затем в нём создали экземпляр Echo и задали несколько роутов. При создании роута ему передаётся шаблон запроса первым параметром и функция-обработчик вторым.<br /> Наши роуты пока могут только выдавать статический текст, доработаем их позже. А в конце мы запускаем наше приложение по адресу http://localhost:8000 используя встроенный в Go HTTP-сервер.</p> <p>Для тестирования работы роутов сначала скомпилируем приложение и запустим его, а потом воспользуемся расширением Chrome под названием <a href="https://www.getpostman.com/" target="_blank">Postman</a>.</p> <p><code>$ go build todo.go<br /> $ ./todo</code><br /> После запуска приложения откроем Postman и подключим его к адресу localhost:8000. Нужно протестировать роут «/tasks» с помощью запросов GET, PUT и DELETE, обработчики которых мы создали ранее.<br /> Если всё работает правильно, мы увидим следующую картину:<br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/92ce5c1ed8.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/f26e879ab0.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/a7e3a92121.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /></p> <p>Теперь перейдём к созданию базы данных. Назовём файл «storage.db» и если его нет, драйвер создаст его для нас.<br /> После создания базы данных необходимо будет запустить миграции.</p> <p>Доработаем наш файл todo.go:</p> <p><code>// todo.go<br /> package main<br /> import (<br /> "database/sql"</p> <p> "github.com/labstack/echo"<br /> "github.com/labstack/echo/engine/standard"<br /> _ "github.com/mattn/go-sqlite3"<br /> )</code><br /> И доработаем метод main() :</p> <p><code>// todo.go<br /> func main() {</p> <p> db := initDB("storage.db")<br /> migrate(db)</code><br /> Добавим метод работы с базой данных:</p> <p><code>// todo.go<br /> func initDB(filepath string) *sql.DB {<br /> //откроем файл или создадим его<br /> db, err := sql.Open("sqlite3", filepath)</p> <p> // проверяем ошибки и выходим при их наличии<br /> if err != nil {<br /> panic(err)<br /> }</p> <p> // если ошибок нет, но не можем подключиться к базе данных,<br /> // то так же выходим<br /> if db == nil {<br /> panic("db nil")<br /> }<br /> return db<br /> }</p> <p>func migrate(db *sql.DB) {<br /> sql := `<br /> CREATE TABLE IF NOT EXISTS tasks(<br /> id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,<br /> name VARCHAR NOT NULL<br /> );<br /> `</p> <p> _, err := db.Exec(sql)<br /> // выходим, если будут ошибки с SQL запросом выше<br /> if err != nil {<br /> panic(err)<br /> }<br /> }</code><br /> Здесь мы просто создаём таблицу для наших задач, если её ещё нет и завершаем приложение при любой ошибке.</p> <p>Протестируем ещё раз:</p> <p><code>$ go build todo.go<br /> $ ./todo</code><br /> Теперь откроем терминал, перейдём в папку проекта и запустим следующую команду для проверки созданного файла БД:</p> <p><code>$ sqlite3 storage.db</code><br /> Если у вас не получилось запустить команду sqlite, возможно требуется установить её с <a href="https://www.sqlite.org/download.html" target="_blank">официального сайта</a> или через менеджер пакетов вашей ОС.</p> <p>Если команда запустилась, то введите команду <strong>«.tables»</strong>. Вы должны увидеть примерно следующее:<br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/e8bccf48fe.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> Чтобы выйти, введите «.quit».</p> <h5>Обработчики</h5> <p>Мы создали обработчики запросов, теперь нам нужно доработать их.<br /> Откроем файл <strong>todo.go</strong> и вставим в блок импортов файл с обработчиками, который создадим позже:</p> <p><code>package main<br /> import (<br /> "database/sql"<br /> "go-echo-vue/handlers"</p> <p> "github.com/labstack/echo"<br /> "github.com/labstack/echo/engine/standard"<br /> _ "github.com/mattn/go-sqlite3"<br /> )</code><br /> Тут же доработаем вызовы обработчиков:</p> <p><code>// todo.go<br /> e := echo.New()</p> <p> e.File("/", "public/index.html")<br /> e.GET("/tasks", handlers.GetTasks(db))<br /> e.PUT("/tasks", handlers.PutTask(db))<br /> e.DELETE("/tasks/:id", handlers.DeleteTask(db))</p> <p> e.Run(standard.New(":8000"))<br /> }</code><br /> Здесь мы добавили к существующим роутам дополнительный. В этом html-файле мы будем хранить код VueJS.</p> <p>Теперь создадим директорию ‘handlers’, а в ней файл «tasks.go»:</p> <p><code>// handlers/tasks.go<br /> package handlers</p> <p>import (<br /> "database/sql"<br /> "net/http"<br /> "strconv"</p> <p> "github.com/labstack/echo"<br /> )</code><br /> А в строке ниже будет небольшой трюк, который позволит возвращать произвольный JSON. Это просто map со ключами типа string и любым типом значения.</p> <p><code>// handlers/tasks.go<br /> type H map[string]interface{}</code></p> <p>Для того, чтобы просто проверить работу обработчиков, сделаем вывод левых данных, а не из базы данных:</p> <p><code>// handlers/tasks.go</p> <p>// конечная точка GetTasks<br /> func GetTasks(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> return c.JSON(http.StatusOK, "tasks")<br /> }<br /> }</p> <p>// конечная точка PutTask<br /> func PutTask(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> return c.JSON(http.StatusCreated, H{<br /> "created": 123,<br /> }<br /> }</p> <p>// конечная точка DeleteTask<br /> func DeleteTask(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> id, _ := strconv.Atoi(c.Param("id"))<br /> return c.JSON(http.StatusOK, H{<br /> "deleted": id,<br /> })<br /> }<br /> }</code></p> <p>Библиотека http в Go позволяет работать со статусами HTTP и мы можем ответить http.StatusCreated для запроса PUT.<br /> Функция «DeleteTask» принимает параметром id задачи, а мы с помощью пакета strconv и метода Atoi (alpha в число) проверяем, что id это число. Так мы более безопасно сможем передать запрос к базе данных позже.</p> <p>Пересоберём приложение и заново запустим Postman:</p> <p><img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/4b02ced590.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/c259deaab4.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/541afda119.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /></p> <h5>Модель</h5> <p>Нам осталось подключить приложение к базе данных. Вместо того, чтобы делать прямые вызовы из обработчиков, мы сохраним код простым, превратив логику базы данных в модель.</p> <p>Но сначала включим ссылки на нашу модель в созданный файл обработчиков.<br /> Включим файл моделей в блок импорта:</p> <p><code>// handlers/tasks.go<br /> package handlers</p> <p>import (<br /> "database/sql"<br /> "net/http"<br /> "strconv"</p> <p> "go-echo-vue/models"</p> <p> "github.com/labstack/echo"<br /> )</code><br /> Затем добавим вызовы к модели в метод обработчика:</p> <p><code>// handlers/tasks.go</p> <p>// конечная точка GetTasks<br /> func GetTasks(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> // получаем задачи из модели<br /> return c.JSON(http.StatusOK, models.GetTasks(db))<br /> }<br /> }</p> <p>// конечная точка PutTask<br /> func PutTask(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> // создаём новую задачу<br /> var task models.Task<br /> // привязываем пришедший JSON в новую задачу<br /> c.Bind(&task)<br /> // добавим задачу с помощью модели<br /> id, err := models.PutTask(db, task.Name)<br /> // вернём ответ JSON при успехе<br /> if err == nil {<br /> return c.JSON(http.StatusCreated, H{<br /> "created": id,<br /> })<br /> // обработка ошибок<br /> } else {<br /> return err<br /> }<br /> }<br /> }</p> <p>// конечная точка DeleteTask<br /> func DeleteTask(db *sql.DB) echo.HandlerFunc {<br /> return func(c echo.Context) error {<br /> id, _ := strconv.Atoi(c.Param("id"))<br /> // используем модель для удаления задачи<br /> _, err := models.DeleteTask(db, id)<br /> // вернём ответ JSON при успехе<br /> if err == nil {<br /> return c.JSON(http.StatusOK, H{<br /> "deleted": id,<br /> })<br /> // обработка ошибок<br /> } else {<br /> return err<br /> }<br /> }<br /> }</code></p> <p>А теперь создадим модель. Создайте каталог «models», а в нём файл «tasks.go».</p> <p><code>// models/tasks.go<br /> package models</p> <p>import (<br /> "database/sql"</p> <p> _ "github.com/mattn/go-sqlite3"<br /> )</p> <p>// Task это структура с данными задачи<br /> type Task struct {<br /> ID int `json:"id"`<br /> Name string `json:"name"`<br /> }</p> <p>// TaskCollection это список задач<br /> type TaskCollection struct {<br /> Tasks []Task `json:"items"`<br /> }<br /> </code><br /> Мы создали тип «Task» — структуру с двумя полями, ID и Name. Go позволяет добавлять метаданные с помощью обратных кавычек. Мы указали, что хотим видеть поля в виде JSON. И это позволяет функции «c.Bind» (из обработчиков) знать, какие привязки данных нужны при создании новой задачи.<br /> Тип «TaskCollection» это просто коллекция наших задач. Мы используем её при возврате списка всех задач из базы данных.</p> <p><code>// models/tasks.go</p> <p>func GetTasks(db *sql.DB) TaskCollection {<br /> sql := "SELECT * FROM tasks"<br /> rows, err := db.Query(sql)<br /> // выходим, если SQL не сработал по каким-то причинам<br /> if err != nil {<br /> panic(err)<br /> }<br /> // убедимся, что всё закроется при выходе из программы<br /> defer rows.Close()</p> <p> result := TaskCollection{}<br /> for rows.Next() {<br /> task := Task{}<br /> err2 := rows.Scan(&task.ID, &task.Name)<br /> // выход при ошибке<br /> if err2 != nil {<br /> panic(err2)<br /> }<br /> result.Tasks = append(result.Tasks, task)<br /> }<br /> return result<br /> }</code><br /> Метод GetTasks выбирает все задачи из базы данных, добавляет их в новую коллекцию задач, и возвращает их.</p> <p><code>// models/tasks.go</p> <p>func PutTask(db *sql.DB, name string) (int64, error) {<br /> sql := "INSERT INTO tasks(name) VALUES(?)"</p> <p> // выполним SQL запрос<br /> stmt, err := db.Prepare(sql)<br /> // выход при ошибке<br /> if err != nil {<br /> panic(err)<br /> }<br /> // убедимся, что всё закроется при выходе из программы<br /> defer stmt.Close()</p> <p> // заменим символ '?' в запросе на 'name'<br /> result, err2 := stmt.Exec(name)<br /> // выход при ошибке<br /> if err2 != nil {<br /> panic(err2)<br /> }</p> <p> return result.LastInsertId()<br /> }</code><br /> Метод PutTask вставляет новую задачу в базу данных и возвращает её id при успехе или panic при неудаче.</p> <p><code>// models/tasks.go</p> <p>func DeleteTask(db *sql.DB, id int) (int64, error) {<br /> sql := "DELETE FROM tasks WHERE id = ?"</p> <p> // выполним SQL запрос<br /> stmt, err := db.Prepare(sql)<br /> // выход при ошибке<br /> if err != nil {<br /> panic(err)<br /> }</p> <p> // заменим символ '?' в запросе на 'id'<br /> result, err2 := stmt.Exec(id)<br /> // выход при ошибке<br /> if err2 != nil {<br /> panic(err2)<br /> }</p> <p> return result.RowsAffected()<br /> }</code><br /> Можно ещё раз протестировать приложение с Postman. Проверим роут «GET /tasks» — в ответ должен прийти JSON с «tasks» равным null:<br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/d145f4391f.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> Добавим задачу. В Postman переключите метод на «PUT», затем откройте вкладку «Body». Выберите «raw», затем JSON (application/json) как тип. В текстовом поле введите следующее:</p> <p><code>{<br /> "name": "Foobar"<br /> }</code><br /> В ответ должно прийти ‘created’, типа такого:<br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/de9015cdce.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> Обратите внимание, id вернулся потму, что нам нужно протестировать роут «DELETE /tasks». Смените метод на «DELETE» и укажите Postman «/tasks/:id», заменив «:id» на id, который вернулся в прошлом пункте. В ответ должно прийти сообщение об успехе «deleted».<br /> <img title="Создание одностраничного веб-приложения на Go, Echo и Vue" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/02/27/baa8f3cb02.png" class="image-center" alt="Создание одностраничного веб-приложения на Go, Echo и Vue" /><br /> Если всё хорошо, то запросив «GET /tasks», вы получите в ответ null.</p> <h5>Фронтенд</h5> <p>Для простоты изложения, мы включим код Javascript в файл разметки HTML. Включим несколько библиотек: Bootstrap, JQuery и, конечно, VueJS. В интерфейсе у нас будет только поле ввода, кнопки и неупорядоченный список для наших задач.<br /> Создайте директорию ‘public’ и в ней файл «index.html»:</p> <p><code><!-- public/index.html --></p> <p><html><br /> <head><br /> <meta http-equiv="content-type" content="text/html; charset=utf-8"></p> <p> <title>TODO App







My Tasks

  • {{ task.name }}







Пересоберите приложение, запустите его и откройте в браузере http://localhost:8000.
Создание одностраничного веб-приложения на Go, Echo и Vue

Ниже последнего тега «div» мы разместим наш код VueJS в теге «script». Код этот не очень прост, но хорошо прокомментирован. Здесь у нас несколько методов для создания и удаления задач, а также метод инициализации, возвращающий список всех задач в базе.
Для общения с бэкендом нам понадобится HTTP клиент, мы будем использовать vue-resource так: «this.$http» и далее что нам нужно, типа (get, put, и т.п.).

Запуск

У нас всё готово, пересоберём приложение и запустим его!

$ go build todo.go
$ ./todo

Откроем в браузере http://localhost:8000

Создание одностраничного веб-приложения на Go, Echo и Vue

Мы рассмотрели несложную задачу по созданию бэкенда на Go с помощью фреймворка Echo и фронтенда на VueJS. Надеемся, она сподвигнет вас на создание по-настоящему хороших и сложных веб-приложений!

Исходный код примера.

UPD: в новой версии Echo была поломана обратная совместимость и этот пример уже не работает. Устанавливайте предыдущую версию:

go get gopkg.in/labstack/echo.v2

Full stack проект на Go за неделю. День 6: Настраиваем мониторинг

Full stack проект на Go за неделю. День 6: Настраиваем мониторинг

Это шестая часть материала. Первая часть. Вторая часть. Третья часть. Четвёртая часть. Пятая часть.

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

Какой мониторинг мы можем добавить?

Так как наше приложение написано на Go, мы можем воспользоваться библиотекой expvar, с которой мы можем получить доступ к некоторым переменным внутри Go и предоставить доступ к ним из Datadog.

В блоге Datadog есть пост о похожем процессе, в нём описано то, что нам нужно (англ.).

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

  • currency_hits счётчик запросов точки /currencies — как GET, так и POST, будут считаться одинаково
  • conversion_hits счётчик запросов точки /convert
  • webhook_hits счётчик запросов (успешной) регистрации веб-хука
  • webhook_triggers счётчик количества веб-хуков, вызванных сервером
Запуск

Перейдём в /etc/dd-agent/conf.d/ и переименуем go_expvar.yaml.example в go_expvar.yaml.

Затем отредактируем этот файл:

init_config:

instances:
- expvar_url: http://localhost/debug/vars
metrics:
- path: currency_hits
- path: convert_hits
- path: webhook_hits
- path: webhook_triggers

Готово. Перезапустим агента:

service datadog-agent restart
Всё работает!

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

При настройке мониторинга в Datadog, мы добавили новые метрики из expvar:

Full stack проект на Go за неделю. День 6: Настраиваем мониторинг
И теперь Datadog позволил добавить наши метрики к стандартным способам мониторинга.

Написание документации

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

Все изменения в репозитории добавлены в документацию.

Создание библиотеки

Итак, мы добавили документацию по использованию библиотеки. Супер. А теперь добавим что-то более крутое!

Мы добавим конечную точку, которая будет Javascript библиотекой:


Идея в том, что этот скрипт произведёт нужную конвертацию в фоне, без затрагивания бэкенда.

Изменения в коде описаны в этом коммите. Генератор сценария сделан с помощью стандартной библиотеки text/template и шаблон считывается при каждом запросе — это нужно будет оптимизировать в дальнейшем.

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

(function() {
var currency = {
currencies: ['HRK', 'INR', 'TRY', 'AUD', 'CNY', 'HKD', 'KRW', 'NZD', 'HUF', 'CHF', 'MXN', 'PHP', 'JPY', 'GBP', 'USD', 'RON', 'SEK', 'THB', 'BRL', 'CAD', 'ILS', 'SGD', 'EUR', 'CZK', 'PLN', 'NOK', 'RUB', 'IDR', 'ZAR', 'BGN', 'DKK', 'MYR', ],

rates: {
'HRK': 1.0010224123550462,
'INR': 9.613568488174995,
'TRY': 0.5262329485834208,
'AUD': 0.18688621626711868,
'CNY': 0.9838566470255872,
'HKD': 1.1119945112599887,
'KRW': 164.76444157451502,
'NZD': 0.19922242849839913,
'HUF': 41.477654908924585,
'CHF': 0.143097371324024,
'MXN': 2.9333817633922568,
'PHP': 7.171550030941426,
'JPY': 16.15411520972906,
'GBP': 0.11531735141388866,
'USD': 0.14327225764791346,
'RON': 0.6083757096348913,
'SEK': 1.2709930852638094,
'THB': 5.013856377969705,
'CAD': 0.18738396965049642,
'ILS': 0.5309817849167272,
'SGD': 0.2029488524766594,
'EUR': 0.13452794145343988,
'CZK': 3.6350795060133994,
'PLN': 0.5825732504641213,
'NOK': 1.1925229370140178,
'RUB': 8.336266042457018,
'IDR': 1910.1770387709528,
'ZAR': 1.8747410337127022,
'BGN': 0.2631097478946377,
'DKK': 1,
'MYR': 0.6379853095487933,
},

convert: function(amount, target) {
return this.rates[target] * amount
}
}

window.currency = currency
})();

Он создал глобальную (в window) переменную currency, в которой есть массив .currencies с наименованиями всех валют (краткими). А функция .convert(amount, target) конвертирует сумму из базовой в указанную валюту.

Это очень простой скрипт, но в нём описаны азы построения приложений с конвертацией в UI.

Всем спасибо за чтение!

Источник на английском.

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Это пятая часть материала. Первая часть. Вторая часть. Третья часть. Четвёртая часть.

В этой части мы настроим

непрерывную интеграцию

и

развёртывание

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

Как?

Для этой цели мы воспользуемся сторонним сервисом: Codeship

Код нашего приложения хранится на GitHub и Codeship настроен так, что каждое изменение репозитория на GitHub, автоматически вызывает преднастроенные процессы в Codeship.

Это процесс состоит из следующих шагов:

  1. Получение кода и завимостей
  2. Сборка бинарника
  3. Копирование бинарника на сервер
  4. Перезапуск приложения на сервере
Получение кода

Эта часть самая простая — Codeship сделает это за нас. Нам нужно только настроить проект в Codeship и привязать его к GitHub. При привязке аккаунта GitHub к Codeship будет создан веб-хук для оповещения Codeship об изменениях в репозитории.

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Затем нужно настроить скрипты, запускаемые при срабатывании хука.

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Вот сценарий, который работает у меня:

GO_VERSION=${GO_VERSION:="1.8.0"}
# strip all components from PATH which point toa GO installation and configure the
# download location
CLEANED_PATH=$(echo $PATH | sed -r 's|/(usr/local\|tmp)/go(/([0-9]\.)+[0-9])?/bin:||g')
CACHED_DOWNLOAD="${HOME}/cache/go${GO_VERSION}.linux-amd64.tar.gz"
# configure the new GOROOT and PATH
export GOROOT="/tmp/go/${GO_VERSION}"
export PATH="${GOROOT}/bin:${CLEANED_PATH}"
# no set -e because this file is sourced and with the option set a failing command
# would cause an infrastructur error message on Codeship.
mkdir -p "${GOROOT}"
wget --continue --output-document "${CACHED_DOWNLOAD}" "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz"
tar -xaf "${CACHED_DOWNLOAD}" --strip-components=1 --directory "${GOROOT}"
# check the correct version is used
go version | grep ${GO_VERSION}
go get -t -v ./...

Команда для запуска тестов:

go test -v ./...
Затем добавим скрипт для развёртывания. Но вначале настроим, чтобы применялись коммиты/пуши только из ветки «production».

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Выберем вариант «custom script» и вставим такой текст:

go build
scp -P 2222 currencyconverter root@$SERVER:/root/tmp_currencyconverter
ssh -p 2222 root@$SERVER< /dev/null 2>&1 &
EOF

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

Для этого нужно будет добавить открытый ключ SSH (из Project settings > General) в папку ~/.ssh/authorized_keys пользователя root. Это даст возможность Codeship соединяться с сервером без указания паролей.

Обратите внимание на переменную $SERVER — так можно указывать переменные проекта, которые потом легко будет заменить.

Full stack проект на Go за неделю. День 5: Настраиваем CI/CD

Готово. Мы настроили CI/CD и теперь при каждом обновлении ветки “production” Codeship получит измененный код, протестирует его, скопирует на сервер и запустит приложение. И всё это автоматически!

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

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

Full stack проект на Go за неделю. День 4: Запускаем бэкенд

Full stack проект на Go за неделю. День 4: Запускаем бэкенд

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

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

Выбор платформы

При выборе платформы нужно учесть множество параметров, вот только часть из них:

  • Опыт команды
  • Предложения вендоров
  • Поддерживаемые языки и т.д.

Выбор платформы целиком за вами, а мы остановимся на сервере amd64 с установленной Ubuntu — основываясь на нашем опыте и предложениях Digital Ocean.

Сборка бэкенда

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

Собрать приложение в Go проще простого:

$ go build
И всё! После этой команды в папке проекта появится исполняемый файл, это и есть наше приложение.

Постойте! Я выполняю сборку на моём MacBook и соответственно бинарный файл будет собран для моей платформы, а нам нужно собрать его под Ubuntu linux.

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

$ GOARCH=amd64 GOOS=linux go build
Вуаля! У нас теперь есть бинарник для linux. Осталось установить его на сервер.

Установка на сервер

После настройки сервера у нас должен быть доступ к нему по SSH. Скопируем приложение с помощью команды scp:

$ scp currencyconverter user@server.ip.address:.

Запуск приложения

После загрузки приложения на сервер нам осталось только запустить его — так же, как и любое другое приложение под linux:

$ ssh user@server.ip.address
server$ ./currencyconverter

Готово. Наш бэкенд запущен и работает, но на адресе и порте по-умолчанию. Изменим это и запустим его на всех адресах с портом 80, как любой другой веб-сервер (для этого нужен root доступ, воспользуемся командой sudo):

server$ sudo GFS_CURRENCY_HOST=0.0.0.0 GFS_CURRENCY_PORT=80 ./currencyconverter
Теперь мы можем открыть наш браузер и перейти на http://localhost/currencies (если запустили на локальной машине), мы должны увидеть JSON-вывод с курсами валют, относительно курса EUR.

Автозапуск при загрузке системы

Самое лёгкое решение этой задачи — переложить её на cron. Предположим, что наш бинарник лежит в /home/user — с помощью команды crontab edit под root (sudo crontab -e, так как приложению необходимы права root) добавим следующую строку:

@reboot GFS_CURRENCY_HOST=0.0.0.0 GFS_CURRENCY_PORT=80 nohup /home/user/currencyconverter &
Теперь даже после перезапуска сервера наше приложение будет так же работать и обрабатывать запросы.

Что дальше?

Завтра мы настроим continious integration и continious deployment, таким образом наше приложение будет автоматически обновляться при каждом обновлении кодовой базы.

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

Full stack проект на Go за неделю. День 3: Тестируем бэкенд

Full stack проект на Go за неделю. День 3: Тестируем бэкенд

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

Сегодня мы будет тестировать разработанный ранее бэкенд.

Цель тестирования

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

Вспомним, какие методы API должны работать в приложении. На текущий момент у нас реализованы три конечные точки и две из них поддерживают два HTTP-метода:

  • /currencies (GET, POST)
  • /convert (POST)
  • /webhook (POST, DELETE)

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

Методика тестирования

В Go есть встроенная утилита тестирования (go test), с её помощью писать и выполнять тесты очень удобно. Файлы, оканчивающиеся на _test.go будут автоматически включены в план тестирования, а функции тестирования имеют запись типа func(t *testing.T) и они будут выполнены в том же порядке, в каком написаны в файле. Имейте в виду, что файлы тестов не попадают в приложение при сборке.

План

Встроенная в Go подсистема тестирования требует небольшой инициализации. К счастью, в Go есть метод init(), всегда выполняющийся первым — даже при тестировании.

Вот пример инициализации:

var (
server *Server
runError error
)

func init() {
var err error
server, err = New()
if err != nil {
panic(err)
}

go func() {
runError = server.Run()
if runError != nil {
panic(runError)
}
}()
}

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

Тестирование инициализации

В нижеприведённом кусочке кода описана небольшая функция, проверяющая первичный запуск сервера и его непрерывную работу (в цикле, с помощью time.Sleep). При каждой итерации проверяется server.hasCurrencies для остановки цикла тестирования, когда сервер станет готов:

func TestServerCreation(t *testing.T) {
if server == nil {
t.Fatal("Server not initialized!")
}

if runError != nil {
t.Fatal("Error starting the server!")
}

for loops := 0; !server.hasCurrencies && loops < 10; loops++ { t.Log("Sleep:", loops+1) time.Sleep(time.Duration(500*(loops+1)) * time.Millisecond) } if !server.hasCurrencies { t.Fatal("Currencies not loaded!") } }
Эта функция располагается в файле a_test.go. Выбор названия файла объясняется тем, что тесты запускают файлы в алфавитном порядке, поэтому называем их, начиная с "a".

Дальнейшее тестирование

Все тесты проводятся таким же способом, используя функционал go test.
Загрузите их себе из репозитория и проверьте сами.

Проверим вывод после прохода тестов:

➜ currencyconverter git:(master) go test -v -cover ./...
? github.com/goingfullstack/currencyconverter [no test files]
=== RUN TestServerCreation
2017/02/15 20 : 02 : 05 Starting server on 127.0.0.1:4000
2017/02/15 20 : 02 : 05 Starting currency fetching...
2017/02/15 20 : 02 : 05 Starting new currency fetch...
2017/02/15 20 : 02 : 05 Currencies updated.
2017/02/15 20 : 02 : 05 Sleeping 1h0m0s
--- PASS: TestServerCreation (0.50s)
a_test.go: 18 : Sleep: 1
=== RUN TestCurrencyConversionNewServer
--- PASS: TestCurrencyConversionNewServer (0.00s)
=== RUN TestCurrencyConversion
--- PASS: TestCurrencyConversion (0.00s)
=== RUN TestUnknownCurrency
--- PASS: TestUnknownCurrency (0.00s)
=== RUN TestConvertResponseCreation
--- PASS: TestConvertResponseCreation (0.00s)
=== RUN TestConvertedResponseUnknownCurrency
--- PASS: TestConvertedResponseUnknownCurrency (0.00s)
=== RUN TestCreateCurrencyResponse
--- PASS: TestCreateCurrencyResponse (0.00s)
=== RUN TestCreateInvalidCurrencyResponse
--- PASS: TestCreateInvalidCurrencyResponse (0.00s)
=== RUN TestCurrencyGet
2017/02/15 20 : 02 : 05 [GET] /currencies
--- PASS: TestCurrencyGet (0.00s)
=== RUN TestCurrencyPostKnownCurrency
2017/02/15 20 : 02 : 05 [POST] /currencies
--- PASS: TestCurrencyPostKnownCurrency (0.00s)
=== RUN TestCurrencyPostInvalidRequest
2017/02/15 20 : 02 : 05 [POST] /currencies
2017/02/15 20 : 02 : 05 EOF
--- PASS: TestCurrencyPostInvalidRequest (0.00s)
=== RUN TestCurrencyPostUnknownCurrency
2017/02/15 20 : 02 : 05 [POST] /currencies
2017/02/15 20 : 02 : 05 Unknown currency: FOO
--- PASS: TestCurrencyPostUnknownCurrency (0.00s)
=== RUN TestCurrencyPut
2017/02/15 20 : 02 : 05 [PUT] /currencies
--- PASS: TestCurrencyPut (0.00s)
=== RUN TestConvert
2017/02/15 20 : 02 : 05 [POST] /convert
--- PASS: TestConvert (0.00s)
=== RUN TestConvertInvalidMethod
2017/02/15 20 : 02 : 05 [PUT] /convert
--- PASS: TestConvertInvalidMethod (0.00s)
=== RUN TestConvertInvalidCurrency
2017/02/15 20 : 02 : 05 [POST] /convert
2017/02/15 20 : 02 : 05 Unknown currency: INVALID
--- PASS: TestConvertInvalidCurrency (0.00s)
=== RUN TestConvertInvalidRequestData
2017/02/15 20 : 02 : 05 [POST] /convert
2017/02/15 20 : 02 : 05 EOF
--- PASS: TestConvertInvalidRequestData (0.00s)
=== RUN TestNotFoundRoute
2017/02/15 20 : 02 : 05 [GET] /notfound
--- PASS: TestNotFoundRoute (0.00s)
=== RUN TestWebhookRegister
2017/02/15 20 : 02 : 05 [POST] /webhook
2017/02/15 20 : 02 : 05 Webhook return code: 200
--- PASS: TestWebhookRegister (0.00s)
=== RUN TestWebhookCalling
2017/02/15 20 : 02 : 05 Webhook return code: 200
--- PASS: TestWebhookCalling (0.00s)
=== RUN TestWebhookWrongSecret
2017/02/15 20 : 02 : 05 [POST] /webhook
2017/02/15 20 : 02 : 05 Webhook return code: 403
--- PASS: TestWebhookWrongSecret (0.00s)
=== RUN TestWebhookWrongBase
2017/02/15 20 : 02 : 05 [POST] /webhook
--- PASS: TestWebhookWrongBase (0.00s)
=== RUN TestWebhookGet
2017/02/15 20 : 02 : 05 [GET] /webhook
--- PASS: TestWebhookGet (0.00s)
PASS
coverage: 85.1{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} of statements
ok github.com/goingfullstack/currencyconverter/server 0.524s coverage: 85.1{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} of statements

Как видно из последней строки, тест покрыл 85,1{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} нашего кода - можно сделать ещё лучше, но пока этого достаточно.

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

Продолжение