5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LaravelとVueを使ってモダンなWebアプリケーションを構築する

Posted at

Build a modern web application with Laravel and Vue

上記のリンク通りに進めていきますが、LaravelやVueなどは最新版にします。最終的にはTrelloのクローンアプリを作ります。
laravel-vue-trello-clone-app-preview.gif
※画像は転載元より引用
少しずつ加筆していきます。自分用のメモなので、解説は少なめです。関係ないところは飛ばしていきます。

Part 1: Setting up your environment(https://blog.pusher.com/web-application-laravel-vue-part-1/)

記事ではMacでの環境構築が紹介されていますが、Windowsで環境構築を行います。

Prerequisites

このシリーズに沿って進むためには、以下のものが必要です。

  • PHPの知識。
  • Laravelフレームワークの基本的な知識
  • JavaScriptとVueフレームワークの知識
  • Node.js **とNPMの基本的な知識

Setting up for PHP development: installing composer

Installing Composer on Windows

Composer-Setup.exeをダウンロードして実行してください。最新のComposerをインストールし、ターミナル内の任意のディレクトリからComposerを呼び出すことができるようにPATHを設定します。
完了したら次のコマンドを実行して確認をします。

shell
$ composer
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.10.7 2020-06-03 10:03:56

環境構築

Xamppにて環境構築を行います。以下のコマンドを実行します。

shell
$ cd C:\xampp\htdocs
$ composer create-project laravel/laravel trello-clone --prefer-dist
$ cd ./trello-clone

詳しい解説が必要な方は、以前に書いたこちらの記事を参考にしてみてください。

Laravelのタイムゾーンと言語設定

config/app.php
70行目 'timezone' => 'Asia/Tokyo',
83行目 'locale' => 'ja',

データベースの設定

xamppからphpMyAdminを起動してデータベースとユーザーを設定し、設定した情報を「.env」ファイルに書きこみます。
データベース名はフォルダ名と同一にしておくと後から見やすいので、今回はtrello-clone という名前で作成しました。

.env
4行目 DB_CONNECTION=mysql
5行目 DB_HOST=127.0.0.1
6行目 DB_PORT=3306
7行目 DB_DATABASE=trello-clone
8行目 DB_USERNAME=ユーザー名
9行目 DB_PASSWORD=パスワード

Useful Laravel CLI commands

make コマンドは最もよく使うコマンドです。これを使うと、アプリケーション用のさまざまな種類のコンポーネントを作成することができます。このコマンドを使って、モデル、コントローラ、データベースの移行、その他このシリーズでは取り上げないものを作成することができます。

app/ディレクトリに作成される新しいモデルクラスを作成するには、次のコマンドを実行します。

shell
$ php artisan make:model SampleModel

app/Http/Controller/ディレクトリに作成される新しいコントローラクラスを作成するには、次のコマンドを実行します。

shell
$ php artisan make:controller SampleController

データベース/migrations/ディレクトリに作成される新しいデータベースマイグレーションクラスを作成するには、次のコマンドを実行します。

shell
$ php artisan make:migration create_sample_table

先に生成したマイグレーションを使用してデータベースに変更をマイグレーションするには、次のコマンドを実行します。

shell
$ php artisan migrate

本当にシンプルなLaravelアプリケーションでは、これらのコマンドを使用することになるでしょう。これらのコマンドについては、アプリケーションを進めていくうちにさらに詳しく知ることができます。コンソールのドキュメントもチェックしてみてください。

Installing and configuring dependencies (Passport)

これから作るアプリケーションはAPIベースのものになるので、認識されたユーザーだけがアクセスできるようにAPIをセキュアにする方法が必要です。

Installing Laravel Passport

Laravel PassportはAPI認証のためのツールです。Passportは認証を簡単にし、数分でLaravelアプリケーションにOAuth2サーバーの完全な実装を提供します。
Passportをインストールするには、以下のコマンドを実行します。

shell
$ composer require laravel/passport

API Auth using Laravel Passport

Laravel Passportを使った認証についての詳細な記事がここにあります。このシリーズの次のパートに進む前に一読しておくと、Laravel Passportに慣れるのに役立ちます。

Conclusion

このパートでは、最新のWebアプリケーションを構築するための開発環境の設定方法を考えました。次のパートでは、Laravelアプリケーションを構築し、構築中に知っておくべき概念を説明します。

アプリケーションの構築を進めていくうちに、VueとLaravelを使って最新のアプリケーションを構築するために、どのようにすべてのツールを組み合わせることができるかがわかるでしょう。

Part 2 : Creating our endpoints with REST in mind

Building the models

Trello-cloneでは3つもモデルを使いますが、後から見やすくなるのでモデルはModelフォルダにまとめます。

Userモデル

まずapp/Model を作成して、その中にUserモデル:app/User.php
を移動し、app/Model/User.php になるように変更します。
次に以下のファイルを書き換えます。

app/Model/User.php
3行目 namespace App; => namespace App\Model;
config/app.php
71行目 'model' => App\User::class, => 'model' => App\Model\User::class,
database/factories/User.php
5行目 use App\User; => use App\Model\User;

Taskモデル、Categoryモデル

以下のコマンドを実行して、-mr オプションを利用してモデルとMigrationファイルを同時に作成します。

shell
$ php artisan make:model Model/Task -mr
$ php artisan make:model Model/Category -mr

The User model

Laravelにはデフォルトのユーザーモデルがバンドルされているので、ユーザーモデルを作成する必要はありません。ユーザーモデルは以下のフィールドを持ちます。

  • id - ユーザーのユニークなオートインクリメントID
  • name - ユーザーの名前
  • email - ユーザーのE-mail
  • password - 認証に使用されるパスワード

Userモデルを開き、以下のように編集します。

app/Model/User.php
<?php

namespace App\Model;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
  use SoftDeletes, Notifiable;

  protected $fillable = ['name', 'email', 'password'];

  protected $hidden = [
    'password', 'remember_token',
  ];

  public function tasks()
  {
    return $this->hasMany(Task::class);
  }
}

