3
3

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 5 years have passed since last update.

DartとRedstoneを使った簡単カスタムWEBサーバー (翻訳)

Last updated at Posted at 2015-07-06

原題: Easy Custom Web Servers with Dart and Redstone
著者: Monty Rasmussen
原著公開日: 2015年06月25日
原著: http://www.sitepoint.com/custom-web-servers-dart-redstone/

この記事はsitepointの、Monty Rasmussen氏による記事の翻訳です。
怪しい日本語の表現が出てきた場合は、私の翻訳力不足ですので、ぜひ原文をご覧になってください。
また、誤訳を発見した方はコメント欄でご連絡いただければ修正します。

なお、この翻訳の公開にあたり、sitepointの Ophélie Lechat氏に許可をいただいています。
ありがとうございます!
Thanks Ophélie for permit!

これより下が記事の本文になります。


サーバーサイドのスクリプトにNode.jsをつかうことは、現在はとても流行っていますし、それだけの理由があります。

Node.jsはJavaScriptでつくられ、速く、イベントドリブンで、おそらくほとんどのWEBデベロッパーにとって良い選択肢ですし、もしもあなたが、フロントエンドを全てJavaScriptで記述しているなら、バックエンドでも同じ言語を使うことの利点は明らかです。

Node.jsにはExpressのような優れたサーバーサイドフレームワークがありますし、それによってカスタムWEBサーバーを、早く、そして簡単に作ることができます。

さて、もっと良い方法はあるでしょうか?

Dartとは何でしょうか?

Dartはオープンソースの、スケーラブルで、オブジェクト指向プログラミング言語で、WEBサーバーやモバイルアプリを作るための、しっかりとしたライブラリとランタイムを備えています。
もともとはGoogleの開発者であるLars Bak氏とKasper Lund氏によって開発されていましたが、最近ECMA標準になりました。

Dartと、DartのサーバーサイドフレームワークであるRedstoneを使うと、Node.jsの利点とプラスアルファを享受することができます。
あとは、JavaScriptの奇妙な癖を忘れさることができますね。

Node.jsのように、DartのVirtualMachine(訳注:以後VM)もイベントドリブンで、非同期で、クライアントもサーバーもひとつの言語で、コードを共有しながら作る事ができます。
この場でほかのJavaScriptに勝るDartの利点の全てを述べていくことはしませんが(別の記事で書くかも)、もしもっと詳細な利点に興味があるようなら次のリストをみてください。

Dartのアドバンテージ

  • イミュータブルなオブジェクトとシンプルなセマンティクス、VMによる(高速化する)より良いコード最適化。
  • オプショナルタイプと、final、constのサポート。
  • オプショナルなデフォルト値を持つ名前付き引数をサポート。
  • 静的スコープ(lexical scope)を持つ変数、クロージャ、this。
  • 変数のhoisting(変数の巻き上げ)が無い。(訳注: hoistingについては文末に説明を付けてます。varのアレのこと)
  • 代入や比較の際に型の強制がありません。
  • No type coercion in assignments or comparisons.
  • Future (Promise) と Stream
  • undefined は無く、null だけです。
  • true だけが truthy です。
  • 包括的な標準ライブラリを備えています。
  • クラスのコンストラクタにおけるシンタックスシュガーで冗長性を減らします。
  • 遅延ロードによるビルトインコードモジュールをサポートします。
  • DartはObservatoryという、独自の高機能なプロファイラを備えています。
  • ある開発者の経験が垣間見れる Moving from Node.js to Dart を是非ご覧になってください。

これらのリストは単なる表面の傷にすぎません。Dart言語の短期集中コースとして Dart: up and Runningをチェックしてみてください。
もしJavaScriptやJava、PHP、ActionScript、C/C++や他の "波括弧" 言語の知識があるなら、Dartはとても親しみやすく、1時間ちょっとでDartによってプログラミングを行えるようになるでしょう。
(訳注: 波括弧言語は、C系の構文のように {} が良く出てくる言語の事だと思われます)

Get Dart

Dartの開発をサポートする多数のエディタがありますし、Dart開発チームはJetBrainのWebStorm対応を優先的に進めていくと発表しました。
他にも、簡単に(そして無料で)、Sublime Text 3に、Dartプラグインをいれて使うことで、このチュートリアルを進める事ができます。
いずれにせよ技術的にはまだベータ版ですが、それを利用する事をお勧めしています。