? A fillable column
代入可能なデータベースカラムのことです。つまり、代入したいデータをモデルのcreate関数に配列を渡すだけで代入できるということです。

? SoftDeletes
データベースから実際にデータを削除せずにリソースを削除する方法です。テーブルが作成されると、'deleted_at'というフィールドができ、ユーザーがタスクを削除しようとすると、'deleted_at'フィールドには削除した日付時刻が入力されます。そのため、リソースの取得が行われた場合、'deleted'リソースはレスポンスの一部にはなりません。

Task model

タスクモデルは以下のフィールドを持ちます。

  • id - タスク自体のID
  • name - タスクの名前
  • category_id - タスクが属するカテゴリのID
  • user_id - タスクが所属するユーザーのID
  • order - それぞれのカテゴリ内でのタスクの順番

Taskモデルを以下のように編集します。

app/Model/Task.php
<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
  use SoftDeletes;

  protected $fillable = ['name', 'category_id', 'user_id', 'order'];

  public function category()
  {
    return $this->hasOne(Category::class);
  }

  public function user()
  {
    return $this->belongsTo(User::class);
  }
}

Category model

カテゴリモデルには以下のフィールドがあります。

  • id - カテゴリのID
  • name - カテゴリの名前

Categoryモデルを以下のように編集します。

app/Model/Category.php
<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Category extends Model
{
  use SoftDeletes;

  protected $fillable = ['name'];

  public function tasks()
  {
    return $this->hasMany(Task::class);
  }
}

Writing our migrations

Migrationファイルを以下のように編集します。

Updating our user migration

database/migrations/2014_10_12_000000_create_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
        Schema::table('flights', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
}

Updating our category migration

database/migrations/XXXX_XX_XX_XXXXXX_create_categories_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('categories');
        Schema::table('categories', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
}

Creating our task migration

外部キーの書き方が変更になっているの(参考リンク)で、チュートリアルとは異なります。

database/migrations/XXXX_XX_XX_XXXXXX_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->foreignId('user_id')->constrained();
            $table->foreignId('category_id')->constrained();
            $table->integer('order');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tasks');
        Schema::table('tasks', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
}

ファイルの並びを確認する

create_tasks_table.phpusers テーブルのIDとcategories テーブルのIDを外部キーとして使用するため、先にusers テーブルとcategories テーブルを作成しないと以下のようなエラーが起こります。

SQLSTATE[HY000]: General error: 1005 Can't create table `trello-clone`.`tasks`
(errno: 150 "Foreign key constraint is incorrectly formed")
(SQL: alter table `tasks` add constraint `tasks_category_id_foreign` 
foreign key (`category_id`) references `categories` (`id`))