ソフトウェアをダウンロードする

このチュートリアルを完了するためには、いくつかのソフトウェアが必要です。

Sublime Text 3

もしまだSublime Text 3をインストールしていない場合、使っているOSに合わせて最適なバージョンのものをダウンロードし、インストールしてください。ちなみにこの記事を書いている時点での最新バージョンは3083でした。

Dart SDK

あなたのシステムに合わせた正しいDart SDKをダウンロードしてください。ひとつ注意事項としては、このチュートリアルを実施するにあたってDartEditor(現在は非推奨)や Dartium(Dart VMをのせたスペシャルエディションのChrominium)は必要ありません。

ダウンロードしたDart SDKをシステム内の適切なフォルダに解凍・設置してください。Windowsの場合、私は C:\Program Files\dart\dart-sdk に置きます。

Sublime Text 3 の設定

Sublime Text 3を起動してください。その後、Dartのサポートを有効にするための設定を行う必要があります。

Package Control

もしまだPackage Controlをインストールしていないのであれば、このページの指示に従ってインストールしてみてください。
注意点としては、インストール完了後にSublime Text 3を再起動する必要があります。

Dart Plugin

  1. Sublime Text 3のメニューから、 Tools -> Command Palette... を選択し、「install」を入力してください。
  2. ドロップダウンメニューから「Package Control: Install Package」を選択してください。
  3. 「dart」と入力し、Dartパッケージを選択してください。注意点としては、全てのプラグインの機能を有効にする前に、Sublime Text 3を再起動する必要があります。
  4. つぎにSublime Text 3のメニューから Preferences -> Package Settings -> Dart -> Settings - User を選択します。
  5. 次のコードを /path/to/dart-sdk の部分をあなたのシステムの dart-sdk フォルダまでのパスに経校したうえで入力してください。
{ 
  "dart_sdk_path": "/path/to/dart-sdk"
}

Dartのプロジェクトを作成する

  1. Sublimeのメニューから Tools -> Command Palette... を選択し、「Dart:」と入力します。
  2. Dart: Stagehand を選択し、さらにconsole-full をコマンドラインアプリケーション作成のために選んでください。
  3. Sublimeのウィンドウの下の方で、DartのStagehandツール(訳注:Dartプロジェクトのスケルトンを作成してくれるツール)が、どのパスに新しいDartのプロジェクトを作成するか聞いてきます。このディレクトリは新規ディレクトリか、空のディレクトリである必要があるので注意してください。今回は redstone_intro という名前にする事をお勧めします。

注意: もし処理中に「Stagehand is not enabled」というエラーを見た場合は、次のコマンドをターミナルで実行してくみてください。

cd /path/to/dart-sdk/bin
pub global activate stagehand

依存関係を解決します