エラーを回避するためにはdatabase/migratsions フォルダのファイルが以下のようにcreate_tasks_table.php が一番最後にあるかどうか確認します。

2014_10_12_000000_create_users_table.php
2019_08_19_000000_create_failed_jobs_table.php
XXXX_XX_XX_XXXXXX_create_categories_table.php
XXXX_XX_XX_XXXXXX_create_tasks_table.php

create_tasks_table.php が一番最後にない場合は、一番最後になるようにファイル名を変更します。
次に以下のコマンドを実行してMigrateします。

shell
$ php artisan migrate

? Migrationsはデータベースのバージョン管理のようなものです。これにより、アプリケーションの進化に合わせてデータベースを作成、変更、削除することができ、SQLクエリ(または任意のデータベースが使用するクエリ)を手動で記述する必要がありません。また、あなたとあなたのチームがアプリケーションのデータベーススキーマを簡単に変更して共有することができます。詳細はこちらをご覧ください。

Database seeders

データベースの移行ができたので、アプリケーションをテストするときのためにダミーデータを入れる方法を見てみましょう。Laravelでは、シーダと呼ばれるものがあります。

? シーダーを使うと、ダミーデータを自動的にデータベースに挿入することができます。

Creating our users table seeder

データベースシーダを作成するには、次のコマンドを入力します。

shell
$ php artisan make:seeder UsersTableSeeder

これにより、database/seeds/UsersTableSeeder.php ファイルが作成されます。ファイルを開き、内容を以下のコードに置き換えてください。

database/seeds/UsersTableSeeder.php
<?php

use App\Model\User;
use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    public function run()
    {
        User::create([
            'name' => 'John Doe',
            'email' => 'demo@demo.com',
            'password' => bcrypt('secret'),
        ]);
    }
}

run関数には、シーダの実行時に実行したいデータベースクエリが含まれています。

? model factoriesを使用して、より良いシードデータを作成することができます。

? 保存する前にパスワードをハッシュするためにbcryptを使用していますが、これはLaravelがパスワードをハッシュするために使用するデフォルトのハッシュアルゴリズムだからです。

Creating our categories table seeder

データベースシーダを作成するには、次のコマンドを入力します。

shell
$ php artisan make:seeder CategoriesTableSeeder

これにより、database/seeds/CategoriesTableSeeder.php ファイルが作成されます。ファイルを開き、内容を以下のコードに置き換えてください。

database/seeds/UsersTableSeeder.php
<?php

use App\Model\Category;
use Illuminate\Database\Seeder;

class CategoriesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $categories = ['Ideas', 'On Going', 'Completed'];

        foreach ($categories as $category) {
            Category::create(['name' => $category]);
        }
    }
}

Running our database seeders

データベースシーダを実行するには、database/seeds/DatabaseSeeder.php ファイルを開き、runメソッドを以下のコードに置き換えます。

database/seeds/DatabaseSeeder.php
public function run()
{
    $this->call([
        UsersTableSeeder::class,
        CategoriesTableSeeder::class,
    ]);
}

次に以下のコマンドを実行します。

shell
$ php artisan db:seed

これでデータベースがデータで更新されるはずです。ある時点でデータをリフレッシュして再度シードしたい場合は、以下のコマンドを実行してください。

shell
$ php artisan migrate:fresh --seed

これはデータベースのテーブルを削除し、それらを元に戻してシーダを実行します。

REST in a nutshell

技術的な用語では、RESTはREpresentational State Transferの略です(他の場所では、単に「リラックスする」という意味です)。この記事をよく理解するために、一口サイズのナゲットに分解する必要がある用語がいくつかあります。

Clients, statelessness, resources – what’s the relationship?

クライアントClients とは、アプリケーションと関わるデバイスのことです。どんなアプリケーションでも、それを操作するクライアントの数は 1 から数十億にもなります。あなたがウェブサイト (https://pusher.com など) にアクセスすると、クライアントはサーバーにリクエストを送信します。サーバーはあなたのリクエストを処理してから、クライアントに応答を送り返し、相互作用を行います。

ステートレスStatelessness とは、最も簡単に言えば、クライアントがすべてのリクエストを完了するために必要なものをすべて持っているような方法でアプリケーションを構築することを意味します。クライアントがそれ以降のリクエストを行うとき、サーバはクライアントに関連するデータを保存したり取得したりしません。アプリケーションがより多くのアクティブな同時使用ユーザを持つようになると、クライアントの状態を管理するサーバに不必要な負担がかかるようになります。ステートレスであることはアプリケーションの設計をシンプルにします。

リソースResources は、コード内の実際のインスタンスを表現したものです。例えば、学生が自分の成績をチェックできるアプリケーションを構築しているとします。これらのリソースは、データベースに保存されるデータとリンクされています。

RESTfulアプリケーションを構築するとき、サーバーはクライアントにリソースへのアクセス権を与えます。クライアントは、リソースを取得、変更、削除するためのリクエストを行うことができます。リソースは通常JSONやXML形式で表現されますが、他にも多くの形式があり、どの形式にするかは実装中に決めることができます。

Creating your first few REST endpoints

エンドポイントの作成を始める前に、REST リソースの命名のベストプラクティスに精通していることを確認してください。

現在、以下の HTTP 動詞を使用しています。

  • GET - 通常はリソースを取得するために使用します。
  • POST - 新しいリソースを作成するために使用します。
  • PUT/PATCH - 既存のリソースを置換/更新するために使用します。
  • DELETE - リソースを削除するために使用する

以下は、タスクリソースの REST エンドポイントがどのように見えるかを表形式で表したものです。

METHOD ROUTE FUNCTION
POST /api/task Creates a new task
GET /api/task Fetches all tasks
GET /api/task/{task_id} Fetches a specific task
PUT/PATCH /api/task/{task_id} Update a specific task
DELETE /api/task/{task_id} Delete a specific task

それでは、アプリケーションでルートの作成を始めましょう。routes/api.phpファイルを開き、更新します。

routes/api.php
Route::resource('/task', 'TaskController');
Route::get('/category/{category}/tasks', 'CategoryController@tasks');
Route::resource('/category', 'CategoryController');

上記では、ルートを定義しました。2つのルートがあり、それらを手動で作成しなくても他のすべてのルートを登録してくれます。リソースコントローラとルートについてはこちらをご覧ください。

Formatting responses and handling API errors

ここでは、リクエストが処理されたときのレスポンスの作成方法とフォーマットについて見ていきましょう。

Creating our controllers

ルートができたので、すべてのリクエストを処理するコントローラロジックを追加する必要があります。コントローラを作成するには、ターミナルで以下のコマンドを実行する必要があります。

shell
$ php artisan make:controller NameController

しかし、先ほどの-mrを使った時にリクエストを作成しているので、それを編集してみましょう。

app/Http/Controller/ディレクトリ内のTaskController.phpを開きます。その中で、先ほど作成したルートを処理するための基本的なメソッドを定義します。
ファイル内のストアメソッドを以下のように更新します。

app/Http/Controller/TaskController.php
public function store(Request $request)
{
    $task = Task::create([
        'name' => $request->name,
        'category_id' => $request->category_id,
        'user_id' => $request->user_id,
        'order' => $request->order
    ]);

    $data = [
        'data' => $task,
        'status' => (bool) $task,
        'message' => $task ? 'Task Created!' : 'Error Creating Task',
    ];

    return response()->json($data);
}

? 16行目で、レスポンスがJSON形式に設定されていることがわかります。データをどのようなレスポンス形式で返したいかを指定することができますが、ここではJSONを使用します。

⚠️ まだアプリケーションの肉付けには力を入れていません。RESTfulエンドポイントを作成する方法を説明しています。後編ではコントローラを完全に作成していきます。

Securing our endpoints with Passport

ルートを作成しましたが、今のままでは誰でもアクセスできてしまうので、制限をかけて安全にしていきます。

Laravelは、デフォルトでWebルートとAPIルートをサポートしています。WebルートはWebブラウザからアクセスした動的に生成されたページのルーティングを処理し、APIルートはJSONまたはXML形式のレスポンスを必要とするクライアントからのリクエストを処理します。

Authentication and authorization (JWT) to secure the APIs

Part 1 では、Laravel Passportを使ったAPI認証について説明しているので、簡単にまとめます。

まず、Laravel Passportをインストールします。

shell
$ composer require laravel/passport

Laravel Passportには、動作に必要なデータベーステーブル用のMigrateファイルが付属しているので、実行します。

shell
$ php artisan migrate

次に、アプリケーションを保護するために必要なキーを作成できるように、パスポートのインストールするコマンドを実行します。

shell
$ php artisan passport:install

このコマンドは、セキュアなアクセストークンを生成するために必要な暗号化キーに加えて、アクセストークンを生成するために使用される「個人アクセス」と「パスワード付与」クライアントを作成します。

インストール後、UserモデルでLaravel Passport HasApiToken trait を使用する必要があります。この特性はモデルにいくつかのヘルパーメソッドを提供し、認証済みユーザーのトークンとスコープを検査できるようにします。

app/Model/User.php
<?php

[...]

use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, SoftDeletes, Notifiable;

    [...]
}