さて、新しいプロジェクトが作成されたので pubspec.yaml というファイルを開いてください。Dartはプロジェクトが依存関係を管理するために pubspec というファイルを用いています。
開いたpubspec.yamlの中に既に書かれている「dependencies」セクションを、次のように変更してください(コメントである事を表す # から始まる行は全て削除してください)。

dependencies:
  redstone: '>=0.5.21 <0.6.0'

ファイルを保存すると、Sublimeは自動的に Pub とよばれるDartのパッケージマネージャに指示を出し、Redstoneフレームワークを含む、全ての必要な依存関係を解決してくれます。
Pubは指定された範囲のバージョンのものを取得します。また、pubspec.yamlを編集中に F7 ホットキーを押すことでSublimeが同じように依存性の解決を行ってくれます。

Redstoneの詳細な情報やサンプルは、プロジェクトのGithubのWikiを見てください。

Webサーバーを作る

簡単なサーバーをRedstoneで作ることは簡単です。
main.dart ファイルを開き(訳注: bin/main.dart)、既に記載されているコードを全て削除します。
そして、次のコードを記入してください。

import 'package:redstone/server.dart' as Server;
 
void main() {
  Server.setupConsoleLog();
  Server.start();
}

これがあなたにとって初めてのDartプログラムです。さぁ、1行ずつ見ていきましょう。
JavaやJavaScript、C#などの言語にくわしい開発者ならこれらの概念はおなじみでしょう。

import 'package:redstone/server.dart' as Server;

まず最初に、DartアナライザーにRedstoneの server.dart をコードから使う事を教えてあげます。
この特別な package: プレフィックスはPubで取得した依存関係である事を表しています。
(もし気になるなら、packages フォルダ内の、他のすべてのダウンロードされたパッケージを調べることができます。)
これによって、このDartプログラムのネームスペースに、Redstoneのクラスとトップレベル関数群をインポートします。
しかしそれは start() のようなよくある名前も含まれるため、カスタムネームスペース Serveras Server というシンタックスで設定します。

void main()

全てのDartプログラムはトップレベル関数の main() から処理が開始されます。
Dartはオプションで変数や関数の返り値の型を指定する事ができます。
そして voidmain() が何も返さない事を示しています。

Server.setupConsoleLog();

RedstoneパッケージをServerというエイリアスでインポートしたので、Redstoneの関数を呼び出すときはその参照(訳注:Serverのこと)を使う必要があります。
必ずしもこのエイリアスは必要ではありませんが、開発の手助けになります。
この1行はRedstoneフレームワークのコンソールログ出力をセットアップし、有益な情報などのRedstoneの実行ログをコンソール上に表示させます。

Server.start();

この行はRedstoneの start() 関数を呼び出していて、WEBサーバーを起動しています。
デフォルトでは 0.0.0.0:8080 (現在のIPの8080ポートの意味) をLISTENしていますが、これは設定で変更できます。

これでおしまいです!このサーバーはまだ意味のあるレスポンスを返してはいませんが、だとしてもちゃんとLISTENしています。main.dartShift+F7を押すとコードが実行されます。
コンソールログはSublimeの画面の下の方にある出力パネルに表示されます。

INFO: <current date/time>: Running on 0.0.0.0:8080

アプリケーションを停止させたい場合は Ctrl+Keypad0 (Ctrlキーとキーパッドのゼロ)を押すといいでしょう。

Note: ターミナルからサーバーの start/stop が行えます

cd /path/to/dart-sdk/bin
./dart /path/to/redstone_intro/bin/main.dart

SublimeでDartファイルの全てのコマンドを使うには、Sublimeのコマンドパレットが必要です(特にあなたのPCにキーパッドが無い場合には)。
Sublimeの Tools -> Command Palette... を選択し、「Dart:」を入力し、必要なコマンドを選択してください。そしてこれのショートカットが Ctrl+. Ctrl+c (2回連続で Ctrlと.を同時押しする)です。

もっとキーボードショートカットが知りたい場合は、Dartプラグインのショートカットページをご覧になってください。

パスセグメントパラメータ

さて、サーバーがいくつかの要求に答えられるようにしてみましょう。
RedstoneのRouteアノテーションを使ってハンドラを作成することができます。

Hello

つぎのコードを main.dart の末尾に追加してみてください(main() の後ですよ)。

@Server.Route("/hello")
String hello() {
  print("User soliciting greeting...");
  return "Hello, Browser!";
}

Redstoneを Server エイリアス付きでインポートしているので、Serverへの参照を含めるかたちで設定する必要がある事に注意してください。
(@から始まる)アノテーションは、次のリクエストがきたときに hello() 関数で、レスポンスする返り値を決めるよう、RedstoneのRouterに伝えます。

http://localhost:8080/hello

もしまだDartのサーバーが起動し続けていた場合は、サーバーを停止後に再起動し、ブラウザを開いてからURLを入力するとサーバーの結果が表示されます。
「Hello, Browser!」文字列を見る事ができるでしょう。また、print()を呼び出すと、有用なメッセージをコンソール上に出力する事ができます。

Hi

他のRouteブロックを main.dart の末尾に追加してみましょう。

@Server.Route("/hi")
String hi() => "Hi, Browser!";

このコードは一つ前の例と似ていますが、とても短い関数を定義するためのファットアローシンタックス(訳注: =>のこと)を使っています。このように書かれているので、 hi() 関数は、矢印の次の計算式の結果を返します。この場合は文字列リテラルを返します。

この例をブラウザで試すには、次のURLをブラウザで開いてください。

http://localhost:8080/hi

高度なパスセグメントパラメータ

静的なパラメータを認めることは良いことですが、現実世界においては、カスタマイズされたレスポンスを得るためにダイナミックな値をサーバーに渡す必要があります。

モックデータ

このあとのいくつかの演習のために、モックのデータベースと、いくつかのヘルパー関数となるデータモデルを追加しておきましょう。

main()の上、importの下に次のようにユーザーのリストを追加しましょう。

import 'package:redstone/server.dart' as Server;
 
List<Map> users = [
  {"id": "1", "username": "User1", "password": "123456", "type": "manager"},
  {"id": "2", "username": "User2", "password": "password", "type": "programmer"},
  {"id": "3", "username": "User3", "password": "12345", "type": "programmer"},
  {"id": "4", "username": "User4", "password": "qwerty", "type": "secretary"},
  {"id": "5", "username": "User5", "password": "123456789", "type": "secretary"}
];
 
void main() {
  Server.setupConsoleLog();
  Server.start();
}

DartにおいてListは本質的な配列で、MapはJavaScriptの標準のオブジェクト(あるいは dictionaryhashmap と他の静的型付け言語ではよばれます)のようなものです。
users 変数は、List<Map>シンタックスによって、Map要素のListである事が定義されています。
[]{} をつかったシンタックスリテラルはJavaScriptプログラマーには馴染みやすいでしょう。
main()の上にusersを定義したので、これはトップレベル変数になり、このファイル内の全ての関数内からアクセスする事ができます。

ヘルパー関数

さて、ユーザーのリストの準備ができたので、サーバーからのレスポンスを整形する2つのヘルパー関数を定義することにしましょう。次のコードをmain.dartの末尾に追記してください。

Map success(String messageType, payload) {
  return {
    "messageType": messageType,
    "payload": payload
  };
}
 
Map error(String errorMessage) {
  print(errorMessage);
 
  return {
    "messageType": "error",
    "error": errorMessage
  };
}

1つめの success()関数は、2つの引数を元に作られるMapを返します。
messageTypeはstringで、ユーザー一人のデータを返すのか、ユーザーのリストを返すのかによって、"user""users"のどちらかになります。
そのためpayloadパラメータは柔軟に型を変えられるように、あえて型宣言をしていません。Dartでは型指定が無い場合、デフォルトでdynamic型が適用されます。

つぎの error()関数も基本的には同じような事をしますが、エラーの状態を表現するのに十分なMapを返すようになっています。

ハンドラが単純な文字列のMapを返した際には、Redstoneフレームワークは自動的にJSONに変換してレスポンスしてくれます。

ユーザーデータをユーザーIDから取得する

main.dartに新たなRouteハンドラを追加する準備が整いました。

@Server.Route("/user/id/:id")
Map getUserByID(String id) {
  print("Searching for user with ID: $id");
 
  // IDをStringからintに変換します
  int index = int.parse(id, onError: (_) => null);
 
  // エラーをチェックします
  if (index == null || index < 1 || index > users.length) {
    return error("Invalid ID");
  }
 
  // ユーザーデータを取得します
  Map foundUser = users[index - 1];
 
  // ユーザーデータをレスポンスします
  return success("user", foundUser);
}

このRouteは2つの静的パラメータ(user と id)を受け取り、もうひとつの動的なパラメータ(:id)を受け取るようになってます。:(コロン)シンタックスはリクエストユーザーが示す値をハンドラ側で受け取れるようにします。この関数のコードでは、あえて分かりやすくするために冗長なコメントを書いています。

print("Searching for user with ID: $id");

まず最初に、サーバーのコンソールにメッセージを出力しています。
$idシンタックスはDartのビルトイン文字列差し込み機能です(これについてはまた後で)。

int index = int.parse(id, onError: (_) => null);

つぎにListのインデックスを参照するときに使えるように、受け取ったIDをStringからintに変換しています。
int.parse()は、変換するための値と、オプショナルで、何らかのパースエラー時の振る舞いを表すコールバック関数を受け取ります。onErrorは名前付き引数で、コールバックはnullを返すファットアロー関数にしています。
コールバックは1つのパラメータを受け取りますが、今回はそのパラメータは使わないので、慣例として _エイリアスを利用して除外しています。
IDを正しくintにパースできなかった場合、onError関数の返り値が index変数にはセットされ、今回だとnullがセットされる事になります。

if (index == null || index < 1 || index > users.length) {
  return error("Invalid ID");
}

もしindexがリストの範囲外だった場合、"Invalid ID"というエラーメッセージが設定されたエラーオブジェクトをerror()ヘルパー関数によって返します。

Map foundUser = users[index - 1];
return success("user", foundUser);

すべてが順調にすすんだ場合、このハンドラはリクエストに対してリクエストされたIDのユーザーデータをレスポンスします。
このsuccess()ヘルパー関数によって、messageTypeが"user"で、payloadがユーザーデータのMapオブジェクトを生成しています。

さて、確認のために次のURLをブラウザで開いてみましょう。

http://localhost:8080/user/id/5

リクエストしたユーザーデータを含むJSON文字列が結果として表示されます。

ユーザーを Type で取得する

もうひとつハンドラを main.dart に追加してみましょう。

@Server.Route("/user/type/:type")
Map getUsersByType(String type) {
  print("Searching for users with type: $type");
 
  // 条件に適したユーザーを見つける
  List<Map> foundUsers = users.where((Map user) => user['type'] == type).toList();
 
  // エラーをチェックする
  if (foundUsers.isEmpty) {
    return error("Invalid type");
  }
 
  // ユーザーのリストを返す
  return success("users", foundUsers);
}

このRouteはIDではなくtypeによってユーザーを取得する事ができます。
ひとつのtypeに複数のユーザーが紐づくので、あらかじめ複数のユーザーを返せるようにしておく必要があります。

typeが一致するそれぞれのユーザーのMapオブジェクトのリストを作るために、 Listオブジェクトの標準関数であるwhere()関数を使っています。
この関数には、そのリストの要素がひとつずつ渡ってくるので、要素をテストし、保持したい場合はtrueを返すような関数にしてください。
where()は実際にはListの祖先(クラス)であるIterableを返しますので、toList()関数を使ってListに変換しています。
もしtypeが一致するユーザーが見つからなかった場合は、foundUsersは空のListになりますので、サーバーはエラーオブジェクトをレスポンスします。

この新しいRouteを確認するためのURLです。
2つのユーザーデータを含んだ配列のJSONが表示されます。

http://localhost:8080/user/type/programmer

クエリーパラメータ

クエリー文字列のキーバリューのペアをRedstoneから取得するのはとても簡単です。

次のハンドラを main.dart に追加してください。

@Server.Route("/user/param")
Map getUserByIDParam(@Server.QueryParam("id") String userID) {
  return getUserByID(userID);
}

この例ではハンドラの引数 userID に、クエリーパラメータの id を渡すためのアノテーションをつけています。

http://localhost:8080/user/param?id=2

静的なページの配信

あなたがDartのサーバーから静的なページを配信したくなったらどうしましょうか?
ちょっとした行数のコードを追加するだけで、それは実現できます。

まず最初に、プロジェクトのbinフォルダと同階層にwebフォルダを作成します。
このwebフォルダの中に、index.htmlというHTMLファイルを作成し、次のコードを記載します。

<!DOCTYPE html>
 
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
 
  <body>
    <p>Hello from index.html!</p>
  </body>
</html>

さらに、スムーズに事を進めるために、いくつかのパッケージをPubで取得します。
pubspec.yamlファイルを今一度開いて、dependenciesセクションを次のようにしてください。

dependencies:
  redstone: '>=0.5.21 <0.6.0'
  shelf_static: '>=0.2.2 <0.3.0'
  path: '>=1.3.5 <1.4.0'

Redstoneは、GoogleのDartチームによって開発・メンテナンスされている低レイヤーなサーバーライブラリである、Shelfを元に作られています。
これは、Shelfのミドルウェア(訳注:プラグインのようなもの。あるいはExpressのミドルウェアのようなもの)をRedstoneに追加して使うことが出来るという事です。
また、pathは、パス文字列をパースしたり操作するのに役立ちます。

Sublime はpubspec.yamlnい新しいdependenciesを追記して保存すると、自動的にPubを使って依存関係を解決してくれます。

これらのパッケージがプロジェクト内にダウンロードされ使えるようになったなら、main.dartの先頭にimportを追加してください。

import 'dart:io' show Platform;
import "package:path/path.dart" as Path;
import 'package:shelf_static/shelf_static.dart';

プラットフォームにアクセスするために、ioというDartのコアライブラリをインポートします。
showキーワードを使うことで、ioライブラリに含まれるその他の I/O系の関数やクラスを無視して、Platformだけをインポートする事ができます。

Path ライブラリは一般的な名前のトップレベル関数を持っているので、Pathという名前でimportするにあたって最適なパッケージです。

main()の先頭に新しく2つの行を追加します。(訳注:下のコード例では分かりやすいように改行しているので6行追加しています)

void main() {
  String pathToWeb = Path.normalize(
    "${Path.dirname(Path.fromUri(Platform.script))}/../web"
  );
  Server.setShelfHandler(
    createStaticHandler(pathToWeb, defaultDocument: "index.html")
  );
  Server.setupConsoleLog();
  Server.start();
}

サーバーを再起動後にサーバーのルート(root)に移動してみると、index.htmlを取得できます。

http://localhost:8080/

読者の皆さんが ShelfPath について調べてほしいので前述の例の詳しい説明はしませんが、ここで、Dartのより便利な機能である、文字列補完について簡単に説明しておくべきでしょう。

文字列の中で ${} リテラルを使うことで、計算式の結果を置くことができます。
式ではなくIDだけでよければ、$だけで良いです。(訳注:ココで言うIDは変数名とかのこと)

int myNumber = 5;
 
// 5 is my favorite number
String str1 = "$myNumber is my favorite number.";
 
// 5 + 10 = 15
String str2 = "$myNumber + 10 = ${myNumber + 10}";

最後に

このチュートリアルで、私は サーバーサイドにおける JavaScript、Node.js、Expressの素晴らしい代替手段について紹介しました。
Darは、数百万行のコードにスケールするために作られた、モダンで高速な言語です。
Redstoneは開発者ライフを簡単にする、たくさんのサーバーサイドフレームワークのひとつではありますが、私の大のお気に入りです。
なぜなら、サーバーの複雑な相互作用のための必要な定型文の量を、Dartのコードアノテーション機能を使って減らしているからです。

もしクライアントサイドも同じようにDartで書いた場合、サーバーとクライアントでコードを共有する事ができ、異なる言語で書かれたときのコンテキストスイッチの切り替えコストを避けることができます。
開発中には、JavaScript開発者が長年エンジョイしてきた Change-And-Refresh フローがすぐに使えるDartiumとよばれる特別なブラウザーを使うこともできます。
クライアントサイドのコードが完成したら、ちょっとクリックするだけで(あるいはコマンドラインを叩くだけで)、dart2jsがDartのコードを、全てのモダンブラウザで動く、最小化し、結合し、Tree-Shaken(訳注: ※2)されたすぐにデプロイ可能なJavaScriptコードにコンパイルしてくれます。

Join the Dart side.


翻訳ここまで。
以下訳注に出てきた馴染みのないことばの説明です。

※1 hoisting(巻き上げる)とは

x = 5; //xに5を代入
var x; //xを初期化
console.log('x: ' + x); // x: 5

上記の例のように var より前に変数に代入し、その後 var x で初期化したように見えるのに、実際に行われる処理は var x = 5;と同じような動作になるJavaScriptの仕様のこと。
日本語で何て表現するのかはしらないが、hoisting と呼ぶらしい。
詳細はMozillaのドキュメントを参照のこと。

※2 Tree-Shakenとは

Minification is not enough, you need tree shaking
http://blog.sethladd.com/2013/01/minification-is-not-enough-you-need.html

引用:
Dart tools support tree shaking, a technique to "shake" off unused code, thus shrinking the size of the deployed application. I can import rich libraries chock full of useful goodness into my application, but only the functions I actually use will be included in my generated output. Awesome!

訳:
Dartのツールは tree shaking をサポートしていて、これは使ってないコードを振り落とすテクニックです。
これによってデプロイ時のアプリケーションサイズが小さくなります。
ぎっしりと有用な機能がつまったリッチなライブラリを自分のアプリケーションにインポートした場合も、生成されるコードには、実際に使っている部分だけが含まれます。素晴らしい!

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?