次に、AuthServiceProviderのブートメソッド内のPassport::routesメソッドを呼び出します。このメソッドは、アプリが必要とするトークンを発行するために必要なルートを登録します。

app/Providers/AuthServiceProvider.php
<?php

[...]

use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    [...]

    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }

    [...]
}

最後に、config/auth.phpの設定ファイルで、API認証ガードのドライバオプションをpassportに設定します。

config/auth.php
[...]

'guards' => [
    [...]

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

[...]

Log in and register using the API

Laravel Passportを使ってこのアプリケーションのAPI認証を設定したところで、ログインと登録のエンドポイントを作っていきます。

routes/api.php ファイルに以下のルートを追加します。

routes/api.php
Route::post('login', 'UserController@login');
Route::post('register', 'UserController@register');

また、APIの認証リクエストを処理するためのUserControllerを作成する必要があります。app/Http/Controllers/UserController.php というファイルを新規作成し、その中に以下のコードを配置します。

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Model\User;
use Validator;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    public function login()
    {
        $credentials = [
            'email' => request('email'),
            'password' => request('password')
        ];

        if (Auth::attempt($credentials)) {
            $success['token'] = Auth::user()->createToken('MyApp')->accessToken;

            return response()->json(['success' => $success]);
        }

        return response()->json($response, $status);
    }

    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required',
        ]);

        if ($validator->fails()) {
            return response()->json(['error' => $validator->errors()], 401);
        }

        $input = $request->all();
        $input['password'] = bcrypt($input['password']);

        $user = User::create($input);
        $success['token'] = $user->createToken('MyApp')->accessToken;
        $success['name'] = $user->name;

        return response()->json(['success' => $success]);
    }

    public function getDetails()
    {
        return response()->json(['success' => Auth::user()]);
    }
}

上のコードでは

Login Method: ここでは、ユーザが提供した認証情報を使って Auth::attempt を呼び出しています。認証に成功した場合は、アクセストークンを作成してユーザに返します。このアクセストークンは、ユーザがAPIにアクセスできるようにするために、すべてのAPIコールと一緒に送信するものです。

Register Method: ログインメソッドと同様に、ユーザー情報を検証し、ユーザーのアカウントを作成し、ユーザーのアクセストークンを生成します。

Grouping routes under a common middleware

ルートについては、認証が必要なルートを共通のミドルウェアの下にグループ化することができます。Laravelにはauth:apiミドルウェアが組み込まれているので、それを使っていくつかのルートを保護することができます。

routes/api.php
<?php

Route::post('login', 'UserController@login');
Route::post('register', 'UserController@register');

Route::group(['middleware' => 'auth:api'], function(){
    Route::resource('/task', 'TasksController');
    Route::resource('/category', 'CategoryController');
    Route::get('/category/{category}/tasks', 'CategoryController@tasks');
});

Handling API errors

サーバーがリソースを提供したり操作したりしているときにエラーが発生した場合、クライアントに何か問題が発生したことを伝える方法を実装しなければなりません。そのためには、特定のHTTP ステータスコードでレスポンスを提供しなければなりません。

UserController.php ファイルを見ると、HTTPステータスコード401を実装しているのがわかります。

Conclusion

このシリーズのこの部分では、アプリケーション用の RESTful エンドポイントをどのように作成するかを考えてきました。また、どのようにしてエラーを処理し、正しいHTTPステータスコードをクライアントに提供するかについても検討しました。

次の章では、Postmanを使ってAPIエンドポイントをテストする方法を取り上げます。コマンドラインからのテストに役立つユニットテストをいくつか設定します。

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?